create-ailk 0.8.0 → 0.9.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 (211) hide show
  1. package/README.md +68 -0
  2. package/component-catalog.json +279 -45
  3. package/dist/add-module.d.ts +20 -0
  4. package/dist/add-module.js +43 -7
  5. package/dist/add-route.d.ts +56 -0
  6. package/dist/add-route.js +286 -0
  7. package/dist/apply-design-md.d.ts +7 -0
  8. package/dist/apply-design-md.js +50 -0
  9. package/dist/apply-design.d.ts +64 -0
  10. package/dist/apply-design.js +273 -0
  11. package/dist/claude-md.d.ts +111 -0
  12. package/dist/claude-md.js +334 -0
  13. package/dist/cli.js +92 -4
  14. package/dist/derive-architecture.js +41 -36
  15. package/dist/design-compile.d.ts +71 -0
  16. package/dist/design-compile.js +98 -0
  17. package/dist/design-package.d.ts +83 -0
  18. package/dist/design-package.js +468 -0
  19. package/dist/design-presets.d.ts +86 -0
  20. package/dist/design-presets.js +642 -0
  21. package/dist/design-scaffold.d.ts +76 -0
  22. package/dist/design-scaffold.js +220 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +4 -1
  25. package/dist/legacy-nav-renderers.d.ts +45 -0
  26. package/dist/legacy-nav-renderers.js +545 -0
  27. package/dist/lib/auth-secret-file.d.ts +19 -0
  28. package/dist/lib/auth-secret-file.js +112 -0
  29. package/dist/lib/design-checks.d.ts +78 -0
  30. package/dist/lib/design-checks.js +174 -0
  31. package/dist/lib/license-key-files.d.ts +9 -0
  32. package/dist/lib/license-key-files.js +2 -2
  33. package/dist/lib/module-license-gate.d.ts +19 -9
  34. package/dist/lib/module-license-gate.js +19 -8
  35. package/dist/lib/render-verdict.d.ts +52 -0
  36. package/dist/lib/render-verdict.js +77 -0
  37. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  38. package/dist/lib/scaffold-render-sweep.js +236 -0
  39. package/dist/module-architecture.d.ts +19 -16
  40. package/dist/module-architecture.js +64 -21
  41. package/dist/parse-args.d.ts +12 -2
  42. package/dist/parse-args.js +74 -13
  43. package/dist/programmatic.d.ts +30 -6
  44. package/dist/programmatic.js +206 -17
  45. package/dist/site-chrome.d.ts +19 -9
  46. package/dist/site-chrome.js +23 -13
  47. package/dist/site-manifest.d.ts +25 -0
  48. package/dist/site-manifest.js +254 -0
  49. package/dist/site-roster.d.ts +41 -0
  50. package/dist/site-roster.js +141 -8
  51. package/dist/starter-grid.d.ts +32 -16
  52. package/dist/starter-grid.js +129 -33
  53. package/dist/surfaces.d.ts +41 -9
  54. package/dist/surfaces.js +200 -83
  55. package/dist/template-manifest.d.ts +30 -13
  56. package/dist/template-manifest.js +49 -24
  57. package/package.json +5 -4
  58. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  59. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  61. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  62. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  63. package/site-templates/designs/minimalism/sample/README.md +48 -0
  64. package/site-templates/designs/standard/DESIGN.md +98 -6
  65. package/site-templates/marketing-site/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  68. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  69. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  71. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  72. package/site-templates/saas-product/DESIGN.md +98 -6
  73. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  75. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  76. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  78. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  79. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  81. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  82. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  83. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  84. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  85. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  86. package/templates/.claude/rules/architecture.md +7 -0
  87. package/templates/.claude/skills/README.md +3 -2
  88. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  89. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  90. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  91. package/templates/.claude/skills/add-page/references/page-types.md +85 -76
  92. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  93. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  94. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  95. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  96. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  97. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  98. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  99. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  100. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  101. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +28 -11
  102. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  103. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  104. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  105. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  106. package/templates/.github/workflows/ci.yml +4 -0
  107. package/templates/.prettierignore +1 -0
  108. package/templates/CLAUDE.md +146 -103
  109. package/templates/DESIGN.md +98 -6
  110. package/templates/README.md +50 -38
  111. package/templates/apps/api/.env.example +2 -1
  112. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  113. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  114. package/templates/apps/api/src/lib/stripe.ts +9 -0
  115. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  116. package/templates/apps/api/src/middleware/auth.ts +54 -0
  117. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  118. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  119. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  120. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  121. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  122. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  123. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  124. package/templates/apps/api/src/server.ts +28 -0
  125. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  126. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  127. package/templates/apps/api/src/services/revalidation.ts +60 -15
  128. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  129. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  130. package/templates/apps/web/.env.example +13 -0
  131. package/templates/apps/web/CLAUDE.md +22 -28
  132. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  133. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  134. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  135. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  136. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  137. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  139. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  140. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  141. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  142. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  143. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  144. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  145. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  146. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  148. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  149. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  150. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  151. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  152. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  153. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  154. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  155. package/templates/apps/web/lib/locale-guard.ts +18 -0
  156. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  157. package/templates/apps/web/lib/nav-config.ts +14 -13
  158. package/templates/apps/web/lib/site-brand.tsx +13 -9
  159. package/templates/apps/web/lib/site-logo.tsx +36 -0
  160. package/templates/apps/web/lib/site-theme.ts +46 -26
  161. package/templates/apps/web/messages/en.json +23 -9
  162. package/templates/apps/web/middleware.ts +28 -4
  163. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  164. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  165. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  166. package/templates/content/en/BookCall/start-project.mdx +56 -0
  167. package/templates/content/en/ContactPage/contact.mdx +40 -1
  168. package/templates/content/en/HomePage/home.mdx +2 -6
  169. package/templates/content/en/LandingPage/meet.mdx +59 -0
  170. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  171. package/templates/database/CHANGELOG.md +7 -0
  172. package/templates/database/package.json +1 -1
  173. package/templates/package.json +2 -1
  174. package/templates/tools/check-claude-md.ts +128 -0
  175. package/versions.json +33 -33
  176. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  177. package/site-templates/minimalism-site/DESIGN.md +0 -162
  178. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  179. package/site-templates/minimalism-site/ailk-template.json +0 -23
  180. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  181. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  182. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  183. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  184. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  185. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  186. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  187. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  188. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  189. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  190. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  191. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  192. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  193. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  194. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  195. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  196. package/site-templates/minimalism-site/provenance.json +0 -4
  197. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  198. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  199. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  200. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  201. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  202. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  203. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  204. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  205. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  206. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  207. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  208. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  209. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  210. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  211. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * The scaffold's root `CLAUDE.md` as marked, self-hashing regions (#6904).
