create-ailk 0.8.0 → 0.10.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 (238) hide show
  1. package/README.md +71 -3
  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 +104 -5
  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 +48 -32
  34. package/dist/lib/module-license-gate.js +117 -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 +67 -23
  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 +216 -19
  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 +16 -63
  52. package/dist/starter-grid.js +102 -84
  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 +53 -28
  57. package/package.json +6 -3
  58. package/release-scope.json +25 -0
  59. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  61. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  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/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +1 -1
  65. package/site-templates/designs/standard/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/DESIGN.md +98 -6
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  68. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  69. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  71. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  72. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  73. package/site-templates/saas-product/DESIGN.md +98 -6
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  75. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  76. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  78. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  79. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  82. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  83. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  84. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  85. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  86. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  87. package/templates/.claude/agents/web.md +10 -8
  88. package/templates/.claude/rules/architecture.md +17 -7
  89. package/templates/.claude/skills/README.md +3 -2
  90. package/templates/.claude/skills/add-auth/SKILL.md +142 -90
  91. package/templates/.claude/skills/add-auth/references/env-vars.md +32 -17
  92. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +17 -8
  93. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  94. package/templates/.claude/skills/add-page/references/page-types.md +94 -86
  95. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  96. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  97. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  98. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  99. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  100. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  101. package/templates/.claude/skills/check-site/SKILL.md +51 -18
  102. package/templates/.claude/skills/check-site/references/common-errors.md +12 -9
  103. package/templates/.claude/skills/check-site/references/findings-glossary.md +4 -4
  104. package/templates/.claude/skills/check-site/references/severity-levels.md +3 -3
  105. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  106. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  107. package/templates/.claude/skills/setup-deploy/SKILL.md +75 -38
  108. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  109. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  110. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  111. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  112. package/templates/.env.example +23 -2
  113. package/templates/.github/workflows/ci.yml +4 -0
  114. package/templates/.prettierignore +1 -0
  115. package/templates/CLAUDE.md +146 -103
  116. package/templates/DESIGN.md +98 -6
  117. package/templates/README.md +50 -38
  118. package/templates/apps/api/.env.example +2 -1
  119. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  120. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  121. package/templates/apps/api/src/lib/stripe.ts +9 -0
  122. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  123. package/templates/apps/api/src/middleware/auth.ts +54 -0
  124. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  125. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  126. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  127. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  128. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  129. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  130. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  131. package/templates/apps/api/src/server.ts +28 -0
  132. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  133. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  134. package/templates/apps/api/src/services/revalidation.ts +60 -15
  135. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  136. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  137. package/templates/apps/web/.env.example +13 -0
  138. package/templates/apps/web/CLAUDE.md +22 -28
  139. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  140. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  141. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  142. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  143. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  144. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  145. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  146. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  147. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  148. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  149. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  150. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  151. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  152. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  153. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  154. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  155. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  156. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  157. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  158. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  159. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  160. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  161. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  162. package/templates/apps/web/lib/locale-guard.ts +18 -0
  163. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  164. package/templates/apps/web/lib/nav-config.ts +14 -13
  165. package/templates/apps/web/lib/site-brand.tsx +13 -9
  166. package/templates/apps/web/lib/site-logo.tsx +36 -0
  167. package/templates/apps/web/lib/site-theme.ts +46 -26
  168. package/templates/apps/web/messages/en.json +23 -9
  169. package/templates/apps/web/middleware.ts +28 -4
  170. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  171. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  172. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  173. package/templates/content/en/BookCall/start-project.mdx +56 -0
  174. package/templates/content/en/ContactPage/contact.mdx +40 -1
  175. package/templates/content/en/HomePage/home.mdx +2 -6
  176. package/templates/content/en/LandingPage/meet.mdx +59 -0
  177. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  178. package/templates/database/CHANGELOG.md +7 -0
  179. package/templates/database/package.json +1 -1
  180. package/templates/package.json +4 -1
  181. package/templates/tools/check-claude-md.ts +128 -0
  182. package/versions.json +33 -33
  183. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  184. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  185. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  186. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  187. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  188. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  189. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  190. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  191. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  192. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  193. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  194. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  195. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  196. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  197. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  198. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  199. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  200. package/site-templates/minimalism-site/DESIGN.md +0 -162
  201. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +0 -506
  202. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  203. package/site-templates/minimalism-site/ailk-template.json +0 -23
  204. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  205. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  206. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  207. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  208. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  209. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  210. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  211. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  212. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  213. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  214. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  215. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  216. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  217. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  218. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  219. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  220. package/site-templates/minimalism-site/provenance.json +0 -4
  221. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  222. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -175
  223. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
  224. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
  225. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
  226. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  227. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  228. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  229. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  230. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  231. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  232. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  233. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  234. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  235. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  236. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  237. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  238. /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,19 @@
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";
46
+ import { assertReleased } from "./lib/module-license-gate.js";
38
47
  import { FREE_PREREQUISITES_NOTE, LICENSED_PREREQUISITES_NOTE, isLicensedInstall, proCallout, } from "./lib/pro-callout.js";
