create-ailk 0.19.1 → 0.20.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 (132) hide show
  1. package/README.md +64 -7
  2. package/dist/cli.js +35 -0
  3. package/dist/copy.d.ts +41 -0
  4. package/dist/copy.js +87 -21
  5. package/dist/lib/fetch-template-snapshot.d.ts +21 -0
  6. package/dist/lib/fetch-template-snapshot.js +205 -0
  7. package/dist/lib/merge-three-way.d.ts +31 -0
  8. package/dist/lib/merge-three-way.js +65 -0
  9. package/dist/module-architecture.js +55 -2
  10. package/dist/programmatic.d.ts +7 -1
  11. package/dist/programmatic.js +32 -2
  12. package/dist/spoke-checkout.d.ts +7 -0
  13. package/dist/spoke-checkout.js +8 -1
  14. package/dist/surfaces.js +67 -2
  15. package/dist/template-record.d.ts +59 -0
  16. package/dist/template-record.js +183 -0
  17. package/dist/update.d.ts +48 -0
  18. package/dist/update.js +504 -0
  19. package/package.json +3 -3
  20. package/templates/.claude/cloudparity-repo-hooks.json +9 -0
  21. package/templates/.claude/hooks/architecture-check.sh +8 -0
  22. package/templates/.claude/rules/loop-contract.md +129 -0
  23. package/templates/.claude/settings.json +3 -2
  24. package/templates/.env.example +13 -0
  25. package/templates/CLAUDE.md +1 -1
  26. package/templates/CONVENTIONS.md +3 -2
  27. package/templates/_gitignore +162 -0
  28. package/templates/apps/api/CLAUDE.md +14 -10
  29. package/templates/apps/api/package.json +2 -1
  30. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
  31. package/templates/apps/api/src/__tests__/setup.ts +7 -0
  32. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
  33. package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
  34. package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
  35. package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
  36. package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
  37. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
  38. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
  39. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
  40. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
  41. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
  42. package/templates/apps/api/src/middleware/auth.ts +8 -4
  43. package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
  44. package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
  45. package/templates/apps/api/src/routes/webhooks/README.md +33 -5
  46. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
  47. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
  48. package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
  49. package/templates/apps/api/src/server.ts +14 -2
  50. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
  51. package/templates/apps/api/src/services/audit-log.ts +72 -10
  52. package/templates/apps/api/src/services/default-tenant.ts +12 -3
  53. package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
  54. package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
  55. package/templates/apps/api/vercel.json +1 -1
  56. package/templates/apps/mcp/CLAUDE.md +19 -7
  57. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +95 -8
  58. package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
  59. package/templates/apps/mcp/src/tools/proposals.ts +27 -0
  60. package/templates/apps/mcp/src/workspace-server.ts +16 -0
  61. package/templates/apps/web/_gitignore +6 -0
  62. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
  63. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
  64. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
  65. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
  66. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
  69. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
  70. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
  72. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
  73. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
  74. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
  75. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
  76. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
  77. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
  78. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
  79. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
  80. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
  81. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
  82. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
  83. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
  84. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
  85. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
  86. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
  87. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
  88. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
  89. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
  92. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
  93. package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
  94. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
  96. package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
  97. package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
  98. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +13 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
  100. package/templates/apps/web/app/[locale]/layout.tsx +23 -2
  101. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
  102. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
  103. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
  104. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
  105. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
  106. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
  107. package/templates/apps/web/app/robots.txt/route.ts +13 -10
  108. package/templates/apps/web/jest.config.cjs +3 -0
  109. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
  110. package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
  111. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +96 -7
  112. package/templates/apps/web/lib/account-config.ts +31 -16
  113. package/templates/apps/web/lib/consent-visitor.ts +103 -0
  114. package/templates/apps/web/lib/relay.ts +20 -0
  115. package/templates/apps/web/messages/en.json +67 -4
  116. package/templates/apps/web/next.config.mjs +8 -2
  117. package/templates/database/CHANGELOG.md +39 -0
  118. package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
  119. package/templates/database/__tests__/person-notes.db.test.ts +249 -0
  120. package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
  121. package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
  122. package/templates/database/_gitignore +8 -0
  123. package/templates/database/inbox/schema.prisma +38 -12
  124. package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
  125. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
  126. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
  127. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
  128. package/templates/database/ops/schema.prisma +16 -0
  129. package/templates/database/package.json +1 -1
  130. package/templates/project.yaml +6 -3
  131. package/versions.json +35 -35
  132. package/templates/.claude/rules/preserve-existing-style.md +0 -51
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `git merge-file` wrapper — the three-way text merge `create-ailk update`
3
+ * runs for a copied app file the site owner edited and the template also
4
+ * changed (#8522 D5).
5
+ *
6
+ * `git merge-file` is the reference diff3 merge: every AILK owner already
7
+ * has `git` on `PATH` (it is how they cloned or will push this site), and
8
+ * using it adds no new npm dependency. The three versions are written to a
9
+ * fresh temp directory (never the site's own tree) and read back as text;
10
+ * nothing is left behind.
11
+ */
12
+ import { spawnSync } from "node:child_process";
13
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ /**
17
+ * Run `git merge-file -p` over the three texts in a `mkdtemp` directory, no
18
+ * shell, argv holding only temp-file paths and the three labels (#8522 D5).
19
+ *
20
+ * Exit 0 is a clean merge (`merged`). Exit 1-127 is a conflicting hunk
21
+ * (`conflict`) — the file is never written with conflict markers by this
22
+ * function; the caller decides what to do with a conflict, and nothing here
23
+ * ever applies one. Any other exit, or a spawn failure (no `git` on `PATH`),
24
+ * is `error`.
25
+ */
26
+ export function mergeThreeWay(opts) {
27
+ const dir = mkdtempSync(join(tmpdir(), "ailk-merge-"));
28
+ try {
29
+ const oursPath = join(dir, "ours");
30
+ const basePath = join(dir, "base");
31
+ const theirsPath = join(dir, "theirs");
32
+ writeFileSync(oursPath, opts.ours);
33
+ writeFileSync(basePath, opts.base);
34
+ writeFileSync(theirsPath, opts.theirs);
35
+ const result = spawnSync("git", [
36
+ "merge-file",
37
+ "-p",
38
+ "-L",
39
+ opts.oursLabel,
40
+ "-L",
41
+ opts.baseLabel,
42
+ "-L",
43
+ opts.theirsLabel,
44
+ oursPath,
45
+ basePath,
46
+ theirsPath,
47
+ ], { shell: false });
48
+ if (result.error !== undefined) {
49
+ return { status: "error", reason: result.error.message };
50
+ }
51
+ if (result.status === 0) {
52
+ return { status: "merged", text: result.stdout.toString("utf8") };
53
+ }
54
+ if (result.status !== null && result.status >= 1 && result.status <= 127) {
55
+ return { status: "conflict" };
56
+ }
57
+ return {
58
+ status: "error",
59
+ reason: `git merge-file exited ${result.status ?? `signal ${result.signal}`}`,
60
+ };
61
+ }
62
+ finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ }
@@ -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
  *
@@ -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)
@@ -495,6 +510,10 @@ export async function scaffold(opts) {
495
510
  const surfaces = template !== undefined
496
511
  ? [...new Set([...(opts.surfaces ?? []), ...rosterSurfaces(template)])].sort()
497
512
  : opts.surfaces;
513
+ // #8522 D2 — every recordable path `copyTemplate` writes, hashed as it is
514
+ // written: the input to the per-file record `scaffold()` writes below,
515
+ // once every later layer has landed.
516
+ const copied = new Map();
498
517
  const result = scaffoldSync({
499
518
  ...opts,
500
519
  modules,
@@ -509,7 +528,7 @@ export async function scaffold(opts) {
509
528
  // #6813 D8 step 6 — the resolved design's OWN layout (never its name):
510
529
  // `resolveSiteChrome` reads a layout object now, not a `StarterDesign`.
511
530
  ...(resolvedDesign !== undefined ? { designLayout: resolvedDesign.pkg.layout } : {}),
512
- });
531
+ }, copied);
513
532
  // Threaded to extractModuleBundle for `package.json` patch targets'
514
533
  // `workspace:*` substitution (spec 5418 § 3 D2) — the same version map
515
534
  // `scaffoldSync` already reads for `copyTemplate`, re-read here since
@@ -785,6 +804,17 @@ export async function scaffold(opts) {
785
804
  templatesDir: TEMPLATES_DIR,
786
805
  markers,
787
806
  }));