3
+ *
4
+ * A rendered `CLAUDE.md` is a run of template-owned regions, each opened by
5
+ * `<!-- ailk:template id="<id>" sha256="<hex>" -->` and closed by
6
+ * `<!-- /ailk:template id="<id>" -->`, followed by one `<!-- ailk:site -->`
7
+ * line that opens the site-owned section running to end of file (spec D1).
8
+ * Each begin marker records the SHA-256 of its own body (D2), so the drift
9
+ * check needs only the file.
10
+ *
11
+ * This is the core `--sync-claude-md` (`syncClaudeMdSync`, programmatic.ts)
12
+ * and the tests use. The scaffold's own drift check,
13
+ * `template-overlay/tools/check-claude-md.ts`, reimplements `findDrift` +
14
+ * `formatDrift` because a scaffold does not carry this source; the
15
+ * `claude-md-sync` suite holds the two to identical verdicts.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
+ import { basename, join, resolve } from "node:path";
20
+ import { buildScaffoldMarkers } from "./copy.js";
21
+ import { substituteMarkers } from "./substitute.js";
22
+ /** The drift check a scaffold ships (`template-overlay/tools/`). */
23
+ export const CLAUDE_MD_CHECK_PATH = "tools/check-claude-md.ts";
24
+ /**
25
+ * SHA-256 of every `templates/CLAUDE.md` a published create-ailk shipped
26
+ * before markers existed, with `{{PROJECT_NAME}}` unsubstituted (spec § 1.7,
27
+ * D4). An unmarked scaffold `CLAUDE.md` that hashes to one of these after
28
+ * undoing the name substitution was never edited by its owner.
29
+ */
30
+ export const LEGACY_TEMPLATE_SHA256 = [
31
+ // 0.1.1 – 0.6.0
32
+ "a47dd9f391f6773f44aa7d7a42aa936bf22dd5fd5e8420f5001340239aea5aae",
33
+ // 0.6.1
34
+ "32c6e3278b5a4663562a2af307e56c3d9b6e74b4e34af02b033975eefe50b5db",
35
+ // 0.7.0, 0.8.0
36
+ "9ce9295be23c7b5e2f87357c78753e6f609a551bf74c8b5f5e463897f523ee89",
37
+ ];
38
+ const BEGIN_RE = /^<!-- ailk:template id="([^"]*)"(?: sha256="([^"]*)")? -->$/;
39
+ const END_RE = /^<!-- \/ailk:template id="([^"]*)" -->$/;
40
+ const SITE_LINE = "<!-- ailk:site -->";
41
+ const ID_RE = /^[a-z][a-z0-9-]*$/;
42
+ const SHA_RE = /^[0-9a-f]{64}$/;
43
+ /** SHA-256, lowercase hex, of a region body with every CRLF folded to LF (D2). */
44
+ export function hashBody(body) {
45
+ return createHash("sha256").update(body.replace(/\r\n/g, "\n"), "utf8").digest("hex");
46
+ }
47
+ /** Parse a `CLAUDE.md` against the D1 grammar. Never throws. */
48
+ export function parseClaudeMd(text) {
49
+ const regions = [];
50
+ const faults = [];
51
+ const seen = new Set();
52
+ let open;
53
+ let siteStart;
54
+ let marked = false;
55
+ let pos = 0;
56
+ while (pos < text.length) {
57
+ const nl = text.indexOf("\n", pos);
58
+ const next = nl === -1 ? text.length : nl + 1;
59
+ const line = text.slice(pos, nl === -1 ? text.length : nl).replace(/\r$/, "");
60
+ if (line === SITE_LINE) {
61
+ marked = true;
62
+ siteStart = pos;
63
+ break;
64
+ }
65
+ const begin = BEGIN_RE.exec(line);
66
+ const end = END_RE.exec(line);
67
+ if (begin) {
68
+ marked = true;
69
+ const id = begin[1] ?? "";
70
+ const sha = begin[2];
71
+ if (!ID_RE.test(id))
72
+ faults.push(`invalid section id "${id}"`);
73
+ if (sha !== undefined && !SHA_RE.test(sha))
74
+ faults.push(`section "${id}" has a malformed sha256`);
75
+ if (open) {
76
+ faults.push(`nested region: section "${id}" opens inside section "${open.id}"`);
77
+ }
78
+ else {
79
+ if (seen.has(id))
80
+ faults.push(`duplicate section id "${id}"`);
81
+ seen.add(id);
82
+ open = { id, sha256: sha, start: pos, bodyStart: next };
83
+ }
84
+ }
85
+ else if (end) {
86
+ marked = true;
87
+ const id = end[1] ?? "";
88
+ if (!open) {
89
+ faults.push(`unbalanced marker: end of section "${id}" has no begin marker`);
90
+ }
91
+ else if (open.id !== id) {
92
+ faults.push(`unbalanced marker: section "${open.id}" is closed by an end marker for "${id}"`);
93
+ open = undefined;
94
+ }
95
+ else {
96
+ regions.push({
97
+ id,
98
+ sha256: open.sha256,
99
+ start: open.start,
100
+ end: next,
101
+ body: text.slice(open.bodyStart, pos),
102
+ });
103
+ open = undefined;
104
+ }
105
+ }
106
+ else if (/^<!-- \/?ailk:template\b/.test(line)) {
107
+ marked = true;
108
+ faults.push(`malformed marker line: ${line}`);
109
+ }
110
+ pos = next;
111
+ }
112
+ if (open)
113
+ faults.push(`unbalanced marker: section "${open.id}" has no end marker`);
114
+ if (marked && siteStart === undefined)
115
+ faults.push("no ailk:site marker");
116
+ return { regions, siteStart, faults, marked };
117
+ }
118
+ /**
119
+ * Render the committed template for a project (D2): substitute the scaffold
120
+ * markers, then stamp each begin marker with its body's hash. LF output.
121
+ *
122
+ * @throws Error when the template is malformed or a begin marker already
123
+ * carries a hash — a hash is never committed by hand.
124
+ */
125
+ export function renderClaudeMd(templateText, projectName) {
126
+ const text = substituteMarkers(templateText.replace(/\r\n/g, "\n"), buildScaffoldMarkers(projectName));
127
+ const parsed = parseClaudeMd(text);
128
+ if (parsed.faults.length > 0) {
129
+ throw new Error(`templates/CLAUDE.md is malformed: ${parsed.faults.join("; ")}`);
130
+ }
131
+ let out = text;
132
+ for (const region of [...parsed.regions].reverse()) {
133
+ if (region.sha256 !== undefined) {
134
+ throw new Error(`templates/CLAUDE.md: section "${region.id}" carries a sha256 — the render stamps it`);
135
+ }
136
+ const lineEnd = out.indexOf("\n", region.start) + 1;
137
+ const stamped = `<!-- ailk:template id="${region.id}" sha256="${hashBody(region.body)}" -->\n`;
138
+ out = out.slice(0, region.start) + stamped + out.slice(lineEnd);
139
+ }
140
+ return out;
141
+ }
142
+ /**
143
+ * The drift verdict for a rendered file (D6). `undefined` = no file. The
144
+ * reference implementation `tools/check-claude-md.ts` is held to.
145
+ */
146
+ export function findDrift(text) {
147
+ if (text === undefined)
148
+ return { state: "missing", edited: [], faults: [] };
149
+ const parsed = parseClaudeMd(text);
150
+ if (!parsed.marked)
151
+ return { state: "unmarked", edited: [], faults: [] };
152
+ const faults = [...parsed.faults];
153
+ const edited = [];
154
+ for (const region of parsed.regions) {
155
+ if (region.sha256 === undefined) {
156
+ faults.push(`begin marker of section "${region.id}" has no sha256`);
157
+ }
158
+ else if (SHA_RE.test(region.sha256) && region.sha256 !== hashBody(region.body)) {
159
+ // A malformed hash is already a parse fault, not an edit.
160
+ edited.push(region.id);
161
+ }
162
+ }
163
+ return { state: "marked", edited, faults };
164
+ }
165
+ /** The drift check's output lines; empty means the file passes. */
166
+ export function formatDrift(drift) {
167
+ if (drift.state === "missing")
168
+ return ["CLAUDE.md: missing"];
169
+ if (drift.state === "unmarked") {
170
+ return ["CLAUDE.md: no ailk:template markers — run pnpm create ailk@latest --sync-claude-md"];
171
+ }
172
+ return [
173
+ ...drift.faults.map((f) => `CLAUDE.md: ${f}`),
174
+ ...drift.edited.map((id) => `CLAUDE.md: template section "${id}" was edited by hand — move your change to Site notes, then run pnpm create ailk@latest --sync-claude-md`),
175
+ ];
176
+ }
177
+ /**
178
+ * Rewrite a marked file's template-owned regions to a rendered template (D3).
179
+ * Every character outside a region — the gaps and the whole site section —
180
+ * is copied through unchanged, except the one blank line a retired region
181
+ * takes with it. Regions and inserted separators are written in the file's
182
+ * own line ending, so a CRLF checkout stays CRLF and syncs idempotently.
183
+ *
184
+ * @throws Error naming the fault when the file is structurally malformed.
185
+ */
186
+ export function mergeClaudeMd(fileText, renderedText) {
187
+ const file = parseClaudeMd(fileText);
188
+ if (file.faults.length > 0 || file.siteStart === undefined) {
189
+ const faults = file.faults.length > 0 ? file.faults : ["no ailk:site marker"];
190
+ throw new Error(`CLAUDE.md: ${faults.join("; ")} — fix the markers by hand, then re-run --sync-claude-md`);
191
+ }
192
+ const first = file.regions[0];
193
+ const eol = first !== undefined && fileText[fileText.indexOf("\n", first.start) - 1] === "\r" ? "\r\n" : "\n";
194
+ const blank = (text) => text === "\n" || text === "\r\n";
195
+ const rendered = parseClaudeMd(renderedText);
196
+ const renderedById = new Map(rendered.regions.map((r) => [r.id, renderedText.slice(r.start, r.end).replace(/\n/g, eol)]));
197
+ const result = { text: "", updated: [], added: [], removed: [], overwritten: [] };
198
+ // The pre-site text as gap / region pieces, with each region already
199
+ // replaced (or dropped, when the template retired it).
200
+ const pieces = [];
201
+ let pos = 0;
202
+ let dropBlank = false;
203
+ for (const region of file.regions) {
204
+ const gap = fileText.slice(pos, region.start);
205
+ pieces.push({ text: dropBlank && blank(gap) ? "" : gap });
206
+ dropBlank = false;
207
+ pos = region.end;
208
+ const next = renderedById.get(region.id);
209
+ if (next === undefined) {
210
+ // A retired region takes the one blank line that separated it from its
211
+ // neighbour with it (the inverse of an insert), so the result matches
212
+ // a fresh render. Any other gap text is kept.
213
+ result.removed.push(region.id);
214
+ const kept = pieces.some((p) => p.region !== undefined);
215
+ const prev = pieces[pieces.length - 1];
216
+ if (kept && prev !== undefined && blank(prev.text))
217
+ prev.text = "";
218
+ else
219
+ dropBlank = !kept;
220
+ continue;
221
+ }
222
+ const current = fileText.slice(region.start, region.end);
223
+ if (region.sha256 !== hashBody(region.body))
224
+ result.overwritten.push(region.id);
225
+ if (next !== current)
226
+ result.updated.push(region.id);
227
+ pieces.push({ region: region.id, text: next });
228
+ }
229
+ const tail = fileText.slice(pos, file.siteStart);
230
+ pieces.push({ text: dropBlank && blank(tail) ? "" : tail });
231
+ // Insert each region the file lacks after its nearest preceding region
232
+ // in template order, one blank line apart (D3 step 5).
233
+ const order = rendered.regions.map((r) => r.id);
234
+ for (const [i, id] of order.entries()) {
235
+ if (pieces.some((p) => p.region === id))
236
+ continue;
237
+ const text = renderedById.get(id) ?? "";
238
+ const before = order
239
+ .slice(0, i)
240
+ .reverse()
241
+ .map((prev) => pieces.findIndex((p) => p.region === prev))
242
+ .find((idx) => idx !== -1);
243
+ if (before !== undefined) {
244
+ pieces.splice(before + 1, 0, { text: eol }, { region: id, text });
245
+ }
246
+ else {
247
+ const first = pieces.findIndex((p) => p.region !== undefined);
248
+ pieces.splice(first === -1 ? pieces.length : first, 0, { region: id, text }, { text: eol });
249
+ }
250
+ result.added.push(id);
251
+ }
252
+ result.text = pieces.map((p) => p.text).join("") + fileText.slice(file.siteStart);
253
+ return result;
254
+ }
255
+ /**
256
+ * True when an unmarked `CLAUDE.md` is a published template the owner never
257
+ * changed (D4): with some candidate project name un-substituted, it hashes to
258
+ * a `LEGACY_TEMPLATE_SHA256` entry. A miss is always safe — it refuses.
259
+ */
260
+ export function isUneditedLegacy(text, names) {
261
+ const folded = text.replace(/\r\n/g, "\n");
262
+ return names
263
+ .filter((name) => name.length > 0)
264
+ .some((name) => {
265
+ const hash = createHash("sha256")
266
+ .update(folded.split(name).join("{{PROJECT_NAME}}"), "utf8")
267
+ .digest("hex");
268
+ return LEGACY_TEMPLATE_SHA256.includes(hash);
269
+ });
270
+ }
271
+ /**
272
+ * `--sync-claude-md` (D3): the `syncCatalogSync` shape — validate the target
273
+ * is an existing scaffold, write at most its `CLAUDE.md`, spawn nothing. An
274
+ * unmarked file is replaced only when it is an unedited published template
275
+ * (D4); anything else unmarked, or a structural fault, is refused with
276
+ * nothing written.
277
+ */
278
+ export function syncClaudeMd(opts) {
279
+ const targetAbs = resolve(opts.targetDir);
280
+ if (!existsSync(targetAbs)) {
281
+ throw new Error(`sync target does not exist: ${targetAbs}. Point --sync-claude-md at an already-scaffolded repo.`);
282
+ }
283
+ if (!statSync(targetAbs).isDirectory()) {
284
+ throw new Error(`sync target is not a directory: ${targetAbs}`);
285
+ }
286
+ if (!existsSync(join(targetAbs, "architecture.yaml"))) {
287
+ throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
288
+ }
289
+ const pkg = readJsonObject(join(targetAbs, "package.json"));
290
+ const pkgName = typeof pkg?.["name"] === "string" ? pkg["name"] : "";
291
+ const scripts = pkg?.["scripts"];
292
+ const hasScript = typeof scripts === "object" && scripts !== null && "claude-md:check" in scripts;
293
+ const rendered = renderClaudeMd(opts.templateText, pkgName || basename(targetAbs));
294
+ const claudeMdPath = join(targetAbs, "CLAUDE.md");
295
+ const base = {
296
+ targetDir: targetAbs,
297
+ claudeMdPath,
298
+ updated: [],
299
+ added: [],
300
+ removed: [],
301
+ overwritten: [],
302
+ missingDriftCheck: !hasScript || !existsSync(join(targetAbs, CLAUDE_MD_CHECK_PATH)),
303
+ };
304
+ if (!existsSync(claudeMdPath)) {
305
+ writeFileSync(claudeMdPath, rendered);
306
+ return { ...base, action: "created" };
307
+ }
308
+ const current = readFileSync(claudeMdPath, "utf8");
309
+ if (!parseClaudeMd(current).marked) {
310
+ if (!isUneditedLegacy(current, [pkgName, basename(targetAbs)])) {
311
+ throw new Error(`${claudeMdPath} has no ailk:template markers and is not an unedited AI Launch Kit template, so --sync-claude-md will not overwrite it. ` +
312
+ "Copy anything you added out of CLAUDE.md, delete it, run --sync-claude-md again (it then writes a fresh file), and paste your notes under Site notes.");
313
+ }
314
+ writeFileSync(claudeMdPath, rendered);
315
+ return { ...base, action: "replaced-legacy" };
316
+ }
317
+ const { text, updated, added, removed, overwritten } = mergeClaudeMd(current, rendered);
318
+ const changed = text !== current;
319
+ if (changed)
320
+ writeFileSync(claudeMdPath, text);
321
+ return { ...base, action: changed ? "updated" : "unchanged", updated, added, removed, overwritten };
322
+ }
323
+ /** A JSON file's top-level object, or undefined when absent or unreadable. */
324
+ function readJsonObject(path) {
325
+ try {
326
+ const value = JSON.parse(readFileSync(path, "utf8"));
327
+ return typeof value === "object" && value !== null && !Array.isArray(value)
328
+ ? value
329
+ : undefined;
330
+ }
331
+ catch {
332
+ return undefined;
333
+ }
334
+ }
package/dist/cli.js CHANGED
@@ -5,10 +5,14 @@
5
5
  * Orchestration:
