create-ailk 0.19.1 → 0.21.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 (199) hide show
  1. package/README.md +84 -7
  2. package/component-catalog.json +184 -6
  3. package/component-catalog.md +20 -9
  4. package/dist/apply-design.d.ts +9 -0
  5. package/dist/apply-design.js +78 -31
  6. package/dist/cli.js +35 -0
  7. package/dist/copy.d.ts +41 -0
  8. package/dist/copy.js +87 -21
  9. package/dist/design-home.d.ts +54 -11
  10. package/dist/design-home.js +160 -22
  11. package/dist/lib/design-checks.d.ts +6 -1
  12. package/dist/lib/design-checks.js +23 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +21 -0
  14. package/dist/lib/fetch-template-snapshot.js +205 -0
  15. package/dist/lib/merge-three-way.d.ts +31 -0
  16. package/dist/lib/merge-three-way.js +65 -0
  17. package/dist/module-architecture.js +55 -2
  18. package/dist/programmatic.d.ts +7 -1
  19. package/dist/programmatic.js +76 -22
  20. package/dist/site-roster.d.ts +4 -2
  21. package/dist/site-roster.js +7 -5
  22. package/dist/spoke-checkout.d.ts +7 -0
  23. package/dist/spoke-checkout.js +8 -1
  24. package/dist/starter-grid.js +16 -0
  25. package/dist/surfaces.js +78 -2
  26. package/dist/sync-routes.d.ts +28 -0
  27. package/dist/sync-routes.js +123 -1
  28. package/dist/template-record.d.ts +59 -0
  29. package/dist/template-record.js +183 -0
  30. package/dist/update.d.ts +53 -0
  31. package/dist/update.js +606 -0
  32. package/package.json +3 -3
  33. package/site-templates/_base/ailk-template.json +2 -2
  34. package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
  35. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  36. package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
  37. package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
  38. package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
  39. package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
  41. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
  43. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
  46. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
  48. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
  49. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
  50. package/templates/.claude/cloudparity-repo-hooks.json +9 -0
  51. package/templates/.claude/hooks/architecture-check.sh +8 -0
  52. package/templates/.claude/rules/loop-contract.md +129 -0
  53. package/templates/.claude/settings.json +3 -2
  54. package/templates/.env.example +13 -0
  55. package/templates/CLAUDE.md +1 -1
  56. package/templates/CONVENTIONS.md +3 -2
  57. package/templates/README.md +26 -25
  58. package/templates/_gitignore +162 -0
  59. package/templates/apps/api/CLAUDE.md +18 -10
  60. package/templates/apps/api/jest.config.cjs +1 -0
  61. package/templates/apps/api/package.json +2 -1
  62. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
  63. package/templates/apps/api/src/__tests__/setup.ts +7 -0
  64. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
  65. package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
  66. package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
  67. package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
  68. package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
  69. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
  70. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
  71. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
  72. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
  74. package/templates/apps/api/src/middleware/auth.ts +8 -4
  75. package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
  76. package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
  77. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
  78. package/templates/apps/api/src/routes/webhooks/README.md +33 -5
  79. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
  80. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
  81. package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
  82. package/templates/apps/api/src/server.ts +14 -2
  83. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
  84. package/templates/apps/api/src/services/audit-log.ts +72 -10
  85. package/templates/apps/api/src/services/default-tenant.ts +12 -3
  86. package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
  87. package/templates/apps/api/src/services/project-listings.ts +6 -1
  88. package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
  89. package/templates/apps/api/vercel.json +1 -1
  90. package/templates/apps/mcp/CLAUDE.md +19 -7
  91. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
  92. package/templates/apps/mcp/jest.config.cjs +1 -0
  93. package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
  94. package/templates/apps/mcp/src/tools/proposals.ts +27 -0
  95. package/templates/apps/mcp/src/workspace-server.ts +16 -0
  96. package/templates/apps/web/CLAUDE.md +26 -1
  97. package/templates/apps/web/_gitignore +6 -0
  98. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
  99. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
  100. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
  101. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
  111. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
  115. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
  117. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
  118. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
  119. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
  123. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
  124. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
  125. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
  129. package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
  132. package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
  133. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
  134. package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
  135. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  136. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  137. package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
  138. package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
  139. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
  140. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
  141. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
  143. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
  144. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
  145. package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
  146. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
  147. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
  148. package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
  149. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  150. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
  151. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  152. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  153. package/templates/apps/web/app/[locale]/layout.tsx +46 -4
  154. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
  155. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  156. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  157. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  158. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  159. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  160. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
  161. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
  162. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
  163. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
  164. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
  165. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  166. package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
  167. package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
  168. package/templates/apps/web/app/robots.txt/route.ts +13 -10
  169. package/templates/apps/web/jest.config.cjs +4 -0
  170. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
  171. package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
  172. package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
  173. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
  174. package/templates/apps/web/lib/account-config.ts +31 -16
  175. package/templates/apps/web/lib/consent-visitor.ts +103 -0
  176. package/templates/apps/web/lib/deck-page.ts +186 -0
  177. package/templates/apps/web/lib/relay.ts +20 -0
  178. package/templates/apps/web/messages/en.json +83 -9
  179. package/templates/apps/web/next.config.mjs +8 -2
  180. package/templates/database/CHANGELOG.md +48 -0
  181. package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
  182. package/templates/database/__tests__/person-notes.db.test.ts +249 -0
  183. package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
  184. package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
  185. package/templates/database/__tests__/seed.test.ts +39 -2
  186. package/templates/database/_gitignore +8 -0
  187. package/templates/database/inbox/schema.prisma +38 -12
  188. package/templates/database/jest.config.cjs +1 -0
  189. package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
  190. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
  191. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
  192. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
  193. package/templates/database/ops/schema.prisma +16 -0
  194. package/templates/database/package.json +1 -1
  195. package/templates/database/seed.ts +29 -0
  196. package/templates/project.yaml +6 -3
  197. package/templates/tools/jest/pin-timezone.cjs +34 -0
  198. package/versions.json +35 -35
  199. package/templates/.claude/rules/preserve-existing-style.md +0 -51