39
48
  import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
40
49
  import { helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
41
- import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncRoutesSync, } from "./programmatic.js";
50
+ import { addModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
42
51
  import { CORE_SURFACE_PATHS, SURFACE_ALIASES } from "./surfaces.js";
43
52
  import { SYNC_ROUTES_HELP, formatSyncRoutesReport, parseSyncRoutesArgs, } from "./sync-routes.js";
44
53
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -47,6 +56,13 @@ function readPackageVersion() {
47
56
  const raw = readFileSync(PKG_JSON, "utf8");
48
57
  return JSON.parse(raw).version;
49
58
  }
59
+ /** The workspace version map `build-templates` wrote; empty before the first build. */
60
+ function readVersionMap() {
61
+ const path = resolve(PKG_ROOT, "versions.json");
62
+ if (!existsSync(path))
63
+ return {};
64
+ return JSON.parse(readFileSync(path, "utf8"));
65
+ }
50
66
  /** Raw value of a `--flag <value>` or `--flag=<value>` argv entry, if present. */
51
67
  function readRawFlagValue(argv, flag) {
52
68
  for (let i = 0; i < argv.length; i++) {
@@ -115,6 +131,36 @@ async function main() {
115
131
  }
116
132
  process.exit(0);
117
133
  }
134
+ // `add-route <route> [dir]` (#6947): add a route surface the site was
135
+ // scaffolded without — add-module's route, architecture and nav steps with
136
+ // no licence and no bundle. A refusal it names (an unknown route, one a
137
+ // module delivers, an unreadable architecture.yaml) exits 2.
138
+ if (argv[0] === "add-route") {
139
+ const args = parseAddRouteArgs(argv.slice(1));
140
+ if (args.help || args.route === undefined) {
141
+ process.stdout.write(ADD_ROUTE_HELP);
142
+ process.exit(0);
143
+ }
144
+ const targetDir = resolve(args.target ?? process.cwd());
145
+ try {
146
+ const result = addRouteToScaffold({
147
+ targetDir,
148
+ route: args.route,
149
+ versions: readVersionMap(),
150
+ markers: buildScaffoldMarkers(basename(targetDir)),
151
+ templatesDir: resolve(PKG_ROOT, "templates"),
152
+ siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
153
+ });
154
+ process.stdout.write(formatAddRouteReport(result));
155
+ }
156
+ catch (err) {
157
+ if (!(err instanceof AddRouteRefusedError))
158
+ throw err;
159
+ process.stderr.write(`[create-ailk] ${err.message}\n`);
160
+ process.exit(err.exitCode);
161
+ }
162
+ process.exit(0);
163
+ }
118
164
  // Subcommand dispatch, ahead of the flag parser: `sync-routes` has its own
119
165
  // argv shape (#4986). Check mode exits 1 iff a template route is missing;
120
166
  // locally modified routes are reported but do not fail; --apply exits 0.
@@ -141,9 +187,38 @@ async function main() {
141
187
  readStdin: () => readFileSync(0, "utf8"),
142
188
  }));
143
189
  }