6
6
  * 0. on an `add-module` / `upgrade` subcommand: parse its own argv, run
7
7
  * addModule(), print the report, exit (2 on a refusal it names)
8
+ * on an `add-route` subcommand: add one route surface to an existing
9
+ * site, print the report, exit (2 on a refusal it names) (#6947)
8
10
  * on a `sync-routes` subcommand: parse its own argv, run syncRoutesSync(),
9
11
  * print the report, exit (1 iff routes are missing in check mode)
10
12
  * on a `license set` subcommand: write the key into the scaffold, push it
11
13
  * to a linked Vercel project and redeploy, exit (#6683 D9)
14
+ * on an `apply-design` subcommand: rewrite an existing site's look,
15
+ * print the changed-file report, exit (#6819)
12
16
  * 1. parse argv → CliOptions
13
17
  * 2. on --help: print helpText, exit 0
14
18
  * 3. on --version: print version, exit 0
@@ -31,14 +35,18 @@
31
35
  * synchronous as before.
32
36
  */
33
37
  import { spawnSync } from "node:child_process";
34
- import { readFileSync } from "node:fs";
35
- import { dirname, resolve } from "node:path";
38
+ import { existsSync, readFileSync } from "node:fs";
39
+ import { basename, dirname, resolve } from "node:path";
36
40
  import { fileURLToPath } from "node:url";
37
41
  import { ADD_MODULE_HELP, AddModuleRefusedError, formatAddModuleReport, parseAddModuleArgs, } from "./add-module.js";
42
+ import { ADD_ROUTE_HELP, AddRouteRefusedError, addRouteToScaffold, formatAddRouteReport, parseAddRouteArgs, } from "./add-route.js";
43
+ import { APPLY_DESIGN_HELP, applyDesign, formatApplyDesignReport, parseApplyDesignArgs, } from "./apply-design.js";
44
+ import { CLAUDE_MD_CHECK_PATH } from "./claude-md.js";
45
+ import { buildScaffoldMarkers } from "./copy.js";
38
46
  import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
39
47
  import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
40
48
  import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
41
- import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncRoutesSync, } from "./programmatic.js";
49
+ import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
42
50
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
43
51
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
44
52
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -47,6 +55,13 @@ function readPackageVersion() {
47
55
  const raw = readFileSync(PKG_JSON, "utf8");
48
56
  return JSON.parse(raw).version;
49
57
  }
58
+ /** The workspace version map `build-templates` wrote; empty before the first build. */
59
+ function readVersionMap() {
60
+ const path = resolve(PKG_ROOT, "versions.json");
61
+ if (!existsSync(path))
62
+ return {};
63
+ return JSON.parse(readFileSync(path, "utf8"));
64
+ }
50
65
  /** Raw value of a `--flag <value>` or `--flag=<value>` argv entry, if present. */
51
66
  function readRawFlagValue(argv, flag) {
52
67
  for (let i = 0; i < argv.length; i++) {
@@ -115,6 +130,36 @@ async function main() {
115
130
  }
116
131
  process.exit(0);
117
132
  }
133
+ // `add-route <route> [dir]` (#6947): add a route surface the site was
134
+ // scaffolded without — add-module's route, architecture and nav steps with
135
+ // no licence and no bundle. A refusal it names (an unknown route, one a
136
+ // module delivers, an unreadable architecture.yaml) exits 2.
137
+ if (argv[0] === "add-route") {
138
+ const args = parseAddRouteArgs(argv.slice(1));
139
+ if (args.help || args.route === undefined) {
140
+ process.stdout.write(ADD_ROUTE_HELP);
141
+ process.exit(0);
142
+ }
143
+ const targetDir = resolve(args.target ?? process.cwd());
144
+ try {
145
+ const result = addRouteToScaffold({
146
+ targetDir,
147
+ route: args.route,
148
+ versions: readVersionMap(),
149
+ markers: buildScaffoldMarkers(basename(targetDir)),
150
+ templatesDir: resolve(PKG_ROOT, "templates"),
151
+ siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
152
+ });
153
+ process.stdout.write(formatAddRouteReport(result));
154
+ }
155
+ catch (err) {
156
+ if (!(err instanceof AddRouteRefusedError))
157
+ throw err;
158
+ process.stderr.write(`[create-ailk] ${err.message}\n`);
159
+ process.exit(err.exitCode);
160
+ }
161
+ process.exit(0);
162
+ }
118
163
  // Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
119
164
  // argv shape (#4986). Check mode exits 1 iff a template route is missing;
120
165
  // locally modified routes are reported but do not fail; --apply exits 0.
@@ -141,6 +186,25 @@ async function main() {
141
186
  readStdin: () => readFileSync(0, "utf8"),
142
187
  }));
143
188
  }