@@ -21,7 +21,7 @@
21
21
  * `architecture.yaml` template files — a fixed template IS the drift this
22
22
  * removes.
23
23
  */
24
- import { existsSync, readFileSync } from "node:fs";
24
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
25
25
  import { join, sep } from "node:path";
26
26
  import { CONTENT_PAYLOADS, } from "./content-payloads.js";
27
27
  import { importClosure } from "./lib/import-graph.js";
@@ -183,12 +183,18 @@ const MEMBER_AREA_FILES = [
183
183
  "apps/api/src/routes/member/request-form.ts",
184
184
  "apps/api/src/routes/member/billing.ts",
185
185
  "apps/api/src/routes/member/requests.ts",
186
+ // #8515 D13 — the client's onboarding read and write.
187
+ "apps/api/src/routes/member/onboarding.ts",
186
188
  "apps/api/src/routes/member/artifact-review.ts",
187
189
  "apps/api/src/routes/member/profile.ts",
188
190
  "apps/api/src/routes/member/deletion-request.ts",
189
191
  // #8438 D6 — the member's own consents; the same gate as the writes above.
190
192
  "apps/api/src/routes/member/consents.ts",
191
193
  "apps/api/src/services/member-area.ts",
194
+ // #8516 D8 — whether proposal payment runs, and the Checkout Session;
195
+ // imported only by member-area files (pieces.ts and the two member
196
+ // artifact routes), and itself imports only base files.
197
+ "apps/api/src/services/proposal-payments.ts",
192
198
  "apps/api/src/lib/member-store.ts",
193
199
  // #8226 D12 — the member-request match CLI; both import member-store.ts,
194
200
  // which a build without the member slice does not carry.
@@ -1488,6 +1494,13 @@ export const MODULE_SLICES = {
1488
1494
  "apps/api/src/routes/slack/index.ts",
1489
1495
  "apps/api/src/routes/slack/actions.ts",
1490
1496
  "apps/api/src/routes/waitlist-signups/index.ts",
1497
+ // #7479 — the listing-draft forwarded-visitor suite exercises the three
1498
+ // project-listings draft routes through the real build(), and those
1499
+ // routes register only under isModuleEnabled("signal-collectors"), so
1500
+ // the suite strips with this slice (the #7389 shape for
1501
+ // schedule-forwarded-rate-limit.test.ts under scheduling) instead of
1502
+ // failing every keyless scaffold's apps/api jest gate.
1503
+ "apps/api/src/__tests__/listings-forwarded-rate-limit.test.ts",
1491
1504
  // apps/api/src/routes/project-listings/{start,patch-draft,submit,public,
1492
1505
  // me,respond,resume-token}.ts and services/{project-listings,
1493
1506
  // listing-config}.ts deliberately NOT listed — see the comment above
@@ -2201,11 +2214,51 @@ export function missingArchitectureEnforcement(siteDir) {
2201
2214
  missing.push("package.json scripts.preflight (runs pnpm architecture)");
2202
2215
  }
2203
2216
  const postToolUse = readJson(".claude/settings.json")?.hooks?.PostToolUse ?? [];
2217
+ const ARCH_PNPM_RE = /\bpnpm\b.*\barchitecture\b/;
2218
+ // CloudParity's repo-owned-hook command grammar (#8509 D2/D6): no
2219
+ // $CLAUDE_PROJECT_DIR prefix, no arguments, no shell operators.
2220
+ const ARCH_HOOK_SCRIPT_RE = /^bash \.claude\/hooks\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/;
2221
+ /**
2222
+ * A command "runs architecture" either in today's inline form (matched on
2223
+ * the command string itself, so a pre-#8509 scaffold still verifies) or in
2224
+ * the script form CloudParity's declaration grammar requires — read the
2225
+ * named script's own text, so a deleted or emptied script still reports
2226
+ * missing even though the command string is unchanged.
2227
+ */
2228
+ const commandRunsArchitecture = (command) => {
2229
+ if (ARCH_PNPM_RE.test(command))
2230
+ return true;
2231
+ const match = ARCH_HOOK_SCRIPT_RE.exec(command);
2232
+ if (!match)
2233
+ return false;
2234
+ const scriptRel = match[1];
2235
+ // The charset `ARCH_HOOK_SCRIPT_RE` allows per segment admits "." and
2236
+ // "..", which CloudParity's own grammar forbids (#8509 spec §1) — reject
2237
+ // them here too, so a crafted command string can't walk this read
2238
+ // outside siteDir/.claude/hooks/.
2239
+ if (scriptRel.split("/").some((seg) => seg === "." || seg === ".."))
2240
+ return false;
2241
+ const hooksDir = join(siteDir, ".claude", "hooks");
2242
+ const scriptPath = join(hooksDir, scriptRel);
2243
+ if (!existsSync(scriptPath))
2244
+ return false;
2245
+ try {
2246
+ const realHooksDir = realpathSync(hooksDir);
2247
+ const realScript = realpathSync(scriptPath);
2248
+ if (!realScript.startsWith(realHooksDir + sep) || !lstatSync(realScript).isFile()) {
2249
+ return false;
2250
+ }
2251
+ return ARCH_PNPM_RE.test(readFileSync(realScript, "utf8"));
2252
+ }
2253
+ catch {
2254
+ return false;
2255
+ }
2256
+ };
2204
2257
  const hooked = postToolUse.some((entry) => {
2205
2258
  const tools = (entry.matcher ?? "").split("|");
2206
2259
  return (tools.includes("Edit") &&
2207
2260
  tools.includes("Write") &&
2208
- (entry.hooks ?? []).some((h) => /\bpnpm\b.*\barchitecture\b/.test(h.command ?? "")));
2261
+ (entry.hooks ?? []).some((h) => commandRunsArchitecture(h.command ?? "")));
2209
2262
  });
2210
2263
  if (!hooked) {
2211
2264
  missing.push(".claude/settings.json PostToolUse hook on Edit|Write (runs pnpm architecture)");
@@ -195,8 +195,14 @@ export interface ScaffoldResult {
195
195
  *
196
196
  * Throws ValidationError if targetDir already exists and is non-empty.
197
197
  * Throws Error if the template copy fails for any other reason.
198
+ *
199
+ * @param copied — INTERNAL (#8522 D2): when passed, every recordable path
200
+ * `copyTemplate` writes is hashed into this map (destination path -> the
201
+ * sha256 of the bytes written), for `scaffold()` to build
202
+ * `.ailk-template-files.json` from once every later layer has landed. No
203
+ * existing caller passes it, so their behaviour is unchanged.
198
204
  */
199
- export declare function scaffoldSync(opts: ScaffoldOptions): ScaffoldResult;
205
+ export declare function scaffoldSync(opts: ScaffoldOptions, copied?: Map<string, string>): ScaffoldResult;
200
206
  /**
201
207
  * Scaffold an AI Launch Kit starter project into targetDir (async wrapper).
202
208
  *
@@ -25,7 +25,7 @@ import { CATALOG_TARGET_PATH, writeComponentCatalog, } from "./component-catalog
25
25
  import { buildScaffoldMarkers, copyTemplate } from "./copy.js";
26
26
  import { deriveArchitecture } from "./derive-architecture.js";
27
27
  import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
28
- import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
28
+ import { DESIGN_SEED_PAGES, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, siteIdentityName, } from "./design-home.js";
29
29
  import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
30
30
  import { resolveDesign, spliceThemeBlock, } from "./design-scaffold.js";
31
31
  import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
@@ -48,6 +48,7 @@ import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from
48
48
  import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
49
49
  import { syncRoutes } from "./sync-routes.js";
50
50
  import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
51
+ import { TEMPLATE_RECORD_PATH, isRecordable, planTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
51
52
  import { SPOKE_ONLY_FILES, TopologyError, applySpokeTransforms, isSpokeExcluded, normalizeHubSignInUrl, normalizeHubUrl, readSiteTopology, spokeExclusions, spokeDestination, spokeModuleRefusal, spokeModules, writeHubOriginEnv, writeHubSignInEnv, } from "./topology.js";
52
53
  import { validateTarget } from "./validate.js";
53
54
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -148,8 +149,14 @@ function resolveShapeTemplate(shape) {
148
149
  *
149
150
  * Throws ValidationError if targetDir already exists and is non-empty.
150
151
  * Throws Error if the template copy fails for any other reason.
152
+ *
153
+ * @param copied — INTERNAL (#8522 D2): when passed, every recordable path
154
+ * `copyTemplate` writes is hashed into this map (destination path -> the
155
+ * sha256 of the bytes written), for `scaffold()` to build
156
+ * `.ailk-template-files.json` from once every later layer has landed. No
157
+ * existing caller passes it, so their behaviour is unchanged.
151
158
  */
152
- export function scaffoldSync(opts) {
159
+ export function scaffoldSync(opts, copied) {
153
160
  // Validate target — throws ValidationError on rejection.
154
161
  const targetAbs = validateTarget(opts.targetDir);
155
162
  const projectName = opts.projectName ?? basename(targetAbs);
@@ -177,6 +184,14 @@ export function scaffoldSync(opts) {
177
184
  : SPOKE_ONLY_FILES),
178
185
  ],
179
186
  markers: buildScaffoldMarkers(projectName),
187
+ ...(copied !== undefined
188
+ ? {
189
+ onWrite: (relPath, bytes) => {
190
+ if (isRecordable(relPath))
191
+ copied.set(relPath, sha256Hex(bytes));
192
+ },
193
+ }
194
+ : {}),
180
195
  });
181
196
  // #7426 D5/D6 — the root files a spoke rewrites, and the spoke seams.
182
197
  if (spoke !== undefined)
@@ -315,6 +330,10 @@ export async function scaffold(opts) {
315
330
  const designSeeds = resolvedDesign !== undefined
316
331
  ? designHomeSeeds(resolvedDesign, { designsDir: DESIGNS_DIR, catalog })
317
332
  : [];
333
+ // #8661 D11 — and its contact seeds, read and validated the same way.
334
+ const designContactSeeds = resolvedDesign !== undefined
335
+ ? designHomeSeeds(resolvedDesign, { designsDir: DESIGNS_DIR, catalog, page: "contact" })
336
+ : [];
318
337
  // #6813 D7 — the resolved design only when the CALLER chose one (`opts.design`);
319
338
  // a template's own declared design is never gated (#6778 option (c)).
320
339
  const chosenDesign = opts.design !== undefined ? resolvedDesign : undefined;
@@ -495,6 +514,10 @@ export async function scaffold(opts) {
495
514
  const surfaces = template !== undefined
496
515
  ? [...new Set([...(opts.surfaces ?? []), ...rosterSurfaces(template)])].sort()
497
516
  : opts.surfaces;
517
+ // #8522 D2 — every recordable path `copyTemplate` writes, hashed as it is
518
+ // written: the input to the per-file record `scaffold()` writes below,
519
+ // once every later layer has landed.
520
+ const copied = new Map();
498
521
  const result = scaffoldSync({
499
522
  ...opts,
500
523
  modules,
@@ -509,7 +532,7 @@ export async function scaffold(opts) {
509
532
  // #6813 D8 step 6 — the resolved design's OWN layout (never its name):
510
533
  // `resolveSiteChrome` reads a layout object now, not a `StarterDesign`.
511
534
  ...(resolvedDesign !== undefined ? { designLayout: resolvedDesign.pkg.layout } : {}),
512
- });
535
+ }, copied);
513
536
  // Threaded to extractModuleBundle for `package.json` patch targets'
514
537
  // `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
515
538
  // `scaffoldSync` already reads for `copyTemplate`, re-read here since
@@ -700,21 +723,35 @@ export async function scaffold(opts) {
700
723
  // every layer has landed, before the presets dress it. A home that is not
701
724
  // the shape's placeholder is never written; `--design standard` (no
702
725
  // seeds) leaves the shape's home byte-identical. The one
703
- // `planDesignHome` call site in scaffold().
704
- const homePath = join(result.targetDir, ...HOME_PAGE_PATH.split("/"));
705
- let homeSeeded = false;
726
+ // `planDesignHome` call site in scaffold(). #8661 D11 — the contact page
727
+ // is planned right after the home, its heading named from the scaffold's
728
+ // `siteIdentity.name`.
729
+ const seededPages = [];
706
730
  if (template !== undefined) {
707
- const homePlan = planDesignHome({
708
- current: existsSync(homePath) ? readFileSync(homePath, "utf8") : undefined,
709
- shape: template.shape,
710
- target: designSeeds.find((seed) => seed.shape === template.shape),
711
- shapeHome: readFileSync(shapeHomePath(template.shape, { siteTemplatesDir: SITE_TEMPLATES_DIR, templatesDir: TEMPLATES_DIR }), "utf8"),
712
- shippedSeeds: shippedHomeSeeds({ designsDir: DESIGNS_DIR, catalog }),
713
- catalog: catalog(),
714
- });
715
- if (homePlan.outcome === "seeded" && homePlan.text !== undefined) {
716
- writeFileSync(homePath, homePlan.text);
717
- homeSeeded = true;
731
+ const siteName = siteIdentityName(existsSync(siteMdxPath) ? readFileSync(siteMdxPath, "utf8") : undefined);
732
+ for (const [page, pageSeeds] of [
733
+ ["home", designSeeds],
734
+ ["contact", designContactSeeds],
735
+ ]) {
736
+ const pagePath = join(result.targetDir, ...DESIGN_SEED_PAGES[page].path.split("/"));
737
+ const plan = planDesignHome({
738
+ page,
739
+ siteName,
740
+ current: existsSync(pagePath) ? readFileSync(pagePath, "utf8") : undefined,
741
+ shape: template.shape,
742
+ target: pageSeeds.find((seed) => seed.shape === template.shape),
743
+ shapeHome: readFileSync(shapeHomePath(template.shape, {
744
+ siteTemplatesDir: SITE_TEMPLATES_DIR,
745
+ templatesDir: TEMPLATES_DIR,
746
+ page,
747
+ }), "utf8"),
748
+ shippedSeeds: shippedHomeSeeds({ designsDir: DESIGNS_DIR, catalog, page }),
749
+ catalog: catalog(),
750
+ });
751
+ if (plan.outcome === "seeded" && plan.text !== undefined) {
752
+ writeFileSync(pagePath, plan.text);
753
+ seededPages.push(page);
754
+ }
718
755
  }
719
756
  }
720
757
  // #6816 D6 step 10b — the design's section presets, written onto the
@@ -742,12 +779,18 @@ export async function scaffold(opts) {
742
779
  }
743
780
  }
744
781
  // #7743 D6 — the manifest's `/` row records the seeded home's sections,
745
- // as the presets left them.
782
+ // as the presets left them. #8661 — and the `/contact` row the seeded
783
+ // contact page's.
746
784
  const rosterPath = join(result.targetDir, SITE_ROSTER_PATH);
747
- if (homeSeeded && existsSync(rosterPath)) {
748
- const nextManifest = planHomeRecord(readFileSync(rosterPath, "utf8"), readFileSync(homePath, "utf8"), catalog());
749
- if (nextManifest !== undefined)
750
- writeFileSync(rosterPath, nextManifest);
785
+ if (seededPages.length > 0 && existsSync(rosterPath)) {
786
+ const recorded = readFileSync(rosterPath, "utf8");
787
+ let manifestText = recorded;
788
+ for (const page of seededPages) {
789
+ const pagePath = join(result.targetDir, ...DESIGN_SEED_PAGES[page].path.split("/"));
790
+ manifestText = planHomeRecord(manifestText, readFileSync(pagePath, "utf8"), catalog(), page) ?? manifestText;
791
+ }
792
+ if (manifestText !== recorded)
793
+ writeFileSync(rosterPath, manifestText);
751
794
  }
752
795
  }
753
796
  // #7838 D8 — a commerce spoke's own checkout: two routes, its config and
@@ -785,6 +828,17 @@ export async function scaffold(opts) {
785
828
  templatesDir: TEMPLATES_DIR,
786
829
  markers,
787
830
  }));
831
+ // #8522 D2 — the per-file record, as this scaffold's LAST write (same
832
+ // guard and roster as the site manifest just above): a kept copied
833
+ // path only when its final bytes still hash to the copy — a later
834
+ // layer (a module bundle's patch, a generator) that rewrote the file
835
+ // is absent here too.
836
+ writeFileSync(join(result.targetDir, TEMPLATE_RECORD_PATH), serializeTemplateRecord(planTemplateRecord({
837
+ targetDir: result.targetDir,
838
+ copied,
839
+ version: markers["CREATE_AILK_VERSION"] ?? "",
840
+ projectName,
841
+ })));
788
842
  }
789
843
  }
790
844
  // #7536 D3 — the registry follows `apps/web/package.json`, which a module
@@ -1,4 +1,5 @@
1
1
  import type { CompiledDesignTheme } from "./design-compile.js";
2
+ import { type DesignSeedPage } from "./design-home.js";
2
3
  import { type SectionLookCatalog } from "./design-presets.js";
3
4
  import { type PageRosterRow } from "./module-architecture.js";
4
5
  import type { ModuleName } from "./parse-args.js";
@@ -102,9 +103,10 @@ export declare const MANIFEST_OPTION_KEYS: readonly ["itemStyle", "actionsPositi
102
103
  * `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
103
104
  * when the record already says so, or the manifest has no `/` row with a
104
105
  * `sections` key. The value is spliced in place at its own depth; no other
105
- * byte moves.
106
+ * byte moves. `page: "contact"` records the `/contact` row from the contact
107
+ * page instead (#8661 D11).
106
108
  */
107
- export declare function planHomeRecord(manifestText: string, homeText: string, catalog: SectionLookCatalog): string | undefined;
109
+ export declare function planHomeRecord(manifestText: string, homeText: string, catalog: SectionLookCatalog, page?: DesignSeedPage): string | undefined;
108
110
  /**
109
111
  * The generator's row for each of a module's `pageContributions`, in
110
112
  * declaration order — read from the committed manifest of the first
@@ -28,7 +28,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
28
28
  import { join, relative, sep } from "node:path";
29
29
  import { isDeepStrictEqual } from "node:util";
30
30
  import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-core.js";
31
- import { HOME_PAGE_PATH } from "./design-home.js";
31
+ import { DESIGN_SEED_PAGES } from "./design-home.js";
32
32
  import { locatePageSections } from "./design-presets.js";
33
33
  import { diffTwoStrings } from "./lib/extract-module-bundle.js";
34
34
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
@@ -296,14 +296,16 @@ function scalarValue(raw) {
296
296
  * `MANIFEST_OPTION_KEYS` values, as the generator writes them. `undefined`
297
297
  * when the record already says so, or the manifest has no `/` row with a
298
298
  * `sections` key. The value is spliced in place at its own depth; no other
299
- * byte moves.
299
+ * byte moves. `page: "contact"` records the `/contact` row from the contact
300
+ * page instead (#8661 D11).
300
301
  */
301
- export function planHomeRecord(manifestText, homeText, catalog) {
302
+ export function planHomeRecord(manifestText, homeText, catalog, page = "home") {
303
+ const { route, path } = DESIGN_SEED_PAGES[page];
302
304
  const file = parseRosterFile(manifestText, SITE_ROSTER_PATH);
303
- const row = file.rows.find((r) => r.value["route"] === "/");
305
+ const row = file.rows.find((r) => r.value["route"] === route);
304
306
  if (row === undefined || !("sections" in row.value))
305
307
  return undefined;
306
- const sections = (locatePageSections(homeText, HOME_PAGE_PATH) ?? []).map((section) => {
308
+ const sections = (locatePageSections(homeText, path) ?? []).map((section) => {
307
309
  const options = {};
308
310
  for (const key of MANIFEST_OPTION_KEYS) {
309
311
  const entry = section.entries.find((e) => e.key === key);
@@ -8,6 +8,13 @@ export declare const SPOKE_CHECKOUT_PACKAGE = "@working-theory/spoke-checkout";
8
8
  export declare const SPOKE_CHECKOUT_CONFIG_PATH = "apps/web/lib/checkout-config.ts";
9
9
  /** The two return pages: Stripe's success URL and every refused buy's target. */
10
10
  export declare const SPOKE_CHECKOUT_PAGE_FILES: readonly string[];
11
+ /**
12
+ * The marker `renderPage()` stamps into both generated return pages' header
13
+ * comment (D3). `apps/web`'s structured-data test cannot import create-ailk
14
+ * (the import boundary), so it carries this text as a literal, kept in sync
15
+ * by `packages/create-ailk/__tests__/spoke-checkout.test.ts` (#8513 AC-5).
16
+ */
17
+ export declare const SPOKE_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
11
18
  /** The variables a commerce spoke depends on (D9). The package reads three of them. */
12
19
  export declare const SPOKE_CHECKOUT_ENV_CATALOG: Readonly<Record<string, EnvCatalogEntry>>;
13
20
  /** The names a commerce spoke declares although only the package reads them (D9). */
@@ -38,6 +38,13 @@ export const SPOKE_CHECKOUT_PAGE_FILES = Object.freeze([
38
38
  "apps/web/app/[locale]/checkout/thank-you/page.tsx",
39
39
  "apps/web/app/[locale]/checkout/unavailable/page.tsx",
40
40
  ]);
41
+ /**
42
+ * The marker `renderPage()` stamps into both generated return pages' header
43
+ * comment (D3). `apps/web`'s structured-data test cannot import create-ailk
44
+ * (the import boundary), so it carries this text as a literal, kept in sync
45
+ * by `packages/create-ailk/__tests__/spoke-checkout.test.ts` (#8513 AC-5).
46
+ */
47
+ export const SPOKE_CHECKOUT_PAGE_MARKER = "GENERATED by create-ailk (#7838)";
41
48
  /** The variables a commerce spoke depends on (D9). The package reads three of them. */
42
49
  export const SPOKE_CHECKOUT_ENV_CATALOG = Object.freeze({
43
50
  STRIPE_SECRET_KEY: {
@@ -245,7 +252,7 @@ function renderConfig(site) {
245
252
  /** One return page (D7): a server component that reads nothing but its copy. */
246
253
  function renderPage(opts) {
247
254
  return [
248
- `// ${opts.title} — GENERATED by create-ailk (#7838).`,
255
+ `// ${opts.title} — ${SPOKE_CHECKOUT_PAGE_MARKER}.`,
249
256
  ...opts.about.map((line) => `// ${line}`),
250
257
  `import { SectionRenderer } from "${UI_SECTIONS_PACKAGE}";`,
251
258
  `import type { PageSection } from "${VALIDATION_PACKAGE}";`,
@@ -117,6 +117,10 @@ const TEMPLATE_BASE_ROSTER = [
117
117
  { route: "/sign-up" },
118
118
  { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
119
119
  { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
120
+ {
121
+ route: "/working-agreement",
122
+ content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
123
+ },
120
124
  {
121
125
  route: "/purchase-policy",
122
126
  content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
@@ -235,6 +239,10 @@ const ORGANIZATION_SERVICE_ROWS = [
235
239
  { route: "/sign-up" },
236
240
  { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
237
241
  { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
242
+ {
243
+ route: "/working-agreement",
244
+ content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
245
+ },
238
246
  {
239
247
  route: "/purchase-policy",
240
248
  content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
@@ -267,6 +275,10 @@ const LOCAL_BUSINESS_SERVICE_ROWS = [
267
275
  },
268
276
  { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
269
277
  { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
278
+ {
279
+ route: "/working-agreement",
280
+ content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
281
+ },
270
282
  ];
271
283
  /**
272
284
  * `person-service`'s own rows (#6758 D1, D3), derived from
@@ -302,6 +314,10 @@ const PERSON_SERVICE_ROWS = [
302
314
  },
303
315
  { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
304
316
  { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
317
+ {
318
+ route: "/working-agreement",
319
+ content: ["content/en/WorkingAgreementPage/working-agreement.mdx"],
320
+ },
305
321
  ];
306
322
  /**
307
323
  * Each buildable shape's standard roster (#6753 D3) — the rows an alias starts
package/dist/surfaces.js CHANGED
@@ -94,6 +94,11 @@ export const CORE_ROUTE_DIRS = [
94
94
  // 404s. Neither `GATEABLE_SURFACES` nor `MODULE_SURFACES.commerce` gains
95
95
  // an entry — the claim decides whether the pages serve, not the scaffold.
96
96
  `${ROUTE_ROOT}/purchase`,
97
+ // #8438 D17/D21 — the one unsubscribe landing every signed link calls.
98
+ // Consent logging is part of the never-gated privacy floor (CLAUDE.md §
99
+ // License & tier hygiene's never-gate list), so this ships unconditionally
100
+ // like `/about`/`/contact`/`/faq` above — never a `GATEABLE_SURFACES` entry.
101
+ `${ROUTE_ROOT}/unsubscribe`,
97
102
  ];
98
103
  /**
99
104
  * Directory names skipped by `listTemplateRouteDirs`'s walker even though they
@@ -326,6 +331,13 @@ export const GATEABLE_SURFACES = [
326
331
  routes: [`${ROUTE_ROOT}/whitepapers`],
327
332
  content: ["content/<locale>/Whitepaper"],
328
333
  },
334
+ {
335
+ // #8661 D10 — the working agreement, module-less like `privacy` and
336
+ // `terms`: every shape roster names it right after /terms.
337
+ id: "working-agreement",
338
+ routes: [`${ROUTE_ROOT}/working-agreement`],
339
+ content: ["content/<locale>/WorkingAgreementPage"],
340
+ },
329
341
  ];
330
342
  /** The recognized `--surfaces` vocabulary, sorted. */
331
343
  export const RECOGNIZED_SURFACES = GATEABLE_SURFACES.map((s) => s.id);
@@ -547,6 +559,7 @@ export function isUnservedSurfaceContent(path, enabled) {
547
559
  */
548
560
  export const ROUTE_SINCE = {
549
561
  about: "0.0.0",
562
+ "about/[slug]": "0.42.0",
550
563
  "blog/[[...slug]]": "0.0.0",
551
564
  careers: "0.0.0",
552
565
  "careers/[slug]": "0.0.0",
@@ -633,6 +646,9 @@ export const ROUTE_SINCE = {
633
646
  "vs/[slug]": "0.28.0",
634
647
  whitepapers: "0.0.0",
635
648
  "whitepapers/[slug]": "0.0.0",
649
+ // #8661 D10 — the working-agreement page. Genuinely new, so it attaches at
650
+ // the fixed group's next release line, the same rule #7430 applied.
651
+ "working-agreement": "0.42.0",
636
652
  // #5014 — the (authed) route GROUP: layout.tsx + sign-in-gate.tsx sit
637
653
  // directly under "(authed)" (a Next.js route group — its parens are not a
638
654
  // URL segment); account/ and waitlist/ are its two syncable sub-trees.
@@ -666,12 +682,20 @@ export const ROUTE_SINCE = {
666
682
  // version, the same rule #7430/#8232 applied.
667
683
  "(authed)/account/security": "0.38.0",
668
684
  "(authed)/account/notifications": "0.38.0",
685
+ // #8438 D21 — User settings' fifth tab, Privacy, and the public
686
+ // unsubscribe landing every signed link calls. Genuinely new, so they
687
+ // attach at today's fixed-group version, the same rule #8339 applied.
688
+ "(authed)/account/privacy": "0.40.0",
689
+ unsubscribe: "0.40.0",
669
690
  // #8234 — the member Artifacts pages and a version's file route. Genuinely
670
691
  // new, so they attach at today's fixed-group version, the same rule #7430
671
692
  // applied.
672
693
  "(authed)/account/artifacts": "0.38.0",
673
694
  "(authed)/account/artifacts/[pieceId]": "0.38.0",
674
695
  "(authed)/account/artifacts/[pieceId]/versions/[version]/file": "0.38.0",
696
+ // #8515 — the client's onboarding page. Genuinely new, so it attaches at
697
+ // today's fixed-group version, the same rule #7430 applied.
698
+ "(authed)/account/onboarding": "0.40.0",
675
699
  // The leads inbox (#5678). The ROUTE ships in every scaffold, Free
676
700
  // included — lead capture is a never-gate surface, so reading a lead back
677
701
  // is one too. Whether it shows a ROW is a separate question with two
@@ -774,7 +798,13 @@ export const ROUTE_DEPENDENCIES = {
774
798
  "apps/web/lib/auth-routes.ts",
775
799
  ],
776
800
  "(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
777
- "(auth)/sign-up": ["apps/web/lib/auth-env.ts"],
801
+ // #8438 D13/D21 — the sign-up card's agreements gate also carries the
802
+ // carried-cookie mint, `sign-up.actions.ts`, beside it in this route's own
803
+ // directory.
804
+ "(auth)/sign-up": [
805
+ "apps/web/lib/auth-env.ts",
806
+ `${ROUTE_ROOT}/(auth)/sign-up/sign-up.actions.ts`,
807
+ ],
778
808
  "(auth)/forgot-password": ["apps/web/lib/auth-env.ts"],
779
809
  "(auth)/reset-password": ["apps/web/lib/auth-env.ts"],
780
810
  // #7430 — the group's layout reads `isAccountPath()` to give the account
@@ -789,6 +819,17 @@ export const ROUTE_DEPENDENCIES = {
789
819
  "apps/web/lib/server-api.ts",
790
820
  "apps/web/lib/site-brand.tsx",
791
821
  "apps/web/lib/site-logo.tsx",
822
+ // #8438 D13/D21 — two brand-new files in this already-existing route
823
+ // directory; a scaffold whose `(authed)/account` predates this unit
824
+ // needs them added, which the directory-level sync alone would not do
825
+ // (sync never overwrites or adds to a directory it already has).
826
+ // `acceptCarriedAgreementsAction` (the carried-cookie CONSUME side) lives
827
+ // in this directory's own `agreements-actions.ts`, never in
828
+ // `(auth)/sign-up` — see that file's header: `(auth)/**` is a sign-in
829
+ // seed a sign-in-less spoke drops, so a cross-directory reach from here
830
+ // into it would dangle.
831
+ `${ROUTE_ROOT}/(authed)/account/agreements-prompt.tsx`,
832
+ `${ROUTE_ROOT}/(authed)/account/agreements-actions.ts`,
792
833
  ],
793
834
  // #7430 — the three child routes also import files in their PARENT's
794
835
  // directory. A scaffold whose `(authed)/account` predates #7430 keeps its
@@ -875,12 +916,31 @@ export const ROUTE_DEPENDENCIES = {
875
916
  `${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
876
917
  ],
877
918
  // #8339 D11 — Notifications holds the former Privacy section's
878
- // cookie-settings link, moved verbatim from General.
919
+ // cookie-settings link, moved verbatim from General. #8438 D15 — that
920
+ // link moved on again, to Privacy; Notifications now reads the registry
921
+ // through `member.consents` instead, so it keeps the same dependency set.
879
922
  "(authed)/account/notifications": [
880
923
  "apps/web/lib/account-config.ts",
881
924
  "apps/web/lib/server-api.ts",
882
925
  `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
883
926
  `${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
927
+ `${ROUTE_ROOT}/(authed)/account/privacy/actions.ts`,
928
+ ],
929
+ // #8438 D14 — Privacy's five sections: the summary, the cookie-choice
930
+ // read from the browser cookie, data-use consents and read-only
931
+ // agreements (both through `member.consents`), and Request account
932
+ // deletion, moved here unchanged from General (D16) — so Privacy needs
933
+ // General's own deletion files too, the same cross-directory rule #8339
934
+ // D11 applied to Notifications' cookie-settings link.
935
+ "(authed)/account/privacy": [
936
+ "apps/web/lib/account-config.ts",
937
+ "apps/web/lib/server-api.ts",
938
+ `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
939
+ `${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
940
+ `${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
941
+ `${ROUTE_ROOT}/(authed)/account/profile-actions.ts`,
942
+ `${ROUTE_ROOT}/(authed)/account/general/deletion-request.tsx`,
943
+ `${ROUTE_ROOT}/(authed)/account/general/profile-source.ts`,
884
944
  ],
885
945
  "(authed)/account/billing": [
886
946
  "apps/web/lib/account-config.ts",
@@ -924,6 +984,17 @@ export const ROUTE_DEPENDENCIES = {
924
984
  `${ROUTE_ROOT}/(authed)/account/artifacts/artifact-actions.ts`,
925
985
  ],
926
986
  "(authed)/account/artifacts/[pieceId]/versions/[version]/file": ["apps/web/lib/server-api.ts"],
987
+ // #8515 D12 — the onboarding page imports its parent's account-data.ts
988
+ // and error-row.tsx (which renders retry-button.tsx); its actions forward
989
+ // the credential `forwardedCookieInit` returns. account-data.ts reads the
990
+ // account config, as the Artifacts pages' does.
991
+ "(authed)/account/onboarding": [
992
+ "apps/web/lib/account-config.ts",
993
+ "apps/web/lib/server-api.ts",
994
+ `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
995
+ `${ROUTE_ROOT}/(authed)/account/error-row.tsx`,
996
+ `${ROUTE_ROOT}/(authed)/account/retry-button.tsx`,
997
+ ],
927
998
  "(authed)/account/support": [
928
999
  "apps/web/lib/account-config.ts",
929
1000
  "apps/web/lib/server-api.ts",
@@ -997,10 +1068,13 @@ export const ROUTE_MESSAGE_NAMESPACES = {
997
1068
  "(authed)/account/general": ["account"],
998
1069
  "(authed)/account/security": ["account"],
999
1070
  "(authed)/account/notifications": ["account"],
1071
+ "(authed)/account/privacy": ["account"],
1000
1072
  "(authed)/account/billing": ["account"],
1001
1073
  // #8234 — the two Artifacts pages; the file route renders no string.
1002
1074
  "(authed)/account/artifacts": ["account"],
1003
1075
  "(authed)/account/artifacts/[pieceId]": ["account"],
1076
+ // #8515 — the onboarding page.
1077
+ "(authed)/account/onboarding": ["account"],
1004
1078
  "(authed)/admin": ["admin"],
1005
1079
  "(authed)/leads": ["leadsInbox"],
1006
1080
  // #5977 — every string on both editor routes comes from the `edit`
@@ -1012,6 +1086,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1012
1086
  "(authed)/waitlist": ["waitlistDashboard.gate"],
1013
1087
  // #7744 — the /team hub's empty state reads the `team` namespace.
1014
1088
  team: ["team"],
1089
+ // #8438 D21 — the unsubscribe landing's own namespace.
1090
+ unsubscribe: ["unsubscribe"],
1015
1091
  };
1016
1092
  /** Template-relative path of the generated site-nav registry. */
1017
1093
  export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
@@ -1,4 +1,6 @@
1
1
  import type { WorkspaceVersionMap } from "./substitute.js";
2
+ /** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
3
+ export declare const STRUCTURED_DATA_TEST_PATH = "apps/web/lib/__tests__/structured-data.test.tsx";
2
4
  export interface SyncRoutesOptions {
3
5
  /**
4
6
  * The already-scaffolded repo (absolute or relative). Must exist, be a
@@ -65,7 +67,32 @@ export interface SyncRoutesResult {
65
67
  missingMessageNamespaces: string[];
66
68
  /** Gateable surface ids the target does not carry, sorted. */
67
69
  skippedSurfaces: string[];
70
+ /**
71
+ * When `apply` added a route the template exempts in its structured-data
72
+ * test and the site's copy of that test is site-edited (its bytes differ
73
+ * from the recorded hash), each template `NO_STRUCTURED_DATA_ROUTES` entry
74
+ * the site's test lacks, verbatim, with the added routes it covers (#8704).
75
+ * Sorted by entry; empty otherwise. The site's test is never written.
76
+ */
77
+ structuredDataEntries: Array<{
78
+ routes: string[];
79
+ entry: string;
80
+ }>;
81
+ }
82
+ /** One `NO_STRUCTURED_DATA_ROUTES` element: its source text, dedented, and the routes it exempts. */
83
+ interface StructuredDataExemption {
84
+ route: string;
85
+ subtree: boolean;
86
+ text: string;
68
87
  }
88
+ /**
89
+ * The elements of `source`'s `NO_STRUCTURED_DATA_ROUTES` array literal, by a
90
+ * brace scan that skips string literals and comments. Empty when the
91
+ * file declares no such array. It assumes what the template's array holds:
92
+ * object literals of string, boolean and identifier-array values — no regex
93
+ * literal or `${…}` interpolation.
94
+ */
95
+ export declare function parseStructuredDataExemptions(source: string): StructuredDataExemption[];
69
96
  /**
70
97
  * The template's route directories, ROUTE_ROOT-relative and sorted.
71
98
  *
@@ -99,3 +126,4 @@ export declare const SYNC_ROUTES_HELP = "create-ailk sync-routes \u2014 copy tem
99
126
  * like `parseCliArgs`; at most one positional.
100
127
  */
101
128
  export declare function parseSyncRoutesArgs(argv: string[]): SyncRoutesArgs;
129
+ export {};