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,78 @@
1
+ /** One row of theme's contrast audit, as the audit script prints it. */
2
+ export interface ContrastRow {
3
+ name: string;
4
+ bg: string;
5
+ fg: string;
6
+ ratio: number | null;
7
+ threshold: number;
8
+ status: "pass" | "fail" | "unmeasurable";
9
+ }
10
+ export type DesignContrastVerdict = {
11
+ ok: true;
12
+ pairs: number;
13
+ } | {
14
+ ok: false;
15
+ pairs: number;
16
+ failing: ContrastRow[];
17
+ reason: string;
18
+ };
19
+ /**
20
+ * Decide whether `design`'s derived token set passes WCAG AA. Every row whose
21
+ * status is not `pass` fails it; an audit of no pair never passes.
22
+ */
23
+ export declare function assessDesignContrast(design: string, results: ContrastRow[]): DesignContrastVerdict;
24
+ /** `__tests__/fixtures/page-jsonld.ts`'s stdout: page path → its composed JSON-LD. */
25
+ export type ComposedPages = Record<string, {
26
+ jsonld: string;
27
+ sectionsValid: boolean;
28
+ }>;
29
+ /** One changed node: the page and its JSON-pointer path (or a `(page …)` pseudo-path). */
30
+ export interface DriftNode {
31
+ page: string;
32
+ path: string;
33
+ }
34
+ /**
35
+ * Every node that differs between two compositions, sorted by page then path.
36
+ * A removed or added page, and a page whose sections no longer validate after,
37
+ * are one pseudo-path node each; a `logo` subtree is never compared.
38
+ */
39
+ export declare function jsonLdDrift(before: ComposedPages, after: ComposedPages): DriftNode[];
40
+ export type JsonLdDriftVerdict = {
41
+ ok: true;
42
+ pages: number;
43
+ } | {
44
+ ok: false;
45
+ pages: number;
46
+ changed: DriftNode[];
47
+ reason: string;
48
+ };
49
+ /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
50
+ export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
51
+ export interface MeasureApplyDesignDriftOptions {
52
+ /** create-ailk's built `dist/cli.js`. */
53
+ cliPath: string;
54
+ /** The monorepo's `tsx` binary. */
55
+ tsxPath: string;
56
+ /** `__tests__/fixtures/page-jsonld.ts` — the one production composer, run from source. */
57
+ composerPath: string;
58
+ /** Its tsconfig, passed as `TSX_TSCONFIG_PATH`. */
59
+ composerTsconfig: string;
60
+ /** The design `apply-design` applies (a name or a package path). */
61
+ design: string;
62
+ /** An existing empty directory; the drift site is scaffolded at `<workDir>/site`. */
63
+ workDir: string;
64
+ /** The loopback registry the CLI resolves modules and templates from. */
65
+ registryUrl?: string;
66
+ /** Written to the site's own `ailk.key`, which `apply-design` gates a non-Free design on. */
67
+ licenseKey?: string;
68
+ }
69
+ /**
70
+ * Scaffold a keyless `organization-product` site on `standard`, compose its
71
+ * JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
72
+ * exit at any step throws naming the step and the child's stderr.
73
+ */
74
+ export declare function measureApplyDesignDrift(opts: MeasureApplyDesignDriftOptions): {
75
+ before: ComposedPages;
76
+ after: ComposedPages;
77
+ applyStdout: string;
78
+ };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The two design checks a design-wearing run of `scripts/verify-scaffold.mjs`
3
+ * runs before install (#6823, spec
4
+ * docs/specs/6823-the-matrix-runs-the-derived-spec.md D3).
5
+ *
6
+ * CONTRAST — `assessDesignContrast` turns the rows
7
+ * `packages/theme/scripts/audit-site-theme-contrast.ts` prints (theme's
8
+ * `auditTokenSetContrast` over the design's derived token set) into a verdict
9
+ * naming every non-passing `{bg, fg}` pair and the design. The WCAG math and
10
+ * the pair list stay theme's; this file only decides pass/fail.
11
+ *
12
+ * DRIFT — `jsonLdDrift` / `assessJsonLdDrift` compare every page's composed
13
+ * JSON-LD before and after `apply-design`, excluding the logo image (the PRD
14
+ * metric "structural drift after apply"), and `measureApplyDesignDrift`
15
+ * gathers the two snapshots (the `renderRoute` precedent, #6821 D4): it
16
+ * touches the filesystem and processes only through the paths it is handed,
17
+ * so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
18
+ * procedure. Nothing here imports `@working-theory/theme`.
19
+ */
20
+ import { execFileSync } from "node:child_process";
21
+ import { writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ function describePair(row) {
24
+ const measured = row.status === "unmeasurable" || row.ratio === null
25
+ ? "unmeasurable"
26
+ : `${row.ratio.toFixed(2)}:1 < ${row.threshold}:1`;
27
+ return `${row.name} {bg ${row.bg}, fg ${row.fg}} ${measured}`;
28
+ }
29
+ /**
30
+ * Decide whether `design`'s derived token set passes WCAG AA. Every row whose
31
+ * status is not `pass` fails it; an audit of no pair never passes.
32
+ */
33
+ export function assessDesignContrast(design, results) {
34
+ const pairs = results.length;
35
+ if (pairs === 0) {
36
+ return { ok: false, pairs, failing: [], reason: `design ${design}: no contrast pair was audited` };
37
+ }
38
+ const failing = results.filter((row) => row.status !== "pass");
39
+ if (failing.length === 0)
40
+ return { ok: true, pairs };
41
+ return {
42
+ ok: false,
43
+ pairs,
44
+ failing,
45
+ reason: `design ${design}: ${failing.length} of ${pairs} contrast pair(s) below WCAG AA — ${failing
46
+ .map(describePair)
47
+ .join("; ")}`,
48
+ };
49
+ }
50
+ /** schema.org's logo property — the one subtree the PRD's "excluding the logo image" skips. */
51
+ const LOGO_KEY = "logo";
52
+ const kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
53
+ function walk(before, after, path, out) {
54
+ const kind = kindOf(before);
55
+ if (kind !== kindOf(after)) {
56
+ out.push(path);
57
+ return;
58
+ }
59
+ if (kind === "array") {
60
+ const a = before;
61
+ const b = after;
62
+ if (a.length !== b.length)
63
+ out.push(path);
64
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
65
+ walk(a[i], b[i], `${path}/${i}`, out);
66
+ return;
67
+ }
68
+ if (kind === "object") {
69
+ const a = before;
70
+ const b = after;
71
+ for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
72
+ if (key === LOGO_KEY)
73
+ continue;
74
+ walk(a[key], b[key], `${path}/${key}`, out);
75
+ }
76
+ return;
77
+ }
78
+ if (before !== after)
79
+ out.push(path);
80
+ }
81
+ /**
82
+ * Every node that differs between two compositions, sorted by page then path.
83
+ * A removed or added page, and a page whose sections no longer validate after,
84
+ * are one pseudo-path node each; a `logo` subtree is never compared.
85
+ */
86
+ export function jsonLdDrift(before, after) {
87
+ const nodes = [];
88
+ for (const page of [...new Set([...Object.keys(before), ...Object.keys(after)])]) {
89
+ const b = before[page];
90
+ const a = after[page];
91
+ if (a === undefined) {
92
+ nodes.push({ page, path: "(page removed)" });
93
+ }
94
+ else if (b === undefined) {
95
+ nodes.push({ page, path: "(page added)" });
96
+ }
97
+ else if (!a.sectionsValid) {
98
+ nodes.push({ page, path: "(sections invalid)" });
99
+ }
100
+ else {
101
+ const paths = [];
102
+ walk(JSON.parse(b.jsonld), JSON.parse(a.jsonld), "", paths);
103
+ for (const path of paths)
104
+ nodes.push({ page, path: path === "" ? "/" : path });
105
+ }
106
+ }
107
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
108
+ return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
109
+ }
110
+ /** How many changed nodes a drift reason lists before it stops. */
111
+ const REASON_NODE_LIMIT = 10;
112
+ /** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
113
+ export function assessJsonLdDrift(design, before, after) {
114
+ const pages = Object.keys(before).length;
115
+ if (pages === 0) {
116
+ return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
117
+ }
118
+ const changed = jsonLdDrift(before, after);
119
+ if (changed.length === 0)
120
+ return { ok: true, pages };
121
+ const listed = changed
122
+ .slice(0, REASON_NODE_LIMIT)
123
+ .map((n) => `${n.page} ${n.path}`)
124
+ .join("; ");
125
+ return {
126
+ ok: false,
127
+ pages,
128
+ changed,
129
+ reason: `design ${design}: ${changed.length} JSON-LD node(s) changed by apply-design — ${listed}${changed.length > REASON_NODE_LIMIT ? "; …" : ""}`,
130
+ };
131
+ }
132
+ /**
133
+ * Scaffold a keyless `organization-product` site on `standard`, compose its
134
+ * JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
135
+ * exit at any step throws naming the step and the child's stderr.
136
+ */
137
+ export function measureApplyDesignDrift(opts) {
138
+ const baseEnv = { ...process.env };
139
+ delete baseEnv["AILK_LICENSE_KEY"];
140
+ const registryEnv = opts.registryUrl === undefined
141
+ ? {}
142
+ : { AILK_MODULE_REGISTRY_URL: opts.registryUrl, AILK_TEMPLATE_REGISTRY_URL: opts.registryUrl };
143
+ const site = join(opts.workDir, "site");
144
+ const exec = (label, cmd, args, env) => {
145
+ try {
146
+ return execFileSync(cmd, args, {
147
+ encoding: "utf8",
148
+ env: { ...baseEnv, ...env },
149
+ stdio: ["ignore", "pipe", "pipe"],
150
+ maxBuffer: 64 * 1024 * 1024,
151
+ });
152
+ }
153
+ catch (err) {
154
+ const { stderr, message } = err;
155
+ throw new Error(`drift: ${label} failed: ${(stderr || message || "").trim()}`, { cause: err });
156
+ }
157
+ };
158
+ const compose = (label) => {
159
+ const stdout = exec(label, opts.tsxPath, [opts.composerPath, site], { TSX_TSCONFIG_PATH: opts.composerTsconfig });
160
+ try {
161
+ return JSON.parse(stdout);
162
+ }
163
+ catch (err) {
164
+ throw new Error(`drift: ${label} printed no JSON: ${err.message}`, { cause: err });
165
+ }
166
+ };
167
+ exec("scaffold organization-product on standard", process.execPath, [opts.cliPath, site, "--shape", "organization-product", "--design", "standard"], registryEnv);
168
+ if (opts.licenseKey !== undefined)
169
+ writeFileSync(join(site, "ailk.key"), `${opts.licenseKey}\n`);
170
+ const before = compose("compose JSON-LD before apply-design");
171
+ const applyStdout = exec(`apply-design ${opts.design}`, process.execPath, [opts.cliPath, "apply-design", opts.design, "--target", site], opts.registryUrl === undefined ? {} : { AILK_MODULE_REGISTRY_URL: opts.registryUrl });
172
+ const after = compose("compose JSON-LD after apply-design");
173
+ return { before, after, applyStdout };
174
+ }
@@ -6,6 +6,15 @@ export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
6
6
  * describes the rule, never the value.
7
7
  */
8
8
  export declare function assertLicenseKeyShape(key: string): void;
9
+ /**
10
+ * Write `content` only when it differs, with mode 0600 either way. An existing
11
+ * file is tightened BEFORE the write — `mode` applies only on creation, so a
12
+ * looser pre-existing `.env` would otherwise hold the key for a moment at its
13
+ * old permissions.
14
+ */
15
+ export declare function writeOwnerOnly(path: string, content: string): void;
16
+ /** `text` plus `line`, starting on a fresh line. */
17
+ export declare function appendLine(text: string, line: string): string;
9
18
  /**
10
19
  * Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
11
20
  * `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
@@ -48,7 +48,7 @@ export function assertLicenseKeyShape(key) {
48
48
  * looser pre-existing `.env` would otherwise hold the key for a moment at its
49
49
  * old permissions.
50
50
  */
51
- function writeOwnerOnly(path, content) {
51
+ export function writeOwnerOnly(path, content) {
52
52
  mkdirSync(dirname(path), { recursive: true });
53
53
  if (existsSync(path))
54
54
  chmodSync(path, 0o600);
@@ -58,7 +58,7 @@ function writeOwnerOnly(path, content) {
58
58
  chmodSync(path, 0o600);
59
59
  }
60
60
  /** `text` plus `line`, starting on a fresh line. */
61
- function appendLine(text, line) {
61
+ export function appendLine(text, line) {
62
62
  const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
63
63
  return `${text}${sep}${line}\n`;
64
64
  }
@@ -22,7 +22,6 @@
22
22
  * run's level, before `scaffold()`'s first disk write.
23
23
  */
24
24
  import type { ModuleName } from "../parse-args.js";
25
- import type { StarterDesign } from "../template-manifest.js";
26
25
  import { type Level } from "./module-tier.js";
27
26
  /**
28
27
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
@@ -39,18 +38,29 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
39
38
  export declare class LicenseRequiredError extends Error {
40
39
  constructor(message: string);
41
40
  }
42
- /** Designs every level renders (#6760 D1). Anything else in `STARTER_DESIGNS` is Pro and up. */
43
- export declare const FREE_DESIGNS: readonly StarterDesign[];
41
+ /**
42
+ * Design names every level renders (#6760 D1). Anything else in
43
+ * `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
44
+ * `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
45
+ * `assertDesignAllowed`'s `free` parameter carries that verdict in.
46
+ */
47
+ export declare const FREE_DESIGNS: readonly string[];
44
48
  /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
45
49
  export declare const DESIGN_LEVELS: readonly Level[];
46
50
  /**
47
- * Refuse a non-`standard` design at a level below Pro (#6760 D1, D6). Pure
48
- * and synchronous — takes the level `deriveLevel`/`resolveModuleSet` already
49
- * resolved; never reads a key or derives a level itself (D2, fail-closed:
50
- * both checks below are allow-lists, so an unrecognized `level` refuses
51
- * rather than granting it).
51
+ * Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
52
+ * path design #6813 D7). Pure and synchronous — takes the level
53
+ * `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
54
+ * derives a level itself (D2, fail-closed: both checks below are allow-lists,
55
+ * so an unrecognized `level` refuses rather than granting it).
56
+ *
57
+ * @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
58
+ * (a design NAME's own answer), so a caller passing a name never changes
59
+ * behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
60
+ * identity rule) passes it explicitly: a byte-identical copy of a shipped
61
+ * Free design is Free; any other path is not, whatever it is named.
52
62
  */
53
- export declare function assertDesignAllowed(design: StarterDesign | undefined, level: Level): void;
63
+ export declare function assertDesignAllowed(design: string | undefined, level: Level, free?: boolean): void;
54
64
  export interface ResolvePaidModulesOptions {
55
65
  /** The requested module set's paid subset (see `module-tier.ts`). */
56
66
  paidModules: ModuleName[];
@@ -19,19 +19,30 @@ export class LicenseRequiredError extends Error {
19
19
  this.name = "LicenseRequiredError";
20
20
  }
21
21
  }
22
- /** Designs every level renders (#6760 D1). Anything else in `STARTER_DESIGNS` is Pro and up. */
22
+ /**
23
+ * Design names every level renders (#6760 D1). Anything else in
24
+ * `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
25
+ * `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
26
+ * `assertDesignAllowed`'s `free` parameter carries that verdict in.
27
+ */
23
28
  export const FREE_DESIGNS = ["standard"];
24
29
  /** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
25
30
  export const DESIGN_LEVELS = ["pro", "pro-plus"];
26
31
  /**
27
- * Refuse a non-`standard` design at a level below Pro (#6760 D1, D6). Pure
28
- * and synchronous — takes the level `deriveLevel`/`resolveModuleSet` already
29
- * resolved; never reads a key or derives a level itself (D2, fail-closed:
30
- * both checks below are allow-lists, so an unrecognized `level` refuses
31
- * rather than granting it).
32
+ * Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
33
+ * path design #6813 D7). Pure and synchronous — takes the level
34
+ * `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
35
+ * derives a level itself (D2, fail-closed: both checks below are allow-lists,
36
+ * so an unrecognized `level` refuses rather than granting it).
37
+ *
38
+ * @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
39
+ * (a design NAME's own answer), so a caller passing a name never changes
40
+ * behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
41
+ * identity rule) passes it explicitly: a byte-identical copy of a shipped
42
+ * Free design is Free; any other path is not, whatever it is named.
32
43
  */
33
- export function assertDesignAllowed(design, level) {
34
- if (design === undefined || FREE_DESIGNS.includes(design))
44
+ export function assertDesignAllowed(design, level, free = design !== undefined && FREE_DESIGNS.includes(design)) {
45
+ if (design === undefined || free)
35
46
  return;
36
47
  if (DESIGN_LEVELS.includes(level))
37
48
  return;
@@ -0,0 +1,52 @@
1
+ /** What the driver observed of one route's render. */
2
+ export interface RenderObservation {
3
+ /** The requested route, e.g. `/` or `/about`. */
4
+ route: string;
5
+ /** The message `page.goto` threw with, when it threw. */
6
+ gotoError?: string;
7
+ /** The status of the response `page.goto` resolved; undefined when it resolved none. */
8
+ status: number | undefined;
9
+ /** `page.url()` after navigation settled. */
10
+ finalUrl: string;
11
+ /** The screenshot's size on disk; undefined when none was written. */
12
+ pngBytes: number | undefined;
13
+ }
14
+ export type RenderVerdict = {
15
+ ok: true;
16
+ } | {
17
+ ok: false;
18
+ reason: string;
19
+ };
20
+ /**
21
+ * Decide whether `route` rendered. Checked in order: a navigation error (a
22
+ * net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
23
+ * status other than 200, a final pathname other than the requested route (a
24
+ * redirect elsewhere is not a render of this route), and a missing or empty
25
+ * PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
26
+ */
27
+ export declare function assessRender(observation: RenderObservation): RenderVerdict;
28
+ /** The slice of a Playwright `Page` that `renderRoute` uses. */
29
+ export interface RenderPage {
30
+ goto(url: string, options: {
31
+ waitUntil: "load";
32
+ timeout: number;
33
+ }): Promise<{
34
+ status(): number;
35
+ } | null>;
36
+ url(): string;
37
+ screenshot(options: {
38
+ path: string;
39
+ fullPage: boolean;
40
+ }): Promise<unknown>;
41
+ }
42
+ /** How long one navigation may take before it counts as a failed render. */
43
+ export declare const RENDER_TIMEOUT_MS = 60000;
44
+ /**
45
+ * Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
46
+ * `assessRender`'s verdict on what happened. `status` comes from the response
47
+ * `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
48
+ * requested URL, never a side `fetch` — so a redirect cannot pass as a render.
49
+ * A failed navigation takes no screenshot; a screenshot that throws counts
50
+ * as no PNG written — never a stale one left at `pngPath` by an earlier run.
51
+ */
52
+ export declare function renderRoute(page: RenderPage, base: string, route: string, pngPath: string): Promise<RenderVerdict>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The verdict on one browser render of a scaffold route (#6821, spec
3
+ * docs/specs/6821-the-shape-design-render-matrix-spec.md D4).
4
+ *
5
+ * A design leg of `scripts/verify-scaffold.mjs` renders its scaffold's home
6
+ * route and one inner route in Chromium and stores a screenshot of each — the
7
+ * shape × design render matrix. A render that did not happen must fail the
8
+ * leg, and proving that rule must not cost a scaffold build, so it lives here
9
+ * as a pure function (`assessRender`, the `api-boot-assertion.ts` precedent)
10
+ * plus the one function that gathers its facts (`renderRoute`), which touches
11
+ * Playwright only through the `page` it is handed. The driver loads both from
12
+ * `dist/`; `__tests__/verify-scaffold-render.test.ts` drives them with a fake
13
+ * page. Nothing here imports `@playwright/test`.
14
+ */
15
+ import { existsSync, rmSync, statSync } from "node:fs";
16
+ /**
17
+ * Decide whether `route` rendered. Checked in order: a navigation error (a
18
+ * net error — `ERR_TOO_MANY_REDIRECTS` included — or a timeout), a final
19
+ * status other than 200, a final pathname other than the requested route (a
20
+ * redirect elsewhere is not a render of this route), and a missing or empty
21
+ * PNG. Pathnames are compared, never whole URLs, so a query or hash is fine.
22
+ */
23
+ export function assessRender(observation) {
24
+ const { route, gotoError, status, finalUrl, pngBytes } = observation;
25
+ if (gotoError !== undefined) {
26
+ return { ok: false, reason: `navigation failed: ${gotoError}` };
27
+ }
28
+ if (status !== 200) {
29
+ return {
30
+ ok: false,
31
+ reason: status === undefined ? "navigation resolved no response" : `final status ${status}, expected 200`,
32
+ };
33
+ }
34
+ const finalPath = new URL(finalUrl).pathname;
35
+ if (finalPath !== route) {
36
+ return { ok: false, reason: `requested "${route}" but the page settled on "${finalPath}"` };
37
+ }
38
+ if (typeof pngBytes !== "number" || pngBytes <= 0) {
39
+ return { ok: false, reason: pngBytes === 0 ? "the screenshot PNG is empty" : "no screenshot PNG was written" };
40
+ }
41
+ return { ok: true };
42
+ }
43
+ /** How long one navigation may take before it counts as a failed render. */
44
+ export const RENDER_TIMEOUT_MS = 60_000;
45
+ /**
46
+ * Navigate `page` to `base + route`, screenshot it to `pngPath`, and return
47
+ * `assessRender`'s verdict on what happened. `status` comes from the response
48
+ * `page.goto` itself resolved and `finalUrl` from `page.url()` — never the
49
+ * requested URL, never a side `fetch` — so a redirect cannot pass as a render.
50
+ * A failed navigation takes no screenshot; a screenshot that throws counts
51
+ * as no PNG written — never a stale one left at `pngPath` by an earlier run.
52
+ */
53
+ export async function renderRoute(page, base, route, pngPath) {
54
+ let status;
55
+ try {
56
+ const response = await page.goto(base + route, { waitUntil: "load", timeout: RENDER_TIMEOUT_MS });
57
+ status = response === null ? undefined : response.status();
58
+ }
59
+ catch (err) {
60
+ return assessRender({
61
+ route,
62
+ gotoError: err instanceof Error ? err.message : String(err),
63
+ status: undefined,
64
+ finalUrl: page.url(),
65
+ pngBytes: undefined,
66
+ });
67
+ }
68
+ rmSync(pngPath, { force: true });
69
+ try {
70
+ await page.screenshot({ path: pngPath, fullPage: true });
71
+ }
72
+ catch {
73
+ // Falls through to the missing-PNG check below.
74
+ }
75
+ const pngBytes = existsSync(pngPath) ? statSync(pngPath).size : undefined;
76
+ return assessRender({ route, status, finalUrl: page.url(), pngBytes });
77
+ }
@@ -0,0 +1,105 @@
1
+ /** One redirect the driver followed: the status and the raw `Location`. */
2
+ export interface RedirectHop {
3
+ status: number;
4
+ location: string | null;
5
+ }
6
+ export interface SweepVerdict {
7
+ ok: boolean;
8
+ reason: string;
9
+ }
10
+ /** A passing page verdict names the own-content marker that matched. */
11
+ export interface PageRenderVerdict extends SweepVerdict {
12
+ marker?: PageMarker;
13
+ }
14
+ export type PageMarker = "h1" | "auth-card" | "sign-in-gate";
15
+ /** More hops than this is a redirect chain, not a render. */
16
+ export declare const MAX_REDIRECTS = 5;
17
+ /**
18
+ * Routes that are correct to answer 404 in every leg, and the licence claim
19
+ * that gates them. No leg's served app holds a signed licence — the driver
20
+ * scrubs `AILK_LICENSE_KEY`, and the mock key is a fixture string, not a JWT —
21
+ * so each entry must answer exactly 404. A 200 means the gate stopped gating.
22
+ */
23
+ export declare const CLAIM_GATED_ROUTES: Readonly<Record<string, string>>;
24
+ /**
25
+ * A route whose live page needs an env var the driver never sets (#6146).
26
+ * While it is unset the route serves its Free placeholder seed instead
27
+ * (#6942): a 200 whose `<h1>` carries the `CONFIG_GATED_PLACEHOLDERS`
28
+ * heading. A 404 is the bug — the site's own nav link dead — and a rendered
29
+ * page without that heading means the gate stopped gating. Unlike
30
+ * `CLAIM_GATED_ROUTES`, a route absent from the route table is not stale: the
31
+ * module that owns it may simply not be installed in this leg.
32
+ */
33
+ export declare const CONFIG_GATED_ROUTES: Readonly<Record<string, string>>;
34
+ /**
35
+ * The placeholder heading each `CONFIG_GATED_ROUTES` entry renders while its
36
+ * env var is unset — for `/meet`, the hero headline of the overlay's
37
+ * `content/en/LandingPage/meet.mdx` seed. A fresh scaffold never edits it.
38
+ */
39
+ export declare const CONFIG_GATED_PLACEHOLDERS: Readonly<Record<string, string>>;
40
+ /** Reachability only — for routes that are not pages (`/og`, `/llms.txt`, …). */
41
+ export declare function assessRouteStatus(input: {
42
+ route: string;
43
+ hops: RedirectHop[];
44
+ status: number | null;
45
+ }): SweepVerdict;
46
+ /**
47
+ * A `CLAIM_GATED_ROUTES` entry: it must be in the route table (a missing one
48
+ * is stale) and answer exactly 404.
49
+ */
50
+ export declare function assessClaimGatedRoute(input: {
51
+ route: string;
52
+ routeTable: string[];
53
+ hops: RedirectHop[];
54
+ status: number | null;
55
+ }): SweepVerdict;
56
+ /**
57
+ * Decide whether a page route rendered, under the real locale, with its own
58
+ * content. The first failed rule is the reason:
59
+ *
60
+ * 1. no self-redirect, and at most `MAX_REDIRECTS` hops;
61
+ * 2. a final 200;
62
+ * 3. an HTML response;
63
+ * 4. `<html lang>` equal to `locale` — a page under a bogus locale fails;
64
+ * 5. no streamed error or not-found digest;
65
+ * 6. an own-content marker — a non-empty `<h1>` (a content page), an
66
+ * `auth-card` testid (an `(auth)` page), or a `sign-in-gate` testid (an
67
+ * `(authed)` page seen anonymously). The layout carries none of them.
68
+ */
69
+ export declare function assessPageRender(input: {
70
+ route: string;
71
+ hops: RedirectHop[];
72
+ status: number | null;
73
+ contentType: string | null;
74
+ html: string;
75
+ locale: string;
76
+ }): PageRenderVerdict;
77
+ /**
78
+ * A `CONFIG_GATED_ROUTES` entry. Unlike `assessClaimGatedRoute`, a route
79
+ * absent from the route table is not stale — the module that owns it may
80
+ * simply not be installed in this leg — so it PASSES, skipped, with no
81
+ * network call needed to decide that (mirroring how `assessClaimGatedRoute`
82
+ * is called with sentinel `hops`/`status` for its own absent case, never
83
+ * fetched). Once the route IS present it is scored as an ordinary page via
84
+ * `assessPageRender`; with its gating env var unset it must ALSO be the
85
+ * placeholder — an `<h1>` carrying its `CONFIG_GATED_PLACEHOLDERS` heading.
86
+ * Every failure with the env var unset names it, pinning the cause.
87
+ */
88
+ export declare function assessConfigGatedRoute(input: {
89
+ route: string;
90
+ envVar: string;
91
+ envSet: boolean;
92
+ routeTable: string[];
93
+ hops: RedirectHop[];
94
+ status: number | null;
95
+ contentType: string | null;
96
+ html: string;
97
+ locale: string;
98
+ }): SweepVerdict;
99
+ /**
100
+ * Every FILE under `dir`, recursing into directories, as sorted slash-joined
101
+ * paths relative to it — so `public/.well-known/security.txt` is listed and
102
+ * the `.well-known` directory never is. Anything that is neither (a symlink,
103
+ * a socket) throws rather than being silently followed or skipped.
104
+ */
105
+ export declare function listPublicFiles(dir: string): string[];