189
+ // `apply-design <name|path> [--target <dir>]` (#6819): rewrite an existing
190
+ // site's look only. Every refusal — the licence gate included — reaches
191
+ // the top-level catch, exit 1, as a scaffold-time design refusal does.
192
+ if (argv[0] === "apply-design") {
193
+ const args = parseApplyDesignArgs(argv.slice(1));
194
+ if (args.help || args.design === undefined) {
195
+ process.stdout.write(APPLY_DESIGN_HELP);
196
+ process.exit(0);
197
+ }
198
+ const result = await applyDesign({
199
+ design: args.design,
200
+ targetDir: args.target ?? process.cwd(),
201
+ designsDir: resolve(PKG_ROOT, "site-templates", "designs"),
202
+ siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
203
+ catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
204
+ });
205
+ process.stdout.write(formatApplyDesignReport(result));
206
+ process.exit(0);
207
+ }
144
208
  // No silent default (#6754 D2/D3): a scaffold run naming neither --shape
145
209
  // nor --template asks for a shape on a terminal and is refused without one.
146
210
  const opts = await resolveCliShape(parseCliArgs(argv, { interactive: process.stdin.isTTY === true }), { input: process.stdin, output: process.stdout });
@@ -168,6 +232,10 @@ async function main() {
168
232
  "\n" +
169
233
  ADD_MODULE_HELP +
170
234
  "\n" +
235
+ ADD_ROUTE_HELP +
236
+ "\n" +
237
+ APPLY_DESIGN_HELP +
238
+ "\n" +
171
239
  LICENSE_SET_HELP);
