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
@@ -2,6 +2,8 @@ import { type DesignSections } from "./design-package.js";
2
2
  export interface SectionLookCatalogEntry {
3
3
  /** The look group, in registry order (D2). */
4
4
  readonly look: readonly string[];
5
+ /** The type's rendering component (#7743 D6 — what the site manifest records), when the catalog names one. */
6
+ readonly component: string | undefined;
5
7
  /** The type's `data` JSON Schema `properties` (the catalog's `schema.properties`). */
6
8
  readonly properties: Readonly<Record<string, unknown>>;
7
9
  }
@@ -37,6 +39,9 @@ export interface LocatedDataEntry {
37
39
  export interface LocatedSection {
38
40
  readonly type: string;
39
41
  readonly form: "flow" | "block";
42
+ /** #7743 D3 — the element's `[start, end)` span: flow, its `{ … }`; block, from its `- ` to its last line's end. */
43
+ readonly start: number;
44
+ readonly end: number;
40
45
  /** Flow: the index of `data`'s `{`. Block: the end of the `data:` line (before its newline). */
41
46
  readonly dataOpen: number;
42
47
  /** Flow: the index of `data`'s `}`. Block: `dataOpen`. */
@@ -45,6 +50,15 @@ export interface LocatedSection {
45
50
  readonly entryColumn: number;
46
51
  readonly entries: readonly LocatedDataEntry[];
47
52
  }
53
+ /**
54
+ * A page's located sections, with the `[valueStart, valueEnd)` span of the
55
+ * whole `sections:` value (#7743 D3): from just after `sections:` (and the
56
+ * spaces after it) to the flow form's `]`, or the block form's last line end.
57
+ */
58
+ export type LocatedSections = LocatedSection[] & {
59
+ readonly valueStart: number;
60
+ readonly valueEnd: number;
61
+ };
48
62
  /**
49
63
  * Locate a page's `sections[]` in its frontmatter — the prettier-formatted
50
64
  * flow form (`sections:\n [ { "id": …, "type": …, "data": { … } }, … ]`)
@@ -52,7 +66,7 @@ export interface LocatedSection {
52
66
  * Undefined when the page has no frontmatter or no column-0 `sections:` key.
53
67
  * Any other shape throws `TemplateError` naming the element (D5).
54
68
  */
55
- export declare function locatePageSections(text: string, file: string): LocatedSection[] | undefined;
69
+ export declare function locatePageSections(text: string, file: string): LocatedSections | undefined;
56
70
  export interface PresetPage {
57
71
  readonly path: string;
58
72
  readonly text: string;
@@ -64,6 +78,8 @@ export interface PresetApplication {
64
78
  readonly sectionType: string;
65
79
  readonly field: string;
66
80
  }
81
+ /** The frontmatter's column-0 `type:` value, or undefined when it has none. */
82
+ export declare function pageTypeOf(text: string, file: string): string | undefined;
67
83
  /**
68
84
  * Set one look field on section `index` of a page, splicing only its value
69
85
  * span (D5): an existing value is replaced; an absent field is inserted as
@@ -44,6 +44,7 @@ export function readSectionLookCatalog(catalogPath) {
44
44
  const properties = entry.schema["properties"];
45
45
  out[type] = {
46
46
  look: entry.look.map(String),
47
+ component: typeof entry.component === "string" ? entry.component : undefined,
47
48
  properties: isPlainObject(properties) ? properties : {},
48
49
  };
49
50
  }
@@ -397,6 +398,8 @@ function locateFlow(text, open, end, file) {
397
398
  out.push({
398
399
  type,
399
400
  form: "flow",
401
+ start: element.open,
402
+ end: element.close + 1,
400
403
  dataOpen: data.open,
401
404
  dataClose: data.close,
402
405
  entryColumn: -1,
@@ -410,7 +413,7 @@ function locateFlow(text, open, end, file) {
410
413
  }
411
414
  if (text[i] !== "]")
412
415
  cannotLocate(file, undefined, "the closing ]");
413
- return out;
416
+ return { sections: out, close: i };
414
417
  }
415
418
  function splitLines(text, start, end) {
416
419
  const lines = [];
@@ -510,9 +513,12 @@ function locateBlock(text, lines, file) {
510
513
  lastDeep = l.end;
511
514
  }
512
515
  close();
516
+ const last = item.filter((l) => !l.blank).at(-1);
513
517
  return {
514
518
  type,
515
519
  form: "block",
520
+ start: item[0].start + itemIndent,
521
+ end: last.end,
516
522
  dataOpen: data.end,
517
523
  dataClose: data.end,
518
524
  entryColumn,
@@ -545,20 +551,26 @@ export function locatePageSections(text, file) {
545
551
  const nextKey = /\n(?=[^\s#-])/.exec(text.slice(afterKey, fmEnd));
546
552
  const valueEnd = nextKey ? afterKey + nextKey.index : fmEnd;
547
553
  const first = skipSpace(text, afterKey, valueEnd);
548
- if (text[first] === "[")
549
- return locateFlow(text, first, valueEnd, file);
554
+ if (text[first] === "[") {
555
+ const { sections, close } = locateFlow(text, first, valueEnd, file);
556
+ return Object.assign(sections, { valueStart: afterKey, valueEnd: close + 1 });
557
+ }
550
558
  const lineEnd = text.indexOf("\n", afterKey);
551
559
  if (lineEnd >= 0 && text.slice(afterKey, lineEnd).trim() === "") {
552
560
  const lines = splitLines(text, lineEnd + 1, valueEnd);
553
561
  const firstLine = lines.find((l) => !l.blank);
554
562
  if (firstLine === undefined || text.startsWith("- ", firstLine.start + firstLine.indent)) {
555
- return locateBlock(text, lines, file);
563
+ const lastLine = lines.filter((l) => !l.blank).at(-1);
564
+ return Object.assign(locateBlock(text, lines, file), {
565
+ valueStart: afterKey,
566
+ valueEnd: lastLine?.end ?? afterKey,
567
+ });
556
568
  }
557
569
  }
558
570
  return cannotLocate(file, undefined, "a flow or block sequence");
559
571
  }
560
572
  /** The frontmatter's column-0 `type:` value, or undefined when it has none. */
561
- function pageTypeOf(text, file) {
573
+ export function pageTypeOf(text, file) {
562
574
  const fm = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?$/m.exec(text);
563
575
  if (!fm || fm.index !== 0)
564
576
  return undefined;
@@ -1,6 +1,5 @@
1
1
  import { type CompiledDesignTheme } from "./design-compile.js";
2
2
  import { type DesignPackage } from "./design-package.js";
3
- import type { LayerOwnership } from "./lib/extract-module-bundle.js";
4
3
  /** One resolved `--design` reference (D2). */
5
4
  export interface ResolvedDesign {
6
5
  /** "name" when the reference was a registry name, "path" when a directory. */
@@ -38,9 +37,12 @@ export declare function resolveDesign(ref: string, opts: {
38
37
  }): ResolvedDesign;
39
38
  /**
40
39
  * Render a compiled theme's `theme:` frontmatter block (D5) — `theme:`, each
41
- * treatment present as a bare `key: value`, then `colors:` with `light:`/
42
- * `dark:` sub-blocks, each colour slot as `slot: <JSON.stringify(value)>` in
43
- * `DESIGN_COLOR_MAP`'s order. Ends with a trailing newline.
40
+ * treatment present as a bare `key: value` (#7739's `darkInk`/`figures` after
41
+ * `checkmarks`), the `typography:` group when compiled — every role and field
42
+ * in the compile's order, role keys unquoted, sizes as plain scalars (#7720
43
+ * D2, #7739 D2.5) — then `colors:` with `light:`/`dark:` sub-blocks, each
44
+ * colour slot as `slot: <JSON.stringify(value)>` in `DESIGN_COLOR_MAP`'s
45
+ * order. Ends with a trailing newline.
44
46
  */
45
47
  export declare function renderThemeBlock(theme: CompiledDesignTheme): string;
46
48
  /**
@@ -53,24 +55,3 @@ export declare function renderThemeBlock(theme: CompiledDesignTheme): string;
53
55
  * @throws TemplateError when `text` carries no frontmatter to write into.
54
56
  */
55
57
  export declare function spliceThemeBlock(text: string, theme: CompiledDesignTheme, file: string): string;
56
- /**
57
- * Install a design's `sample/` directory onto a scaffold (spec 6892 D4) — a
58
- * FIXED, undeclared convention, not a design-package field: `sample/content/**`
59
- * lands at the scaffold's `content/**`, `sample/public/**` at
60
- * `apps/web/public/**`. A design with no `sample/` directory (e.g. `standard`)
61
- * installs nothing.
62
- *
63
- * Refuses — before its first write, so a refused sample leaves `targetDir`
64
- * untouched — any file outside those two roots, or any `.ts`/`.tsx`/`.js`/
65
- * `.mjs` file (content only, the template §5.2 rule this design package
66
- * amendment carries over). Every installed file's destination is recorded in
67
- * `ownership` as `design:<name>` (`readDesignPackage(designDir).name`), the
68
- * same call-scoped registry `installBundleTree` writes into.
69
- *
70
- * @throws Error on a file outside `content/`/`public/`, or a forbidden extension.
71
- */
72
- export declare function installDesignSample(opts: {
73
- designDir: string;
74
- targetDir: string;
75
- ownership: LayerOwnership;
76
- }): void;
@@ -10,8 +10,8 @@
10
10
  * directory it reads is supplied by the caller — so it stays importable from
11
11
  * Jest's CJS transform, unlike `programmatic.ts`.
12
12
  */
13
- import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync, } from "node:fs";
14
- import { join, relative, resolve, sep } from "node:path";
13
+ import { statSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
15
  import { isDeepStrictEqual } from "node:util";
16
16
  import { DESIGN_COLOR_MAP } from "./design-compile.js";
17
17
  import { readDesignPackage } from "./design-package.js";
@@ -82,9 +82,12 @@ export function resolveDesign(ref, opts) {
82
82
  const RUNTIME_COLOR_SLOT_ORDER = DESIGN_COLOR_MAP.filter((row) => row.mode === "light").map((row) => row.runtime);
83
83
  /**
84
84
  * Render a compiled theme's `theme:` frontmatter block (D5) — `theme:`, each
85
- * treatment present as a bare `key: value`, then `colors:` with `light:`/
86
- * `dark:` sub-blocks, each colour slot as `slot: <JSON.stringify(value)>` in
87
- * `DESIGN_COLOR_MAP`'s order. Ends with a trailing newline.
85
+ * treatment present as a bare `key: value` (#7739's `darkInk`/`figures` after
86
+ * `checkmarks`), the `typography:` group when compiled — every role and field
87
+ * in the compile's order, role keys unquoted, sizes as plain scalars (#7720
88
+ * D2, #7739 D2.5) — then `colors:` with `light:`/`dark:` sub-blocks, each
89
+ * colour slot as `slot: <JSON.stringify(value)>` in `DESIGN_COLOR_MAP`'s
90
+ * order. Ends with a trailing newline.
88
91
  */
89
92
  export function renderThemeBlock(theme) {
90
93
  const lines = ["theme:"];
@@ -96,6 +99,24 @@ export function renderThemeBlock(theme) {
96
99
  lines.push(` placeholders: ${theme.placeholders}`);
97
100
  if (theme.checkmarks !== undefined)
98
101
  lines.push(` checkmarks: ${theme.checkmarks}`);
102
+ if (theme.buttonShape !== undefined)
103
+ lines.push(` buttonShape: ${theme.buttonShape}`);
104
+ if (theme.darkInk !== undefined)
105
+ lines.push(` darkInk: ${theme.darkInk}`);
106
+ if (theme.figures !== undefined)
107
+ lines.push(` figures: ${theme.figures}`);
108
+ if (theme.typography !== undefined) {
109
+ lines.push(" typography:");
110
+ for (const [role, fields] of Object.entries(theme.typography)) {
111
+ lines.push(` ${role}:`);
112
+ for (const [field, value] of Object.entries(fields)) {
113
+ lines.push(` ${field}: ${String(value)}`);
114
+ }
115
+ }
116
+ }
117
+ // #7719 D6 — present only for a design that uses `solid-dark`, or (#7739 D2.3) sets `darkInk: accent`.
118
+ if (theme.darkBands !== undefined)
119
+ lines.push(` darkBands: ${theme.darkBands}`);
99
120
  lines.push(" colors:");
100
121
  for (const mode of ["light", "dark"]) {
101
122
  lines.push(` ${mode}:`);
@@ -149,72 +170,3 @@ export function spliceThemeBlock(text, theme, file) {
149
170
  : [...lines.slice(0, themeStart), ...rendered, ...lines.slice(themeEnd)];
150
171
  return next.join("\n");
151
172
  }
152
- /** Forbidden extensions for a design sample's files (spec 6892 D4) — content only, never code. */
153
- const SAMPLE_FORBIDDEN_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs"];
154
- function hasSampleForbiddenExtension(p) {
155
- return SAMPLE_FORBIDDEN_EXTENSIONS.some((ext) => p.toLowerCase().endsWith(ext));
156
- }
157
- /** Every file under `dir`, as `/`-separated paths relative to `dir`. */
158
- function listFiles(dir) {
159
- const out = [];
160
- const stack = [dir];
161
- for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
162
- for (const entry of readdirSync(cur, { withFileTypes: true })) {
163
- const abs = join(cur, entry.name);
164
- if (entry.isDirectory())
165
- stack.push(abs);
166
- else if (entry.isFile())
167
- out.push(relative(dir, abs).split(sep).join("/"));
168
- }
169
- }
170
- return out;
171
- }
172
- /** Where one `sample/` file lands on a scaffold (spec 6892 D4's fixed convention). */
173
- function sampleDestination(rel) {
174
- if (rel.startsWith("content/"))
175
- return rel;
176
- return `apps/web/public/${rel.slice("public/".length)}`;
177
- }
178
- /**
179
- * Install a design's `sample/` directory onto a scaffold (spec 6892 D4) — a
180
- * FIXED, undeclared convention, not a design-package field: `sample/content/**`
181
- * lands at the scaffold's `content/**`, `sample/public/**` at
182
- * `apps/web/public/**`. A design with no `sample/` directory (e.g. `standard`)
183
- * installs nothing.
184
- *
185
- * Refuses — before its first write, so a refused sample leaves `targetDir`
186
- * untouched — any file outside those two roots, or any `.ts`/`.tsx`/`.js`/
187
- * `.mjs` file (content only, the template §5.2 rule this design package
188
- * amendment carries over). Every installed file's destination is recorded in
189
- * `ownership` as `design:<name>` (`readDesignPackage(designDir).name`), the
190
- * same call-scoped registry `installBundleTree` writes into.
191
- *
192
- * @throws Error on a file outside `content/`/`public/`, or a forbidden extension.
193
- */
194
- export function installDesignSample(opts) {
195
- const sampleDir = join(opts.designDir, "sample");
196
- if (!existsSync(sampleDir))
197
- return;
198
- const name = readDesignPackage(opts.designDir).name;
199
- const subject = `design:${name}`;
200
- // README.md at the sample root is the ONE documented exception (spec 6892
201
- // D5: "the narrative sections move to designs/<name>/sample/README.md,
202
- // which is not installed") — every other file is real content and is
203
- // validated against the two roots below.
204
- const files = listFiles(sampleDir).filter((rel) => rel !== "README.md");
205
- for (const rel of files) {
206
- if (!rel.startsWith("content/") && !rel.startsWith("public/")) {
207
- throw new Error(`${subject}: sample file "${rel}" is outside content/ and public/ (spec 6892 D4)`);
208
- }
209
- if (hasSampleForbiddenExtension(rel)) {
210
- throw new Error(`${subject}: sample file "${rel}" is a code file — a design sample is content only (spec 6892 D4)`);
211
- }
212
- }
213
- for (const rel of files) {
214
- const destinationRel = sampleDestination(rel);
215
- const destinationAbs = resolve(opts.targetDir, destinationRel);
216
- mkdirSync(join(destinationAbs, ".."), { recursive: true });
217
- copyFileSync(join(sampleDir, rel), destinationAbs);
218
- opts.ownership.set(destinationRel, subject);
219
- }
220
- }
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@ export { CATALOG_TARGET_PATH, FREE_MODULES, addModule, LicenseRequiredError, PAI
9
9
  export { DEFAULT_AILK_MODULE_REGISTRY_URL, FetchModuleError, fetchModule, type FetchModuleOptions, } from "./lib/fetch-module.js";
10
10
  export { extractModuleBundle, type ExistingFilePolicy, type ExtractModuleBundleOptions, type InstallReportRow, } from "./lib/extract-module-bundle.js";
11
11
  export { ADD_MODULE_HELP, AddModuleRefusedError, applyModuleToScaffold, formatAddModuleReport, parseAddModuleArgs, type AddModuleReportRow, type AddModuleResult, type ApplyModuleOptions, } from "./add-module.js";
12
- export { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, type ApplyDesignArgs, type ApplyDesignChange, type ApplyDesignOptions, type ApplyDesignResult, } from "./apply-design.js";
12
+ export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, type ApplyDesignArgs, type ApplyDesignChange, type ApplyDesignOptions, type ApplyDesignResult, } from "./apply-design.js";
13
+ export type { HomeSeedOutcome } from "./design-home.js";
13
14
  export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
14
15
  export { SYNC_ROUTES_HELP, formatSyncRoutesReport, listTemplateRouteDirs, parseSyncRoutesArgs, syncRoutes, type RouteReport, type SyncRoutesOptions, type SyncRoutesResult, } from "./sync-routes.js";
15
16
  export { deriveArchitecture } from "./derive-architecture.js";
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ export { extractModuleBundle, } from "./lib/extract-module-bundle.js";
16
16
  export { ADD_MODULE_HELP, AddModuleRefusedError, applyModuleToScaffold, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
17
17
  // `create-ailk apply-design` (#6819) — make an existing site wear a design,
18
18
  // changing only its look. The pure core, beside `add-module`'s.
19
- export { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
19
+ export { APPLY_DESIGN_HELP, UnreadChromeError, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
20
20
  // The site's page roster of record and its install-diff check (#6682).
21
21
  export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
22
22
  // `create-ailk sync-routes` (#4986) — the template-route drift check + copy.
@@ -0,0 +1,21 @@
1
+ import { type ResolvedSiteChrome } from "../site-chrome.js";
2
+ /** One knob a design sets that one of the site's reader files does not read. */
3
+ export interface UnreadChromeKnob {
4
+ knob: string;
5
+ /** Overlay-relative path of the reader file, with `/` separators. */
6
+ file: string;
7
+ }
8
+ /** The pattern that means "this text reads `knob`". */
9
+ export declare function chromeReadPattern(knob: string): RegExp;
10
+ /**
11
+ * knob → the overlay files that read it, sorted. Scans `<templatesDir>/apps/web`;
12
+ * fails closed (throws, naming the directory) when it is absent or unreadable,
13
+ * so a broken overlay can never read as "nothing reads any knob".
14
+ */
15
+ export declare function chromeKnobReaders(templatesDir: string): Map<string, string[]>;
16
+ /**
17
+ * The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
18
+ * the shipped overlay's `file` reads it, the site has its own `file`, and the
19
+ * site's `file` does not read it. A reader file the site lacks is skipped.
20
+ */
21
+ export declare function unreadChromeKnobs(resolved: ResolvedSiteChrome, readers: ReadonlyMap<string, readonly string[]>, siteDir: string): UnreadChromeKnob[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Which generated app files read which chrome knob (#7781, spec
3
+ * docs/specs/apply-design-unread-chrome-spec.md).
4
+ *
5
+ * `apply-design` writes a design's chrome knobs into the site's generated
6
+ * `apps/web/lib/site-chrome.ts`; other generated files read them. A site
7
+ * scaffolded before a knob existed has a reader file that ignores it, so the
8
+ * design is half-applied. These pure functions find that gap:
9
+ *
10
+ * - `chromeKnobReaders` derives, from the shipped overlay, the files that read
11
+ * each knob — nothing is hard-coded, so a new reader file is picked up.
12
+ * - `unreadChromeKnobs` compares the site's own copy of each reader file.
13
+ *
14
+ * "Reads `knob`" means the file's text matches `(?:siteChrome|chrome)\.<knob>\b`.
15
+ * A file that reads a knob under another name (a destructured alias) is not
16
+ * recognised — accepted (spec § Clarifications, detection).
17
+ */
18
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { DEFAULT_SITE_CHROME, } from "../site-chrome.js";
21
+ /** The overlay tree the generated app files live under. */
22
+ const OVERLAY_WEB_DIR = ["apps", "web"];
23
+ /** What precedes a knob name in a read: `siteChrome.` or `chrome.` — the one definition both the overlay scan and the per-site check use. */
24
+ const READ_HEAD = "(?:siteChrome|chrome)\\.";
25
+ /** The pattern that means "this text reads `knob`". */
26
+ export function chromeReadPattern(knob) {
27
+ return new RegExp(`${READ_HEAD}${knob.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
28
+ }
29
+ /** Every `.ts`/`.tsx` file under `dir` outside a `__tests__` segment, as `/`-joined paths relative to `root`. */
30
+ function listSources(root, dir, out) {
31
+ for (const entry of readdirSync(join(root, dir), { withFileTypes: true })) {
32
+ const rel = `${dir}/${entry.name}`;
33
+ if (entry.isDirectory()) {
34
+ if (entry.name !== "__tests__")
35
+ listSources(root, rel, out);
36
+ }
37
+ else if (/\.tsx?$/.test(entry.name)) {
38
+ out.push(rel);
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * knob → the overlay files that read it, sorted. Scans `<templatesDir>/apps/web`;
44
+ * fails closed (throws, naming the directory) when it is absent or unreadable,
45
+ * so a broken overlay can never read as "nothing reads any knob".
46
+ */
47
+ export function chromeKnobReaders(templatesDir) {
48
+ const webDir = join(templatesDir, ...OVERLAY_WEB_DIR);
49
+ const readers = new Map();
50
+ try {
51
+ if (!existsSync(webDir))
52
+ throw new Error("missing");
53
+ const files = [];
54
+ listSources(templatesDir, OVERLAY_WEB_DIR.join("/"), files);
55
+ for (const file of files.sort()) {
56
+ const text = readFileSync(join(templatesDir, file), "utf8");
57
+ for (const match of text.matchAll(new RegExp(`${READ_HEAD}([A-Za-z_$][\\w$]*)\\b`, "g"))) {
58
+ const knob = match[1];
59
+ const list = readers.get(knob) ?? [];
60
+ if (!list.includes(file))
61
+ list.push(file);
62
+ readers.set(knob, list);
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ throw new Error(`cannot check chrome knobs: the overlay has no readable apps/web at ${webDir}. Reinstall create-ailk.`);
68
+ }
69
+ return readers;
70
+ }
71
+ /** A knob is set when it differs from its default; an optional knob (no default) when present. */
72
+ function isSet(resolved, knob) {
73
+ const value = resolved[knob];
74
+ if (value === undefined)
75
+ return false;
76
+ if (!(knob in DEFAULT_SITE_CHROME))
77
+ return true;
78
+ return (value !== DEFAULT_SITE_CHROME[knob]);
79
+ }
80
+ /**
81
+ * The `(knob, file)` pairs where the resolved chrome sets a non-default `knob`,
82
+ * the shipped overlay's `file` reads it, the site has its own `file`, and the
83
+ * site's `file` does not read it. A reader file the site lacks is skipped.
84
+ */
85
+ export function unreadChromeKnobs(resolved, readers, siteDir) {
86
+ const out = [];
87
+ for (const knob of Object.keys(resolved).sort()) {
88
+ if (!isSet(resolved, knob))
89
+ continue;
90
+ const pattern = chromeReadPattern(knob);
91
+ for (const file of readers.get(knob) ?? []) {
92
+ const abs = join(siteDir, file);
93
+ if (!existsSync(abs))
94
+ continue;
95
+ if (!pattern.test(readFileSync(abs, "utf8")))
96
+ out.push({ knob, file });
97
+ }
98
+ }
99
+ return out;
100
+ }
@@ -46,7 +46,18 @@ export type JsonLdDriftVerdict = {
46
46
  changed: DriftNode[];
47
47
  reason: string;
48
48
  };
49
- /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
49
+ /**
50
+ * `after` with the standard home put back when the seeded home's JSON-LD is
51
+ * exactly the standard home's node followed by one `Person` per
52
+ * `TEAM_SEED_MEMBERS` member, in order (the people slide's `team` section,
53
+ * #7744 D8). Any other home, or a home the people slide did not change, is
54
+ * returned as it is.
55
+ */
56
+ export declare function admitSeededPeopleSlide(before: ComposedPages, after: ComposedPages): ComposedPages;
57
+ /**
58
+ * Decide whether `apply-design <design>` left every page's JSON-LD unchanged
59
+ * (logo excluded), the seeded people slide's Persons admitted.
60
+ */
50
61
  export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
51
62
  export interface MeasureApplyDesignDriftOptions {
52
63
  /** create-ailk's built `dist/cli.js`. */
@@ -16,10 +16,17 @@
16
16
  * touches the filesystem and processes only through the paths it is handed,
17
17
  * so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
18
18
  * procedure. Nothing here imports `@working-theory/theme`.
19
+ *
20
+ * One change is content, not drift (#7744 D8): a design that seeds an
21
+ * organization home's people slide adds one `Person` per seeded team member to
22
+ * the home's graph. `admitSeededPeopleSlide` puts the standard home back only
23
+ * when the seeded home is exactly that, so any other change still counts.
19
24
  */
20
25
  import { execFileSync } from "node:child_process";
21
26
  import { writeFileSync } from "node:fs";
22
27
  import { join } from "node:path";
28
+ import { isDeepStrictEqual } from "node:util";
29
+ import { TEAM_SEED_MEMBERS } from "../team-profiles.js";
23
30
  function describePair(row) {
24
31
  const measured = row.status === "unmeasurable" || row.ratio === null
25
32
  ? "unmeasurable"
@@ -107,15 +114,44 @@ export function jsonLdDrift(before, after) {
107
114
  const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
108
115
  return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
109
116
  }
117
+ const HOME_PAGE = "content/en/HomePage/home.mdx";
118
+ /**
119
+ * `after` with the standard home put back when the seeded home's JSON-LD is
120
+ * exactly the standard home's node followed by one `Person` per
121
+ * `TEAM_SEED_MEMBERS` member, in order (the people slide's `team` section,
122
+ * #7744 D8). Any other home, or a home the people slide did not change, is
123
+ * returned as it is.
124
+ */
125
+ export function admitSeededPeopleSlide(before, after) {
126
+ const standard = before[HOME_PAGE];
127
+ const seeded = after[HOME_PAGE];
128
+ if (standard === undefined || seeded === undefined || !seeded.sectionsValid)
129
+ return after;
130
+ const graph = JSON.parse(seeded.jsonld)["@graph"];
131
+ if (!Array.isArray(graph))
132
+ return after;
133
+ const [page, ...people] = graph;
134
+ const expected = TEAM_SEED_MEMBERS.map((m) => ({
135
+ "@context": "https://schema.org",
136
+ "@type": "Person",
137
+ name: m.name,
138
+ jobTitle: m.role,
139
+ }));
140
+ const isPeopleSlide = isDeepStrictEqual(page, JSON.parse(standard.jsonld)) && isDeepStrictEqual(people, expected);
141
+ return isPeopleSlide ? { ...after, [HOME_PAGE]: standard } : after;
142
+ }
110
143
  /** How many changed nodes a drift reason lists before it stops. */
111
144
  const REASON_NODE_LIMIT = 10;
112
- /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
145
+ /**
146
+ * Decide whether `apply-design <design>` left every page's JSON-LD unchanged
147
+ * (logo excluded), the seeded people slide's Persons admitted.
148
+ */
113
149
  export function assessJsonLdDrift(design, before, after) {
114
150
  const pages = Object.keys(before).length;
115
151
  if (pages === 0) {
116
152
  return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
117
153
  }
118
- const changed = jsonLdDrift(before, after);
154
+ const changed = jsonLdDrift(before, admitSeededPeopleSlide(before, after));
119
155
  if (changed.length === 0)
120
156
  return { ok: true, pages };
121
157
  const listed = changed
@@ -49,6 +49,8 @@ export interface ExtractModuleBundleOptions {
49
49
  existingFilePolicy?: ExistingFilePolicy;
50
50
  /** A caller check on the manifest's `patches` (#6653 D3). See `InstallBundleTreeOptions`. */
51
51
  checkPatches?: (patches: readonly ModulePatch[]) => void;
52
+ /** Destinations this scaffold does not carry (#7426 D4). See `InstallBundleTreeOptions`. */
53
+ skip?: (destination: string) => boolean;
52
54
  }
53
55
  /**
54
56
  * Extract a fetched module bundle into `targetDir`, merging its files onto
@@ -131,6 +133,15 @@ export interface InstallBundleTreeOptions {
131
133
  * bundle's patches to the plus-config allowlist.
132
134
  */
133
135
  checkPatches?: (patches: readonly ModulePatch[]) => void;
136
+ /**
137
+ * Scaffold-relative destinations this scaffold does not carry (#7426 D4) —
138
+ * a spoke passes `topology.ts` `isSpokeExcluded`. A skipped destination is
139
+ * never written: a plain file or an `installMap` destination it names is
140
+ * dropped, and so is a `patches` entry whose target it names. It is NOT an
141
+ * ownership claim, and a skipped patch target is not a missing one. Every
142
+ * entry is still validated as before. Absent ⟹ nothing is skipped.
143
+ */
144
+ skip?: (destination: string) => boolean;
134
145
  }
135
146
  /** See `InstallBundleTreeOptions.existingFilePolicy`. */
136
147
  export type ExistingFilePolicy = "overwrite" | "skip-existing";
@@ -385,6 +385,24 @@ function validateLayerOwnership(destinations, ownership, subject, isTemplate, pa
385
385
  }
386
386
  }
387
387
  }
388
+ /** Every regular file under `srcRoot`, as a `/`-separated relative path. */
389
+ function stagedFiles(srcRoot) {
390
+ const out = [];
391
+ const stack = [srcRoot];
392
+ while (stack.length > 0) {
393
+ const dir = stack.pop();
394
+ if (dir === undefined)
395
+ break;
396
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
397
+ const srcPath = join(dir, entry.name);
398
+ if (entry.isDirectory())
399
+ stack.push(srcPath);
400
+ else if (entry.isFile())
401
+ out.push(relative(srcRoot, srcPath).split(sep).join("/"));
402
+ }
403
+ }
404
+ return out;
405
+ }
388
406
  /**
389
407
  * Every scaffold-relative destination this tree will write: each unmapped
390
408
  * staged file at its own path (exactly the set `copyTree` lands, same walk,
@@ -457,6 +475,7 @@ export async function extractModuleBundle(options) {
457
475
  ...(options.checkPatches !== undefined
458
476
  ? { checkPatches: options.checkPatches }
459
477
  : {}),
478
+ ...(options.skip !== undefined ? { skip: options.skip } : {}),
460
479
  });
461
480
  }
462
481
  finally {
@@ -484,13 +503,18 @@ export function installBundleTree(options) {
484
503
  const manifest = isTemplate
485
504
  ? readAilkTemplateManifest(sourceRoot, name, templateRequires, templateExtends)
486
505
  : readAilkModuleManifest(sourceRoot);
487
- const installMap = manifest?.installMap ?? {};
488
- validateInstallMap(installMap, sourceRoot, targetAbs, subject, isTemplate);
506
+ const declaredInstallMap = manifest?.installMap ?? {};
507
+ validateInstallMap(declaredInstallMap, sourceRoot, targetAbs, subject, isTemplate);
508
+ // #7426 D4 — a destination the scaffold does not carry is dropped here,
509
+ // after validation, so a spoke never receives it and never records it.
510
+ const skip = options.skip;
511
+ const isSkipped = (destination) => skip !== undefined && skip(normalize(destination).split(sep).join("/"));
512
+ const installMap = Object.fromEntries(Object.entries(declaredInstallMap).filter(([, destination]) => !isSkipped(destination)));
489
513
  // patches validation (spec 5418 § 3 D6) runs at the same point, before
490
514
  // any copy — a bad target/kind/op rejects the whole tree with nothing
491
515
  // written to targetDir, same posture as installMap and the tar-entry
492
516
  // validation above.
493
- const patches = manifest?.patches ?? [];
517
+ const declaredPatches = manifest?.patches ?? [];
494
518
  // #5514 (Medium) — content, never code (spec 5462 D9/D16): every `patches`
495
519
  // kind either edits a code file directly (`next-config`, `jest-config`)
496
520
  // or a manifest a template has no legitimate reason to touch
@@ -498,10 +522,13 @@ export function installBundleTree(options) {
498
522
  // content template) — no shipped template uses it, so the simplest
499
523
  // fail-closed reading refuses it outright rather than enumerating an
500
524
  // allowed subset that has no current user.
501
- if (isTemplate && patches.length > 0) {
525
+ if (isTemplate && declaredPatches.length > 0) {
502
526
  throw new Error(`${subject}: a template must not carry patches — content, never code (spec 5462 D9)`);
503
527
  }
504
- validatePatches(patches, targetAbs, subject);
528
+ // Every declared patch is validated, skipped or not (#7426 D4), exactly as
529
+ // the installMap is above; only then are the skipped ones dropped.
530
+ validatePatches(declaredPatches, targetAbs, subject);
531
+ const patches = declaredPatches.filter((patch) => !isSkipped(patch.target));
505
532
  options.checkPatches?.(patches);
506
533
  // The exclusion set copyTree/the content-only guard both use — built from
507
534
  // NORMALIZED installMap keys (#5514) so a "./x" key excludes the same
@@ -512,7 +539,10 @@ export function installBundleTree(options) {
512
539
  const excluded = new Set([
513
540
  AILK_TEMPLATE_JSON,
514
541
  AILK_MODULE_JSON,
515
- ...Object.keys(installMap).map((key) => normalize(key).split(sep).join("/")),
542
+ ...Object.keys(declaredInstallMap).map((key) => normalize(key).split(sep).join("/")),
543
+ // A plain file lands at its own staged path, so that path IS its
544
+ // destination: a skipped one is excluded from the copy like a mapped source.
545
+ ...(skip !== undefined ? stagedFiles(sourceRoot).filter(isSkipped) : []),
516
546
  ]);
517
547
  if (isTemplate) {
518
548
  // #6658 D7 — a template's roster of record lands as JSON, through its
@@ -20,5 +20,10 @@ export declare function appendLine(text: string, line: string): string;
20
20
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
21
21
  * `ailk.key` when no existing line covers it. Idempotent — a second call with
22
22
  * the same key changes no byte.
23
+ *
24
+ * `apiEnv: false` skips the `apps/api/.env` line — a spoke (#7426 D6) has no
25
+ * `apps/api`, so it keeps the key in `ailk.key` alone.
23
26
  */
24
- export declare function writeLicenseKeyFiles(dir: string, key: string): void;
27
+ export declare function writeLicenseKeyFiles(dir: string, key: string, opts?: {
28
+ apiEnv?: boolean;
29
+ }): void;