190
+ // `apply-design <name|path> [--target <dir>]` (#6819): rewrite an existing
191
+ // site's look only. Every refusal — the licence gate included — reaches
192
+ // the top-level catch, exit 1, as a scaffold-time design refusal does.
193
+ if (argv[0] === "apply-design") {
194
+ const args = parseApplyDesignArgs(argv.slice(1));
195
+ if (args.help || args.design === undefined) {
196
+ process.stdout.write(APPLY_DESIGN_HELP);
197
+ process.exit(0);
198
+ }
199
+ const result = await applyDesign({
200
+ design: args.design,
201
+ targetDir: args.target ?? process.cwd(),
202
+ designsDir: resolve(PKG_ROOT, "site-templates", "designs"),
203
+ siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
204
+ catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
205
+ });
206
+ process.stdout.write(formatApplyDesignReport(result));
207
+ process.exit(0);
208
+ }
144
209
  // No silent default (#6754 D2/D3): a scaffold run naming neither --shape
145
210
  // nor --template asks for a shape on a terminal and is refused without one.
146
- const opts = await resolveCliShape(parseCliArgs(argv, { interactive: process.stdin.isTTY === true }), { input: process.stdin, output: process.stdout });
211
+ // A --design or --modules outside the release scope is refused before that
212
+ // prompt asks anything (#6984); scaffold() checks the full run again.
213
+ const parsed = parseCliArgs(argv, { interactive: process.stdin.isTTY === true });
214
+ if (parsed.needsShapePrompt && !parsed.help && !parsed.version) {
215
+ assertReleased("designs", [parsed.design]);
216
+ assertReleased("modules", parsed.modules ?? []);
217
+ }
218
+ const opts = await resolveCliShape(parsed, {
219
+ input: process.stdin,
220
+ output: process.stdout,
221
+ });
147
222
  // Deprecation notice for a retired `--surfaces` id (#5563 D2). `surfaces.ts`
148
223
  // stays pure — `canonicalizeSurface` only resolves the alias — so this is
149
224
  // the one place that prints: compare the raw `--surfaces` argv token
@@ -168,6 +243,10 @@ async function main() {
168
243
  "\n" +
169
244
  ADD_MODULE_HELP +
170
245
  "\n" +
246
+ ADD_ROUTE_HELP +
247
+ "\n" +
248
+ APPLY_DESIGN_HELP +
249
+ "\n" +
171
250
  LICENSE_SET_HELP);
172
251
  process.exit(0);
173
252
  }
@@ -186,6 +265,26 @@ async function main() {
186
265
  `[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
266
  process.exit(0);
188
267
  }
268
+ // CLAUDE.md sync mode (#6904 D3): rewrite the template-owned sections of an
269
+ // already-scaffolded repo's CLAUDE.md, then exit. One file, nothing else.
270
+ if (opts.syncClaudeMd) {
271
+ const r = syncClaudeMdSync({ targetDir: opts.target ?? process.cwd() });
272
+ const lines = [`[create-ailk] ${r.claudeMdPath}: ${r.action}`];
273
+ if (r.updated.length > 0)
274
+ lines.push(`[create-ailk] Updated sections: ${r.updated.join(", ")}`);
275
+ if (r.added.length > 0)
276
+ lines.push(`[create-ailk] Added sections: ${r.added.join(", ")}`);
277
+ if (r.removed.length > 0)
278
+ lines.push(`[create-ailk] Removed sections: ${r.removed.join(", ")}`);
279
+ if (r.overwritten.length > 0) {
280
+ lines.push(`[create-ailk] Replaced hand-edits in: ${r.overwritten.join(", ")} — the previous text is in git; move anything you need into Site notes.`);
281
+ }
282
+ if (r.missingDriftCheck) {
283
+ 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.`);
284
+ }
285
+ process.stdout.write(lines.join("\n") + "\n");
286
+ process.exit(0);
287
+ }
189
288
  // Sync mode: regenerate architecture.yaml in an already-scaffolded repo from
190
289
  // the module set, then exit — bypassing the scaffold / --install / next-steps
191
290
  // path entirely (spec #3561 D2). Data-only and non-destructive: only
@@ -284,7 +383,7 @@ async function main() {
284
383
  lines.push("");
285
384
  lines.push(`Route surfaces: ${CORE_SURFACE_PATHS.join(", ")}` +
286
385
  (surfaces.length > 0 ? ` + ${surfaces.join(", ")}` : ""));
287
- lines.push("Add another with the `add-page` skill, or re-run with --surfaces <id>.");
386
+ lines.push("Add another with `create-ailk add-route <id>`, or re-run with --surfaces <id>.");
288
387
  // Only when asked for. The default scaffold's output is unchanged.
289
388
  if (opts.withCatalog) {
290
389
  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
  };