172
240
  process.exit(0);
173
241
  }
@@ -186,6 +254,26 @@ async function main() {
186
254
  `[create-ailk] Search it by CAPABILITY before building a component — it indexes every @working-theory/ui section, block and chrome component by what it does.\n`);
187
255
  process.exit(0);
188
256
  }
257
+ // CLAUDE.md sync mode (#6904 D3): rewrite the template-owned sections of an
258
+ // already-scaffolded repo's CLAUDE.md, then exit. One file, nothing else.
259
+ if (opts.syncClaudeMd) {
260
+ const r = syncClaudeMdSync({ targetDir: opts.target ?? process.cwd() });
261
+ const lines = [`[create-ailk] ${r.claudeMdPath}: ${r.action}`];
262
+ if (r.updated.length > 0)
263
+ lines.push(`[create-ailk] Updated sections: ${r.updated.join(", ")}`);
264
+ if (r.added.length > 0)
265
+ lines.push(`[create-ailk] Added sections: ${r.added.join(", ")}`);
266
+ if (r.removed.length > 0)
267
+ lines.push(`[create-ailk] Removed sections: ${r.removed.join(", ")}`);
268
+ if (r.overwritten.length > 0) {
269
+ lines.push(`[create-ailk] Replaced hand-edits in: ${r.overwritten.join(", ")} — the previous text is in git; move anything you need into Site notes.`);
270
+ }
271
+ if (r.missingDriftCheck) {
272
+ lines.push(`[create-ailk] This site has no CLAUDE.md drift check yet — copy ${CLAUDE_MD_CHECK_PATH} from a fresh scaffold and add "claude-md:check": "tsx ${CLAUDE_MD_CHECK_PATH}" to package.json scripts and preflight.`);
273
+ }
274
+ process.stdout.write(lines.join("\n") + "\n");
275
+ process.exit(0);
276
+ }
189
277
  // Sync mode: regenerate architecture.yaml in an already-scaffolded repo from