807
+ // #8522 D2 — the per-file record, as this scaffold's LAST write (same
808
+ // guard and roster as the site manifest just above): a kept copied
809
+ // path only when its final bytes still hash to the copy — a later
810
+ // layer (a module bundle's patch, a generator) that rewrote the file
811
+ // is absent here too.
812
+ writeFileSync(join(result.targetDir, TEMPLATE_RECORD_PATH), serializeTemplateRecord(planTemplateRecord({
813
+ targetDir: result.targetDir,
814
+ copied,
815
+ version: markers["CREATE_AILK_VERSION"] ?? "",
816
+ projectName,
817
+ })));
788
818
  }
789
819
  }
790
820
  // #7536 D3 — the registry follows `apps/web/package.json`, which a module
@@ -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}";`,
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
@@ -666,12 +671,20 @@ export const ROUTE_SINCE = {
666
671
  // version, the same rule #7430/#8232 applied.
667
672
  "(authed)/account/security": "0.38.0",
668
673
  "(authed)/account/notifications": "0.38.0",
674
+ // #8438 D21 — User settings' fifth tab, Privacy, and the public
675
+ // unsubscribe landing every signed link calls. Genuinely new, so they
676
+ // attach at today's fixed-group version, the same rule #8339 applied.
677
+ "(authed)/account/privacy": "0.40.0",
678
+ unsubscribe: "0.40.0",
669
679
  // #8234 — the member Artifacts pages and a version's file route. Genuinely
670
680
  // new, so they attach at today's fixed-group version, the same rule #7430
671
681
  // applied.
672
682
  "(authed)/account/artifacts": "0.38.0",
673
683
  "(authed)/account/artifacts/[pieceId]": "0.38.0",
674
684
  "(authed)/account/artifacts/[pieceId]/versions/[version]/file": "0.38.0",
685
+ // #8515 — the client's onboarding page. Genuinely new, so it attaches at
686
+ // today's fixed-group version, the same rule #7430 applied.
687
+ "(authed)/account/onboarding": "0.40.0",
675
688
  // The leads inbox (#5678). The ROUTE ships in every scaffold, Free
676
689
  // included — lead capture is a never-gate surface, so reading a lead back
677
690
  // is one too. Whether it shows a ROW is a separate question with two
@@ -774,7 +787,13 @@ export const ROUTE_DEPENDENCIES = {
774
787
  "apps/web/lib/auth-routes.ts",
775
788
  ],
776
789
  "(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
777
- "(auth)/sign-up": ["apps/web/lib/auth-env.ts"],
790
+ // #8438 D13/D21 — the sign-up card's agreements gate also carries the
791
+ // carried-cookie mint, `sign-up.actions.ts`, beside it in this route's own
792
+ // directory.
793
+ "(auth)/sign-up": [
794
+ "apps/web/lib/auth-env.ts",
795
+ `${ROUTE_ROOT}/(auth)/sign-up/sign-up.actions.ts`,
796
+ ],
778
797
  "(auth)/forgot-password": ["apps/web/lib/auth-env.ts"],
779
798
  "(auth)/reset-password": ["apps/web/lib/auth-env.ts"],
780
799
  // #7430 — the group's layout reads `isAccountPath()` to give the account
@@ -789,6 +808,17 @@ export const ROUTE_DEPENDENCIES = {
789
808
  "apps/web/lib/server-api.ts",
790
809
  "apps/web/lib/site-brand.tsx",
791
810
  "apps/web/lib/site-logo.tsx",
811
+ // #8438 D13/D21 — two brand-new files in this already-existing route
812
+ // directory; a scaffold whose `(authed)/account` predates this unit
813
+ // needs them added, which the directory-level sync alone would not do
814
+ // (sync never overwrites or adds to a directory it already has).
815
+ // `acceptCarriedAgreementsAction` (the carried-cookie CONSUME side) lives
816
+ // in this directory's own `agreements-actions.ts`, never in
817
+ // `(auth)/sign-up` — see that file's header: `(auth)/**` is a sign-in
818
+ // seed a sign-in-less spoke drops, so a cross-directory reach from here
819
+ // into it would dangle.
820
+ `${ROUTE_ROOT}/(authed)/account/agreements-prompt.tsx`,
821
+ `${ROUTE_ROOT}/(authed)/account/agreements-actions.ts`,
792
822
  ],
793
823
  // #7430 — the three child routes also import files in their PARENT's
794
824
  // directory. A scaffold whose `(authed)/account` predates #7430 keeps its
@@ -875,12 +905,31 @@ export const ROUTE_DEPENDENCIES = {
875
905
  `${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
876
906
  ],
877
907
  // #8339 D11 — Notifications holds the former Privacy section's
878
- // cookie-settings link, moved verbatim from General.
908
+ // cookie-settings link, moved verbatim from General. #8438 D15 — that
909
+ // link moved on again, to Privacy; Notifications now reads the registry
910
+ // through `member.consents` instead, so it keeps the same dependency set.
879
911
  "(authed)/account/notifications": [
880
912
  "apps/web/lib/account-config.ts",
881
913
  "apps/web/lib/server-api.ts",
882
914
  `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
883
915
  `${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
916
+ `${ROUTE_ROOT}/(authed)/account/privacy/actions.ts`,
917
+ ],
918
+ // #8438 D14 — Privacy's five sections: the summary, the cookie-choice
919
+ // read from the browser cookie, data-use consents and read-only
920
+ // agreements (both through `member.consents`), and Request account
921
+ // deletion, moved here unchanged from General (D16) — so Privacy needs
922
+ // General's own deletion files too, the same cross-directory rule #8339
923
+ // D11 applied to Notifications' cookie-settings link.
924
+ "(authed)/account/privacy": [
925
+ "apps/web/lib/account-config.ts",
926
+ "apps/web/lib/server-api.ts",
927
+ `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
928
+ `${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
929
+ `${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
930
+ `${ROUTE_ROOT}/(authed)/account/profile-actions.ts`,
931
+ `${ROUTE_ROOT}/(authed)/account/general/deletion-request.tsx`,
932
+ `${ROUTE_ROOT}/(authed)/account/general/profile-source.ts`,
884
933
  ],
885
934
  "(authed)/account/billing": [
886
935
  "apps/web/lib/account-config.ts",
@@ -924,6 +973,17 @@ export const ROUTE_DEPENDENCIES = {
924
973
  `${ROUTE_ROOT}/(authed)/account/artifacts/artifact-actions.ts`,
925
974
  ],
926
975
  "(authed)/account/artifacts/[pieceId]/versions/[version]/file": ["apps/web/lib/server-api.ts"],
976
+ // #8515 D12 — the onboarding page imports its parent's account-data.ts
977
+ // and error-row.tsx (which renders retry-button.tsx); its actions forward
978
+ // the credential `forwardedCookieInit` returns. account-data.ts reads the
979
+ // account config, as the Artifacts pages' does.
980
+ "(authed)/account/onboarding": [
981
+ "apps/web/lib/account-config.ts",
982
+ "apps/web/lib/server-api.ts",
983
+ `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
984
+ `${ROUTE_ROOT}/(authed)/account/error-row.tsx`,
985
+ `${ROUTE_ROOT}/(authed)/account/retry-button.tsx`,
986
+ ],
927
987
  "(authed)/account/support": [
928
988
  "apps/web/lib/account-config.ts",
929
989
  "apps/web/lib/server-api.ts",
@@ -997,10 +1057,13 @@ export const ROUTE_MESSAGE_NAMESPACES = {
997
1057
  "(authed)/account/general": ["account"],
998
1058
  "(authed)/account/security": ["account"],
999
1059
  "(authed)/account/notifications": ["account"],
1060
+ "(authed)/account/privacy": ["account"],
1000
1061
  "(authed)/account/billing": ["account"],
1001
1062
  // #8234 — the two Artifacts pages; the file route renders no string.
1002
1063
  "(authed)/account/artifacts": ["account"],
1003
1064
  "(authed)/account/artifacts/[pieceId]": ["account"],
1065
+ // #8515 — the onboarding page.
1066
+ "(authed)/account/onboarding": ["account"],
1004
1067
  "(authed)/admin": ["admin"],
1005
1068
  "(authed)/leads": ["leadsInbox"],
1006
1069
  // #5977 — every string on both editor routes comes from the `edit`
@@ -1012,6 +1075,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1012
1075
  "(authed)/waitlist": ["waitlistDashboard.gate"],
1013
1076
  // #7744 — the /team hub's empty state reads the `team` namespace.
1014
1077
  team: ["team"],
1078
+ // #8438 D21 — the unsubscribe landing's own namespace.
1079
+ unsubscribe: ["unsubscribe"],
1015
1080
  };
1016
1081
  /** Template-relative path of the generated site-nav registry. */
1017
1082
  export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
@@ -0,0 +1,59 @@
1
+ /** Where the per-file record lives, beside `.ailk-site-manifest.json`. */
2
+ export declare const TEMPLATE_RECORD_PATH = ".ailk-template-files.json";
3
+ /**
4
+ * The app paths a generator rewrites after `copyTemplate` lands its copy
5
+ * (§1): the nav, the site chrome, the site fonts, the paid-sections
6
+ * registry, and the four spoke seams. Composed from each writer's own
7
+ * exported path constant, so a renamed constant carries its path along
8
+ * here too (#8522 D2).
9
+ */
10
+ export declare const REGENERATED_APP_PATHS: readonly string[];
11
+ /**
12
+ * Whether a destination path (`/`-separated, as `templateDestination`
13
+ * returns it) is one `update` ever records or touches (#8522 D2): under one
14
+ * of the three app trees, not a `package.json`, not an `.env*` file, no
15
+ * `.claude` segment, and not a path a generator rewrites after the copy.
16
+ */
17
+ export declare function isRecordable(destRel: string): boolean;
18
+ /** The hex sha256 digest of `bytes` — the one hash every record row uses. */
19
+ export declare function sha256Hex(bytes: Uint8Array): string;
20
+ export interface TemplateFileRow {
21
+ sha256: string;
22
+ version: string;
23
+ }
24
+ export interface TemplateRecord {
25
+ schema: 1;
26
+ projectName: string;
27
+ files: Record<string, TemplateFileRow>;
28
+ }
29
+ /** Thrown by `parseTemplateRecord` on a malformed or unsafe record. */
30
+ export declare class TemplateRecordError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ /**
34
+ * Parse `.ailk-template-files.json` text, refusing — naming the field — on:
35
+ * JSON that does not parse; `schema !== 1`; an unknown top-level key; an
36
+ * empty `projectName`; a `files` key that is not a recordable path; a
37
+ * `sha256` that is not 64 lowercase hex; a `version` that is not a semver
38
+ * string (#8522 D1).
39
+ */
40
+ export declare function parseTemplateRecord(text: string): TemplateRecord;
41
+ /** `record` as `scaffold()` and `update` write it: sorted keys, trailing newline. */
42
+ export declare function serializeTemplateRecord(record: TemplateRecord): string;
43
+ export interface PlanTemplateRecordOptions {
44
+ /** The freshly scaffolded site. */
45
+ targetDir: string;
46
+ /** Destination path -> sha256 of the bytes `copyTemplate`'s `onWrite` wrote, for every recordable path. */
47
+ copied: ReadonlyMap<string, string>;
48
+ /** The create-ailk version that wrote this scaffold. */
49
+ version: string;
50
+ projectName: string;
51
+ }
52
+ /**
53
+ * The record a fresh scaffold writes (#8522 D2): one row per copied path
54
+ * whose file still exists and whose current bytes still hash to the copy —
55
+ * a later layer (a module bundle's patch, a generator) that rewrote the
56
+ * file drops it from the record, exactly as it is absent from the record a
57
+ * `create-ailk update` keeps current for it.
58
+ */
59
+ export declare function planTemplateRecord(opts: PlanTemplateRecordOptions): TemplateRecord;