190
278
  // the module set, then exit — bypassing the scaffold / --install / next-steps
191
279
  // path entirely (spec #3561 D2). Data-only and non-destructive: only
@@ -284,7 +372,7 @@ async function main() {
284
372
  lines.push("");
285
373
  lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
286
374
  (surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
287
- lines.push("Add another with the `add-page` skill, or re-run with --surfaces <id>.");
375
+ lines.push("Add another with `create-ailk add-route <id>`, or re-run with --surfaces <id>.");
288
376
  // Only when asked for. The default scaffold's output is unchanged.
289
377
  if (opts.withCatalog) {
290
378
  lines.push("Component catalog: COMPONENT-CATALOG.md — search it by capability before building a component.");
@@ -130,12 +130,6 @@ function appToYaml(app) {
130
130
  }
131
131
  return m;
132
132
  }
133
- function packageToYaml(pkg) {
134
- return {
135
- description: pkg.description,
136
- may_import_from: [...pkg.may_import_from].sort(),
137
- };
138
- }
139
133
  function namespacesToYaml(ns) {
140
134
  const members = {};
141
135
  const sortedMembers = Object.entries(ns.members).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
@@ -170,37 +164,50 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
170
164
  const enabledSurfaces = surfaces === undefined
171
165
  ? resolveSurfaces(selected)
172
166
  : [...new Set(surfaces)].sort();
173
- // apps/web (BASE) — may_import_from filled from the selected modules' webImports.
174
- const webImports = sortedUnion(...slices.map((s) => s.webImports));
175
- const apps = {
167
+ // The apps a scaffold ships on disk: web, api and mcp always (BASE), plus
168
+ // any a module brings (admin, with multi-tenant). A module's own `api`/`mcp`
169
+ // block replaces the base one. Canonical order web → api → mcp → admin.
170
+ // `admin` was `workspace` until #5637 renamed the owner surface; the slice's
171
+ // key was renamed there and this list was not, which dropped the Admin app
172
+ // from every derived architecture without failing anything but its own test.
173
+ const declared = {
176
174
  web: {
177
- path: ARCHITECTURE_BASE.web.path,
178
- description: ARCHITECTURE_BASE.web.description,
179
- may_import_from: webImports,
180
- may_not_import_from: [...ARCHITECTURE_BASE.web.may_not_import_from].sort(),
181
- forbidden_patterns: ARCHITECTURE_BASE.web.forbidden_patterns.map(forbiddenToYaml),
175
+ ...ARCHITECTURE_BASE.web,
176
+ may_not_import_from: [...ARCHITECTURE_BASE.web.may_not_import_from],
177
+ may_import_from: sortedUnion(...slices.map((s) => s.webImports)),
182
178
  },
179
+ ...ARCHITECTURE_BASE.apps,
183
180
  };
184
- // Additional apps (api, mcp, admin) in canonical order. `admin` was
185
- // `workspace` until #5637 renamed the owner surface; the slice's key was
186
- // renamed there and this list was not, which dropped the Admin app from
187
- // every derived architecture without failing anything but its own test.
188
- const extraApps = {};
189
181
  for (const s of slices)
190
- Object.assign(extraApps, s.apps ?? {});
191
- for (const key of ["api", "mcp", "admin"]) {
192
- const a = extraApps[key];
193
- if (a)
194
- apps[key] = appToYaml(a);
195
- }
196
- // packages — sorted union across the selected slices.
197
- const packagesRaw = {};
198
- for (const s of slices)
199
- Object.assign(packagesRaw, s.packages ?? {});
200
- const packages = {};
201
- const sortedPackages = Object.entries(packagesRaw).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
202
- for (const [key, pkg] of sortedPackages) {
203
- packages[key] = packageToYaml(pkg);
182
+ Object.assign(declared, s.apps ?? {});
183
+ // A scaffold carries no `packages/` tree — `@working-theory/*` install from
184
+ // npm — so an entry naming a monorepo folder is dropped rather than shipped
185
+ // as a rule the linter can never apply (#6940). What survives names a path
186
+ // the scaffold has: a declared app, or `database/`.
187
+ const roots = [
188
+ "database",
189
+ ...Object.values(declared).map((a) => a.path),
190
+ ];
191
+ const inScaffold = (p) => {
192
+ const path = p.endsWith("/**") ? p.slice(0, -3) : p;
193
+ return roots.some((r) => path === r || path.startsWith(`${r}/`));
194
+ };
195
+ const scoped = (p) => {
196
+ const { paths_excluded, ...rest } = p;
197
+ const kept = (paths_excluded ?? []).filter(inScaffold);
198
+ return kept.length > 0 ? { ...rest, paths_excluded: kept } : rest;
199
+ };
200
+ const apps = {};
201
+ for (const key of ["web", "api", "mcp", "admin"]) {
202
+ const a = declared[key];
203
+ if (!a)
204
+ continue;
205
+ apps[key] = appToYaml({
206
+ ...a,
207
+ may_import_from: a.may_import_from.filter(inScaffold),
208
+ may_not_import_from: a.may_not_import_from.filter(inScaffold),
209
+ forbidden_patterns: mergeForbidden([...ARCHITECTURE_BASE.appForbiddenPatterns], a.forbidden_patterns ?? []).map(scoped),
210
+ });
204
211
  }
205
212
  // namespaces — present only if a data-tier module contributes one.
206
213
  const nsSlice = slices.find((s) => s.namespaces)?.namespaces;
@@ -208,11 +215,10 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
208
215
  const extraForbidden = [];
209
216
  for (const s of slices)
210
217
  extraForbidden.push(...(s.forbiddenPatterns ?? []));
211
- const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden);
218
+ const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden).map(scoped);
212
219
  const doc = {
213
220
  version: ARCHITECTURE_BASE.version,
214
221
  composition_model: {
215
- reference: ARCHITECTURE_BASE.composition_model.reference,
216
222
  tiers: { ...ARCHITECTURE_BASE.composition_model.tiers },
217
223
  },
218
224
  surfaces: {
@@ -221,7 +227,6 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
221
227
  available: [...RECOGNIZED_SURFACES],
222
228
  },
223
229
  apps,
224
- packages,
225
230
  ...(nsSlice ? { namespaces: namespacesToYaml(nsSlice) } : {}),
226
231
  forbidden_patterns: forbidden.map(forbiddenToYaml),
227
232
  };