create-ailk 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/component-catalog.json +149 -5
  2. package/component-catalog.md +7 -6
  3. package/dist/add-module.js +11 -9
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +7 -5
  8. package/dist/derive-architecture.js +9 -32
  9. package/dist/lib/design-checks.js +4 -2
  10. package/dist/lib/env-catalog.js +0 -4
  11. package/dist/lib/npm-auth.d.ts +9 -0
  12. package/dist/lib/npm-auth.js +48 -0
  13. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  14. package/dist/lib/scaffold-render-sweep.js +5 -8
  15. package/dist/module-architecture.js +98 -10
  16. package/dist/paid-sections.d.ts +21 -2
  17. package/dist/paid-sections.js +69 -7
  18. package/dist/seed-flows.d.ts +4 -1
  19. package/dist/seed-flows.js +50 -4
  20. package/dist/spoke-checkout.d.ts +38 -35
  21. package/dist/spoke-checkout.js +66 -229
  22. package/dist/starter-grid.d.ts +0 -3
  23. package/dist/starter-grid.js +24 -14
  24. package/dist/surfaces.js +33 -12
  25. package/dist/sync-routes.d.ts +38 -1
  26. package/dist/sync-routes.js +184 -11
  27. package/dist/template-record.d.ts +1 -1
  28. package/dist/template-record.js +3 -2
  29. package/dist/update.js +10 -0
  30. package/package.json +3 -3
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  32. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  35. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  37. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  38. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  39. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  40. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  41. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  42. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  43. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/templates/.env.example +9 -7
  48. package/templates/.github/workflows/ci.yml +13 -0
  49. package/templates/README.md +1 -0
  50. package/templates/_gitignore +4 -0
  51. package/templates/apps/api/.env.example +27 -0
  52. package/templates/apps/api/CLAUDE.md +77 -16
  53. package/templates/apps/api/package.json +1 -0
  54. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  55. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  56. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  57. package/templates/apps/api/src/config/index.ts +109 -0
  58. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  59. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  60. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  61. package/templates/apps/api/src/lib/stripe.ts +74 -4
  62. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  63. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  64. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  65. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  66. package/templates/apps/api/src/openapi/spec.ts +10 -7
  67. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  68. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  69. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  70. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  71. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  72. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  73. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  74. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
  75. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  76. package/templates/apps/api/src/routes/consent/account.ts +4 -2
  77. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
  78. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  79. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  80. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  81. package/templates/apps/api/src/routes/content/index.ts +16 -2
  82. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  83. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  84. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  85. package/templates/apps/api/src/routes/leads/index.ts +17 -14
  86. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  87. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  88. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  89. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  90. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  91. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  92. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  93. package/templates/apps/api/src/routes/webhooks/stripe.ts +91 -17
  94. package/templates/apps/api/src/server.ts +140 -51
  95. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  96. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  97. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  98. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
  99. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  100. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  101. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  102. package/templates/apps/api/src/services/entitlements.ts +157 -1
  103. package/templates/apps/api/src/services/lead-routing.ts +113 -58
  104. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  105. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  106. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  107. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  108. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  109. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  110. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  111. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  112. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  113. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  114. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  115. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  116. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  117. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  118. package/templates/apps/api/src/services/person.ts +124 -9
  119. package/templates/apps/api/src/services/plan-offerings.ts +96 -30
  120. package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
  121. package/templates/apps/mcp/CLAUDE.md +22 -14
  122. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  123. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  124. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  125. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  126. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  127. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  128. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  129. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  130. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  131. package/templates/apps/mcp/src/server.ts +5 -0
  132. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  133. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  134. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  135. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  136. package/templates/apps/web/.env.example +10 -31
  137. package/templates/apps/web/CLAUDE.md +315 -42
  138. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
  139. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
  140. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
  141. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  142. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
  143. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
  144. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
  145. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
  146. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  147. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  148. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
  149. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
  150. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
  151. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  152. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
  153. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
  154. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +6 -2
  155. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
  156. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
  157. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  158. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  159. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  160. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  161. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  162. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  163. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  164. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  165. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  166. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  167. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  168. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  169. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  170. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  171. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  172. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  173. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  174. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  175. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  176. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  177. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  178. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  179. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  180. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  181. package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
  182. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  183. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  184. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  185. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  186. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
  187. package/templates/apps/web/lib/auth-routes.ts +36 -2
  188. package/templates/apps/web/lib/checkout-config.ts +9 -20
  189. package/templates/apps/web/lib/deck-page.ts +1 -0
  190. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  191. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  192. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  193. package/templates/apps/web/lib/modules.generated.ts +9 -0
  194. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  195. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  196. package/templates/apps/web/lib/posting-title.ts +17 -0
  197. package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
  198. package/templates/apps/web/messages/en.json +49 -15
  199. package/templates/apps/web/middleware.ts +12 -2
  200. package/templates/apps/web/security-headers.config.ts +17 -0
  201. package/templates/content/en/BlogList/blog.mdx +4 -71
  202. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  203. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  204. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  205. package/templates/content/en/BookCall/start-project.mdx +1 -17
  206. package/templates/content/en/ContactPage/contact.mdx +6 -63
  207. package/templates/content/en/HomePage/home.mdx +3 -21
  208. package/templates/database/CHANGELOG.md +31 -0
  209. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  210. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  211. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  212. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  213. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  214. package/templates/database/__tests__/seed.test.ts +31 -1
  215. package/templates/database/auth/schema.prisma +4 -0
  216. package/templates/database/config/schema.prisma +91 -0
  217. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  218. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  219. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  220. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  221. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  222. package/templates/database/package.json +1 -1
  223. package/templates/database/seed.ts +30 -2
  224. package/versions.json +35 -35
  225. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  226. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  227. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  229. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  230. package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
  231. package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
  232. package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
  233. package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
  234. package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
  235. package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
  236. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  237. package/templates/apps/web/lib/checkout.ts +0 -93
  238. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -46,17 +46,14 @@ export const CLAIM_GATED_ROUTES = {
46
46
  * `CLAIM_GATED_ROUTES`, a route absent from the route table is not stale: the
47
47
  * module that owns it may simply not be installed in this leg.
48
48
  */
49
- export const CONFIG_GATED_ROUTES = {
50
- "/meet": "SCHEDULING_CAL_LINK",
51
- };
49
+ export const CONFIG_GATED_ROUTES = {};
52
50
  /**
53
51
  * The placeholder heading each `CONFIG_GATED_ROUTES` entry renders while its
54
- * env var is unset — for `/meet`, the hero headline of the overlay's
55
- * `content/en/LandingPage/meet.mdx` seed. A fresh scaffold never edits it.
52
+ * env var is unset. Empty since #8843 D14: `/meet` reads the workspace's
53
+ * meetings and answers 200 with its own H1 unconfigured, so no route is
54
+ * config-gated today.
56
55
  */
57
- export const CONFIG_GATED_PLACEHOLDERS = {
58
- "/meet": "{Book a time with us}",
59
- };
56
+ export const CONFIG_GATED_PLACEHOLDERS = {};
60
57
  const LOOPBACK_BASE = "http://localhost";
61
58
  function pathnameOf(location, relativeTo) {
62
59
  return new URL(location, new URL(relativeTo, LOOPBACK_BASE)).pathname;
@@ -793,6 +793,9 @@ export const MODULE_SLICES = {
793
793
  "apps/api/src/routes/workspaces/index.ts",
794
794
  "apps/api/src/routes/workspaces/sessions.ts",
795
795
  "apps/api/src/routes/workspaces/config.ts",
796
+ // #8926 D1 — the workspace Offerings routes, registered in the
797
+ // multi-tenant tenant scope beside the billing routes.
798
+ "apps/api/src/routes/workspaces/offerings.ts",
796
799
  // #8438 D8 — the Admin owner's consents to Working Theory: registered
797
800
  // under `isModuleEnabled("multi-tenant")` alone, the module that
798
801
  // contributes the Admin app.
@@ -805,6 +808,8 @@ export const MODULE_SLICES = {
805
808
  "apps/api/src/routes/billing/index.ts",
806
809
  "apps/api/src/routes/billing/portal.ts",
807
810
  "apps/api/src/routes/billing/read.ts",
811
+ "apps/api/src/routes/billing/stripe-connection.ts",
812
+ "apps/api/src/routes/billing/stripe-products.ts",
808
813
  "apps/api/src/routes/billing/usage.ts",
809
814
  "apps/api/src/routes/waitlist-experiments/index.ts",
810
815
  "apps/api/src/routes/waitlist-experiments/list.ts",
@@ -854,6 +859,10 @@ export const MODULE_SLICES = {
854
859
  "apps/api/src/routes/workspaces/submissions.ts",
855
860
  "apps/api/src/routes/workspaces/subscribers.ts",
856
861
  "apps/api/src/services/inbox-submissions.ts",
862
+ // #9170 — the Inbox orders read: the same gate and the same
863
+ // tenant-db.ts import, co-owned with `workspace` for the same reason.
864
+ "apps/api/src/routes/workspaces/orders.ts",
865
+ "apps/api/src/services/inbox-orders.ts",
857
866
  // #852 D8 — the People reads: the same workspace||multi-tenant gate and
858
867
  // the same tenant-db.ts import, co-owned with `workspace` for the same
859
868
  // reason.
@@ -1023,6 +1032,9 @@ export const MODULE_SLICES = {
1023
1032
  "apps/api/src/routes/workspaces/submissions.ts",
1024
1033
  "apps/api/src/routes/workspaces/subscribers.ts",
1025
1034
  "apps/api/src/services/inbox-submissions.ts",
1035
+ // Co-owned with `multi-tenant` (#9170), as the reads above.
1036
+ "apps/api/src/routes/workspaces/orders.ts",
1037
+ "apps/api/src/services/inbox-orders.ts",
1026
1038
  // Co-owned with `multi-tenant` (#852 D8), as the reads above.
1027
1039
  "apps/api/src/routes/workspaces/people.ts",
1028
1040
  // Co-owned with `multi-tenant` (#7877 D9, D11) — see that slice's entry.
@@ -1134,8 +1146,16 @@ export const MODULE_SLICES = {
1134
1146
  pattern: CAL_DOMAIN_PATTERN,
1135
1147
  // The two server-side apps: apps/api holds the provider client, and
1136
1148
  // apps/mcp's tools name booking links in their fixtures. Everything
1137
- // else — apps/web above all — is browser-reachable.
1138
- paths_excluded: ["apps/api/**", "apps/mcp/**"],
1149
+ // else — apps/web above all — is browser-reachable. The two web files
1150
+ // are the booking pages' CSP (#8843 D12): the policy file names the
1151
+ // embed's origin only to admit it on those pages, and the middleware
1152
+ // applies it. Same carve-out as the source monorepo's architecture.yaml.
1153
+ paths_excluded: [
1154
+ "apps/api/**",
1155
+ "apps/mcp/**",
1156
+ "apps/web/security-headers.config.ts",
1157
+ "apps/web/middleware.ts",
1158
+ ],
1139
1159
  reason: "Only this site's server tier addresses the provider; every browser call goes to this site's own /v1/schedule/* routes instead",
1140
1160
  },
1141
1161
  {
@@ -1224,10 +1244,22 @@ export const MODULE_SLICES = {
1224
1244
  "apps/api/src/services/scheduling/__tests__/cal-provider.test.ts",
1225
1245
  "apps/mcp/src/tools/list_availability.ts",
1226
1246
  "apps/mcp/src/tools/get_event_meta.ts",
1247
+ // #8843 D5 — the twin of GET /v1/schedule/meetings.
1248
+ "apps/mcp/src/tools/list_meetings.ts",
1227
1249
  "apps/mcp/src/tools/create_booking.ts",
1228
1250
  "apps/web/app/[locale]/meet/page.tsx",
1229
1251
  "apps/web/app/[locale]/meet/[slug]/page.tsx",
1230
1252
  "apps/web/app/[locale]/meet/confirmed/page.tsx",
1253
+ // #8843 D6–D8 — the booking pages' one builder, the index frame
1254
+ // `/meet` and `/private` share, the private index, and their suites,
1255
+ // individually (§ D12, property 1: a glob ships no test).
1256
+ "apps/web/lib/meetings.tsx",
1257
+ "apps/web/lib/__tests__/meetings.test.tsx",
1258
+ "apps/web/lib/__tests__/meetings.fixtures.ts",
1259
+ "apps/web/app/[locale]/meet/meeting-index.tsx",
1260
+ "apps/web/app/[locale]/private/page.tsx",
1261
+ "apps/web/app/[locale]/private/__tests__/page.test.tsx",
1262
+ "apps/web/app/[locale]/private/__tests__/sitemap.test.ts",
1231
1263
  // #6085 (brief H7) — the six files the webhook receiver + the
1232
1264
  // tenant-guarded appointments read add. Individually, never as a
1233
1265
  // glob (§ D12 / this card's own comment above, property 1: a glob
@@ -1269,6 +1301,9 @@ export const MODULE_SLICES = {
1269
1301
  "apps/api/src/lib/public-meetings-config.ts",
1270
1302
  "apps/api/src/routes/schedule/meeting-types.ts",
1271
1303
  "apps/api/src/routes/schedule/__tests__/meeting-types.test.ts",
1304
+ // #8843 D1 — the meetings read's own suite; the route is in
1305
+ // `routes/schedule/index.ts`, listed above.
1306
+ "apps/api/src/routes/schedule/__tests__/meetings.test.ts",
1272
1307
  // #7532 — a hub spoke's meeting types and booking, through the real
1273
1308
  // scheduling routes; it ships and strips with them.
1274
1309
  "apps/api/src/routes/schedule/__tests__/spoke.test.ts",
@@ -1301,6 +1336,10 @@ export const MODULE_SLICES = {
1301
1336
  "apps/api/src/services/scheduling/resolve.ts",
1302
1337
  "apps/api/src/services/scheduling/__tests__/connections.test.ts",
1303
1338
  "apps/api/src/services/scheduling/__tests__/provider-resolution.test.ts",
1339
+ // #9129 — the meetings-list assembly the meetings route and the member
1340
+ // meeting types share, plus its suite.
1341
+ "apps/api/src/services/scheduling/meetings.ts",
1342
+ "apps/api/src/services/scheduling/__tests__/meetings.test.ts",
1304
1343
  "apps/api/src/routes/workspaces/__tests__/meetings-connection.test.ts",
1305
1344
  "apps/api/src/routes/schedule/__tests__/public-connection.test.ts",
1306
1345
  "apps/api/src/routes/webhooks/__tests__/calcom-connection.test.ts",
@@ -1323,14 +1362,16 @@ export const MODULE_SLICES = {
1323
1362
  "apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts",
1324
1363
  "apps/api/src/__tests__/openapi-keyless-hub.test.ts",
1325
1364
  ],
1326
- // pageContributions (spec 5478; parent § D6 shape 1) — the booker, the
1327
- // per-booking-type page and the confirmation page, whose route files
1328
- // `ownedFiles` above already carries. No row names seed content: all
1329
- // three render from the /v1/schedule/* routes, not from an MDX file.
1365
+ // pageContributions (spec 5478; parent § D6 shape 1) — the meeting
1366
+ // index, the meeting page, the confirmation page and the private index
1367
+ // (#8843 D8), whose route files `ownedFiles` above already carries. No
1368
+ // row names seed content: all four render from the /v1/schedule/*
1369
+ // routes, not from an MDX file.
1330
1370
  pageContributions: [
1331
1371
  { route: "/meet" },
1332
1372
  { route: "/meet/[slug]" },
1333
1373
  { route: "/meet/confirmed" },
1374
+ { route: "/private" },
1334
1375
  // #6086 — the authed appointments page. Bracket (roster) grammar and no
1335
1376
  // `content` key, for the same reason the three /meet rows carry none:
1336
1377
  // it renders from `/v1/schedule/appointments`, not from an MDX file.
@@ -1668,6 +1709,16 @@ export const MODULE_SLICES = {
1668
1709
  // `NESTED_FOREIGN_MODULE_ENTRIES` for the companion fix that keeps its
1669
1710
  // presence from falsely reading as "projects carried".
1670
1711
  "apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx",
1712
+ // #8840 D11/D13 — the Job application magnet's two walks. Same kind as
1713
+ // `projects/apply` above: they sit under the UNRELATED "careers"
1714
+ // surface's URL segment, which `excludedRoutePaths` does not gate on
1715
+ // signal-collectors, so `ownedFiles` keeps them out of a keyless
1716
+ // scaffold, and `NESTED_FOREIGN_MODULE_ENTRIES` keeps their presence
1717
+ // from reading as "careers carried". `flows/[slug]/magnet-page.tsx`,
1718
+ // the renderer they mount, rides the `flows` surface's directory with
1719
+ // its siblings (the note above).
1720
+ "apps/web/app/[locale]/careers/apply/page.tsx",
1721
+ "apps/web/app/[locale]/careers/[slug]/apply/page.tsx",
1671
1722
  // #5581 U4 — the eleven signal-collectors components + the slice's own
1672
1723
  // re-export entry point moved to packages-paid/ui-signal-collectors;
1673
1724
  // the package is delivered to a keyed scaffold via `patches` below,
@@ -1683,6 +1734,17 @@ export const MODULE_SLICES = {
1683
1734
  // spec 6892 D2 — the /reports hub, alongside its reader above: also a
1684
1735
  // URL segment MODULE_SURFACES["signal-collectors"] does not gate.
1685
1736
  "apps/web/app/[locale]/reports/page.tsx",
1737
+ // #8848 D6, D7 — the hub deliverable at `/reports/<slug>`: its renderer,
1738
+ // the gated walk and the ungated file's byte proxy. Same segment, same
1739
+ // reason as the reader above.
1740
+ "apps/web/app/[locale]/reports/deliverable-page.tsx",
1741
+ "apps/web/app/[locale]/reports/[slug]/start/page.tsx",
1742
+ "apps/web/app/[locale]/reports/[slug]/file/route.ts",
1743
+ // #8848 D12 — `/downloads` redirects into `/reports`, so a scaffold
1744
+ // without this module ships no `/downloads` at all (the redirect target
1745
+ // is this module's) rather than the old broken list.
1746
+ "apps/web/app/[locale]/downloads/page.tsx",
1747
+ "apps/web/app/[locale]/downloads/[slug]/page.tsx",
1686
1748
  //
1687
1749
  // The two sections that route composes — `report-header` and
1688
1750
  // `reports-list` — are deliberately NOT owned here. They are
@@ -1721,6 +1783,9 @@ export const MODULE_SLICES = {
1721
1783
  // the module gets them — `moduleRosterRows` derives the code-route form
1722
1784
  // for this row rather than requiring a template to ship one (D9).
1723
1785
  { route: "/reports/[slug]", contentDir: "content/en/ReportPage" },
1786
+ // #8848 D7's walk (`/reports/[slug]/start`) is NOT a contribution: a
1787
+ // contribution needs a shipped template manifest to carry its row, and
1788
+ // none does yet. Its code ships through `ownedFiles` above.
1724
1789
  ],
1725
1790
  // installMap (#5618 D6.3) — the module's sample files, moved here out
1726
1791
  // of `site-templates/marketing-site/` so the module OWNS its samples
@@ -1750,6 +1815,12 @@ export const MODULE_SLICES = {
1750
1815
  // code of its own for either route.
1751
1816
  // `generate-sample-report.py` keeps its root-level destination
1752
1817
  // (finding 1 — AC7 forbids changing the Pro file set).
1818
+ // `diagnostic` and `join-waitlist` are the starter's two placeholder
1819
+ // flow seeds (#8850 D5): every visible string a `{brace}`, every key one
1820
+ // a spoke may seed, so `create-ailk seed-flows` sends them to a hub with
1821
+ // no edits once `spokeDestination` lands them at `flows/<slug>.json`. A
1822
+ // hub gets them as inert `database/flows/` files, as it gets
1823
+ // `fit-check` and `referral`.
1753
1824
  installMap: {
1754
1825
  "packages/create-ailk/module-templates/signal-collectors/content/en/ProjectsList/projects.mdx": "content/en/ProjectsList/projects.mdx",
1755
1826
  "packages/create-ailk/module-templates/signal-collectors/content/en/Project/fit-check-funnel.mdx": "content/en/Project/fit-check-funnel.mdx",
@@ -1765,6 +1836,8 @@ export const MODULE_SLICES = {
1765
1836
  "packages/create-ailk/module-templates/signal-collectors/content/en/LandingPage/referral.mdx": "content/en/LandingPage/referral.mdx",
1766
1837
  "packages/create-ailk/module-templates/signal-collectors/flows/fit-check.flow.json": "database/flows/fit-check.flow.json",
1767
1838
  "packages/create-ailk/module-templates/signal-collectors/flows/referral.flow.json": "database/flows/referral.flow.json",
1839
+ "packages/create-ailk/module-templates/signal-collectors/flows/diagnostic.flow.json": "database/flows/diagnostic.flow.json",
1840
+ "packages/create-ailk/module-templates/signal-collectors/flows/join-waitlist.flow.json": "database/flows/join-waitlist.flow.json",
1768
1841
  "packages/create-ailk/module-templates/signal-collectors/waitlist/launch-waitlist.experiment.json": "database/waitlist/launch-waitlist.experiment.json",
1769
1842
  "packages/create-ailk/module-templates/signal-collectors/reports/sample-report.pdf": "apps/web/public/reports/sample-report.pdf",
1770
1843
  "packages/create-ailk/module-templates/signal-collectors/reports/generate-sample-report.py": "reports/generate-sample-report.py",
@@ -1812,10 +1885,10 @@ export const MODULE_SLICES = {
1812
1885
  // checkout (`SPOKE_HUB_CHECKOUT_SEEDS`: flows/[slug]/checkout.actions.ts,
1813
1886
  // the flow results pages, dev/purchase — spoke-intake Rule 3). What lands
1814
1887
  // is the scaffold-commerce skill, the commerce-boundary rule and the
1815
- // @working-theory/ui-commerce dependency (`patches` below); the spoke's own
1816
- // checkout — the standard spoke checkout, `apps/web` template source
1817
- // (two routes, lib/checkout.ts, checkout-config.ts, two return pages and
1818
- // their tests) — is copied by `writeSpokeCheckoutSlice`
1888
+ // @working-theory/ui-commerce dependency (`patches` below). A spoke sells
1889
+ // through its hub's Offerings buy route (#9142); its remaining checkout
1890
+ // files — `apps/web` template source (checkout-config.ts, two return
1891
+ // pages and their tests) — are copied by `writeSpokeCheckoutSlice`
1819
1892
  // (`spoke-checkout.ts`, #8748), not carried here.
1820
1893
  spoke: true,
1821
1894
  paidSectionsPackage: "@working-theory/ui-commerce",
@@ -2138,6 +2211,18 @@ export function coOwnedModuleDestinations() {
2138
2211
  // red the scaffold's database jest run. Matched exactly, not by basename
2139
2212
  // in `EXCLUDE_FILES`, which would drop a `build.test.ts` anywhere in the
2140
2213
  // snapshot; every other database suite still ships.
2214
+ // - the /pricing suite `monorepo-only.test.tsx` and its snapshot (#9179) —
2215
+ // it snapshots the monorepo's own `content/en/Pricing/pricing.mdx`, which
2216
+ // a scaffold replaces with its seed, and reads
2217
+ // `packages/api-client/src/offerings.ts`, which no scaffold carries. Only
2218
+ // those two assertions moved there; `unconnected.test.tsx` and
2219
+ // `offerings.test.tsx` keep every scaffold-meaningful case and ship.
2220
+ // - the Offerings `delegate-confinement.test.ts` (#9179) — it runs
2221
+ // `git grep -- apps apps-paid packages` from the repo root to prove the
2222
+ // `stripeConnection` and `offeringsCheckoutSession` delegates are each
2223
+ // named by one file. A scaffold is not a git repository. Only those two
2224
+ // cases moved there; `connection.test.ts` and
2225
+ // `checkout-session-record.test.ts` keep the stores' coverage and ship.
2141
2226
  export const EXCLUDE_TEMPLATE_DIRS = [
2142
2227
  "apps/web/__tests__",
2143
2228
  "apps/web/app/__tests__",
@@ -2163,6 +2248,9 @@ export const EXCLUDE_TEMPLATE_DIRS = [
2163
2248
  "apps/mcp/__tests__/parity.test.ts",
2164
2249
  "apps/mcp/__tests__/audit.test.ts",
2165
2250
  "database/__tests__/build.test.ts",
2251
+ "apps/web/app/[locale]/pricing/__tests__/monorepo-only.test.tsx",
2252
+ "apps/web/app/[locale]/pricing/__tests__/__snapshots__/monorepo-only.test.tsx.snap",
2253
+ "apps/api/src/services/offerings/__tests__/delegate-confinement.test.ts",
2166
2254
  ];
2167
2255
  export function paidModuleTemplateExclusions(snapshotRoots, repoRoot) {
2168
2256
  const roots = new Set(snapshotRoots);
@@ -1,5 +1,7 @@
1
1
  /** Scaffold-relative path of the generated registry. */
2
2
  export declare const PAID_SECTIONS_PATH = "apps/web/lib/paid-sections.generated.ts";
3
+ /** Scaffold-relative path of the generated module-presence flags (#8843 D10). */
4
+ export declare const MODULE_FLAGS_PATH = "apps/web/lib/modules.generated.ts";
3
5
  export interface PaidSectionRow {
4
6
  module: string;
5
7
  package: string;
@@ -15,9 +17,26 @@ export declare function paidSectionRows(): PaidSectionRow[];
15
17
  * public strip drops the file (\`check-oss-strip.sh\` condition (e)).
16
18
  */
17
19
  export declare function renderPaidSections(present: ReadonlySet<string>): string;
20
+ /**
21
+ * Render the module-presence flags. Line 1 is `// oss: false` exactly when a
22
+ * flag is on, the registry's own rule: an `on` flag names pages the public
23
+ * tree does not carry.
24
+ */
25
+ export declare function renderModuleFlags(flags: {
26
+ schedulingPages: boolean;
27
+ }): string;
28
+ /**
29
+ * Write the module-presence flags into a scaffold from its own
30
+ * `apps/web/package.json` (#8843 D10) — never copied from a template, so a
31
+ * site without the scheduling module can never carry `true`. Answers whether
32
+ * the file was `added`, `updated` (changed bytes) or `unchanged`, or
33
+ * `undefined` (writing nothing) for a scaffold with no web app.
34
+ */
35
+ export declare function writeModuleFlags(targetDir: string): "added" | "updated" | "unchanged" | undefined;
18
36
  /**
19
37
  * Write the registry into a scaffold, deriving presence from the scaffold's
20
- * own `apps/web/package.json`. Returns `false` (writing nothing) when that
21
- * file or `apps/web/lib/` is absent — a scaffold with no web app.
38
+ * own `apps/web/package.json`, and the module-presence flags beside it.
39
+ * Returns `false` (writing nothing) when that file or `apps/web/lib/` is
40
+ * absent — a scaffold with no web app.
22
41
  */
23
42
  export declare function writePaidSections(targetDir: string): boolean;
@@ -15,6 +15,13 @@
15
15
  * every layer (snapshot transform, module `patches`, `add-module`) already
16
16
  * keeps true. The monorepo's committed copy is the render with every row
17
17
  * present and is pinned byte-equal by `__tests__/paid-sections.test.ts`.
18
+ *
19
+ * Its sibling, `apps/web/lib/modules.generated.ts` (#8843 D10), is the
20
+ * module-presence flag: `SCHEDULING_PAGES` is true exactly when the site
21
+ * carries the scheduling slice's UI package, so the locale layout drops the
22
+ * marketing chrome on the booking pages only where they exist. A separate
23
+ * file, importing nothing, because the registry's `import()`s would pull UI
24
+ * packages into any edge code that reads the flag.
18
25
  */
19
26
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
27
  import { join } from "node:path";
@@ -22,6 +29,10 @@ import { MODULE_SLICES } from "./module-architecture.js";
22
29
  import { RECOGNIZED_MODULES } from "./parse-args.js";
23
30
  /** Scaffold-relative path of the generated registry. */
24
31
  export const PAID_SECTIONS_PATH = "apps/web/lib/paid-sections.generated.ts";
32
+ /** Scaffold-relative path of the generated module-presence flags (#8843 D10). */
33
+ export const MODULE_FLAGS_PATH = "apps/web/lib/modules.generated.ts";
34
+ /** The dependency whose presence means the scheduling module's pages exist. */
35
+ const SCHEDULING_PAGES_PACKAGE = "@working-theory/ui-scheduling";
25
36
  function pascalCase(shortName) {
26
37
  return shortName
27
38
  .split("-")
@@ -93,27 +104,78 @@ export function renderPaidSections(present) {
93
104
  }
94
105
  return `${lines.join("\n")}\n`;
95
106
  }
107
+ const MODULE_FLAGS_HEADER = `// modules.generated.ts — which module pages this site carries.
108
+ //
109
+ // GENERATED by create-ailk from this app's package.json dependencies (#8843).
110
+ // Do not edit: every scaffold, \`create-ailk add-module\` and
111
+ // \`create-ailk update\` rewrites it. It imports nothing, so edge code may
112
+ // read it as well as the locale layout.
113
+ `;
96
114
  /**
97
- * Write the registry into a scaffold, deriving presence from the scaffold's
98
- * own `apps/web/package.json`. Returns `false` (writing nothing) when that
99
- * file or `apps/web/lib/` is absent — a scaffold with no web app.
115
+ * Render the module-presence flags. Line 1 is `// oss: false` exactly when a
116
+ * flag is on, the registry's own rule: an `on` flag names pages the public
117
+ * tree does not carry.
100
118
  */
101
- export function writePaidSections(targetDir) {
119
+ export function renderModuleFlags(flags) {
120
+ const lines = [];
121
+ if (flags.schedulingPages)
122
+ lines.push("// oss: false");
123
+ lines.push(MODULE_FLAGS_HEADER.trimEnd());
124
+ lines.push("");
125
+ lines.push("/** The scheduling module's booking pages (/meet, /private) exist. */");
126
+ lines.push(`export const SCHEDULING_PAGES: boolean = ${String(flags.schedulingPages)};`);
127
+ return `${lines.join("\n")}\n`;
128
+ }
129
+ /** The web app's `dependencies`, or `undefined` for a scaffold with no web app. */
130
+ function webDependencies(targetDir, writing) {
102
131
  const pkgPath = join(targetDir, "apps/web/package.json");
103
132
  if (!existsSync(pkgPath) || !existsSync(join(targetDir, "apps/web/lib"))) {
104
- return false;
133
+ return undefined;
105
134
  }
106
135
  let pkg;
107
136
  try {
108
137
  pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
109
138
  }
110
139
  catch (err) {
111
- throw new Error(`cannot read ${pkgPath} to write the paid-sections registry: ${err.message}`, { cause: err });
140
+ throw new Error(`cannot read ${pkgPath} to write the ${writing}: ${err.message}`, { cause: err });
112
141
  }
113
- const deps = pkg.dependencies ?? {};
142
+ return pkg.dependencies ?? {};
143
+ }
144
+ /**
145
+ * Write the module-presence flags into a scaffold from its own
146
+ * `apps/web/package.json` (#8843 D10) — never copied from a template, so a
147
+ * site without the scheduling module can never carry `true`. Answers whether
148
+ * the file was `added`, `updated` (changed bytes) or `unchanged`, or
149
+ * `undefined` (writing nothing) for a scaffold with no web app.
150
+ */
151
+ export function writeModuleFlags(targetDir) {
152
+ const deps = webDependencies(targetDir, "module flags");
153
+ if (deps === undefined)
154
+ return undefined;
155
+ const path = join(targetDir, MODULE_FLAGS_PATH);
156
+ const before = existsSync(path) ? readFileSync(path, "utf8") : undefined;
157
+ const next = renderModuleFlags({
158
+ schedulingPages: Object.hasOwn(deps, SCHEDULING_PAGES_PACKAGE),
159
+ });
160
+ if (before === next)
161
+ return "unchanged";
162
+ writeFileSync(path, next);
163
+ return before === undefined ? "added" : "updated";
164
+ }
165
+ /**
166
+ * Write the registry into a scaffold, deriving presence from the scaffold's
167
+ * own `apps/web/package.json`, and the module-presence flags beside it.
168
+ * Returns `false` (writing nothing) when that file or `apps/web/lib/` is
169
+ * absent — a scaffold with no web app.
170
+ */
171
+ export function writePaidSections(targetDir) {
172
+ const deps = webDependencies(targetDir, "paid-sections registry");
173
+ if (deps === undefined)
174
+ return false;
114
175
  const present = new Set(paidSectionRows()
115
176
  .map((row) => row.package)
116
177
  .filter((name) => Object.hasOwn(deps, name)));
117
178
  writeFileSync(join(targetDir, PAID_SECTIONS_PATH), renderPaidSections(present));
179
+ writeModuleFlags(targetDir);
118
180
  return true;
119
181
  }
@@ -26,6 +26,8 @@ export type SeedFlowOutcome = {
26
26
  ok: true;
27
27
  result: string;
28
28
  status: string;
29
+ /** #8850 D6 — the flow's copy is the starter's `{brace}` placeholder. */
30
+ placeholder?: true;
29
31
  } | {
30
32
  slug: string;
31
33
  ok: false;
@@ -33,6 +35,7 @@ export type SeedFlowOutcome = {
33
35
  } | {
34
36
  slug: string;
35
37
  ok: "dry-run";
38
+ placeholder?: true;
36
39
  };
37
40
  export interface SeedFlowsResult {
38
41
  /** The hub origin, when one resolved (a dry run may have none). */
@@ -49,7 +52,7 @@ export interface SeedFlowsResult {
49
52
  /** The whole report, one line per row — what `seedFlows` printed as it went. */
50
53
  export declare function formatSeedFlowsReport(result: SeedFlowsResult): string;
51
54
  export declare function seedFlows(opts: SeedFlowsOptions): Promise<SeedFlowsResult>;
52
- export declare const SPOKE_FLOWS_README = "# flows/\n\nEach file here is one **declared flow**: `flows/<slug>.json` holds one flow\nconfig, exactly as the hub stores it. This repo owns these configs \u2014 the hub\nruns them for this site.\n\n## What a spoke may declare\n\nA spoke key may seed only what this site's own flow pages show:\n`slug`, `defaultLocale`, `steps`, `thankYou` and `flags`. Anything the\nhub acts on outside these pages \u2014 emails, deliverables, pricing, registers,\nlistings \u2014 is refused (`field_not_seedable`). A flow that needs one of those\nis authored in the hub's Admin instead.\n\nThe file name must be the config's `slug` plus `.json`.\n\n## Seeding them into the hub\n\nRun this as part of each rollout, from the site's root, with the spoke key\nyour hub issued set as `AILK_SPOKE_KEY` in the environment (a shell or CI\nsecret \u2014 never a file in this repo):\n\n npx create-ailk seed-flows .\n\nThe hub origin comes from `--hub-url`, else `AILK_API_URL`, else the\n`AILK_API_URL=` line of `apps/web/.env.local`. The key is read from the\nenvironment only. Each flow answers `created`, `updated` or `unchanged`,\nso running it again is safe. `--dry-run` checks the files and sends nothing.\n\nThe hub's operator must first turn seeding on (`AILK_SPOKE_FLOW_SEED=true`\non the hub). Until it is on and this command has run, links to these flows\n(`/flows/<slug>`) reach no flow.\n\n## Who owns what\n\nThe next seed that differs overwrites the hub's copy of a flow's config, so\nedit flows here, not in the Admin. A flow's status (live or draft) is the\nhub's: switching a seeded flow to draft in the Admin keeps it draft through\nevery later seed. Removing a file here does not delete the hub's flow. If\nthe Admin adds a block only it can author (an email, a deliverable, an\noffer), the hub refuses the next seed of that flow (`slug_owned`) rather\nthan delete the block.\n";
55
+ export declare const SPOKE_FLOWS_README = "# flows/\n\nEach file here is one **declared flow**: `flows/<slug>.json` holds one flow\nconfig, exactly as the hub stores it. This repo owns these configs \u2014 the hub\nruns them for this site.\n\n## What a spoke may declare\n\nA spoke key may seed only what this site's own flow pages show:\n`slug`, `defaultLocale`, `steps`, `thankYou` and `flags`. Anything the\nhub acts on outside these pages \u2014 emails, deliverables, pricing, registers,\nlistings \u2014 is refused (`field_not_seedable`). A flow that needs one of those\nis authored in the hub's Admin instead.\n\nThe file name must be the config's `slug` plus `.json`.\n\n## Seeding them into the hub\n\nRun this as part of each rollout, from the site's root, with the spoke key\nyour hub issued set as `AILK_SPOKE_KEY` in the environment (a shell or CI\nsecret \u2014 never a file in this repo):\n\n npx create-ailk seed-flows .\n\nThe hub origin comes from `--hub-url`, else `AILK_API_URL`, else the\n`AILK_API_URL=` line of `apps/web/.env.local`. The key is read from the\nenvironment only. Each flow answers `created`, `updated` or `unchanged`,\nso running it again is safe. `--dry-run` checks the files and sends nothing.\n\nThe hub's operator must first turn seeding on (`AILK_SPOKE_FLOW_SEED=true`\non the hub). Until it is on and this command has run, links to these flows\n(`/flows/<slug>`) reach no flow.\n\n## The starter's placeholder flows\n\nThe `signal-collectors` module ships two declared flows here:\n`flows/diagnostic.json` (an email step, then two questions) and\n`flows/join-waitlist.json` (an email and an optional name). Every visible\nstring in both is a `{brace}` placeholder: replace each one with your own\ncopy. Both go live on the hub with the first seed, so each line this command\nprints for them says they still carry placeholder copy.\n\nA flow is reached at `/flows/<slug>`. To give one its own page pair, run:\n\n npx create-ailk add-route magnet --path /diagnostic --flow diagnostic\n npx create-ailk add-route magnet --path /waitlist --flow join-waitlist\n\nA job application comes with the `careers` surface. Contact, newsletter and\njob application are the hub's own standard magnets: the hub seeds them in\nevery workspace and refuses a spoke's copy of each, so they need no file\nhere. If the hub's Admin already owns a magnet under one of these two slugs,\nthe hub refuses the seed (`slug_owned`); rename the file and its `slug`.\n\n## Who owns what\n\nThe next seed that differs overwrites the hub's copy of a flow's config, so\nedit flows here, not in the Admin. A flow's status (live or draft) is the\nhub's: switching a seeded flow to draft in the Admin keeps it draft through\nevery later seed. Removing a file here does not delete the hub's flow. If\nthe Admin adds a block only it can author (an email, a deliverable, an\noffer), the hub refuses the next seed of that flow (`slug_owned`) rather\nthan delete the block.\n";
53
56
  /**
54
57
  * Write `SPOKE_FLOWS_README` to `<targetDir>/flows/README.md` unless that
55
58
  * file exists. Returns whether it wrote.
@@ -49,6 +49,22 @@ export class SeedFlowsRefusedError extends Error {
49
49
  this.name = "SeedFlowsRefusedError";
50
50
  }
51
51
  }
52
+ /** A starter placeholder string: one `{…}` brace pair and nothing else. */
53
+ const PLACEHOLDER_STRING = /^\{[^{}]+\}$/;
54
+ /**
55
+ * Whether a flow config carries the starter's placeholder copy (#8850 D6):
56
+ * some string value anywhere in it is a whole `{brace}` placeholder.
57
+ */
58
+ function isPlaceholderFlow(config) {
59
+ if (typeof config === "string")
60
+ return PLACEHOLDER_STRING.test(config);
61
+ if (Array.isArray(config))
62
+ return config.some(isPlaceholderFlow);
63
+ if (config !== null && typeof config === "object") {
64
+ return Object.values(config).some(isPlaceholderFlow);
65
+ }
66
+ return false;
67
+ }
52
68
  function readDeclaredFlows(flowsDir) {
53
69
  if (!existsSync(flowsDir))
54
70
  return [];
@@ -78,7 +94,12 @@ function readDeclaredFlows(flowsDir) {
78
94
  if (slug !== expected) {
79
95
  throw new SeedFlowsRefusedError(`seed-flows: flows/${file} declares slug "${slug}"; a declared flow's file is named <slug>.json. Nothing was sent.`);
80
96
  }
81
- return { slug, file, config: config };
97
+ return {
98
+ slug,
99
+ file,
100
+ config: config,
101
+ placeholder: isPlaceholderFlow(config),
102
+ };
82
103
  });
83
104
  }
84
105
  // ── The hub origin ──────────────────────────────────────────────────────────
@@ -158,6 +179,7 @@ async function sendFlow(fetchImpl, origin, key, flow) {
158
179
  ok: true,
159
180
  result,
160
181
  status: safeCode(envelope.data?.status),
182
+ ...(flow.placeholder && { placeholder: true }),
161
183
  };
162
184
  }
163
185
  return { slug: flow.slug, ok: false, failure: "200 unknown" };
@@ -175,12 +197,15 @@ function hubLine(hub) {
175
197
  : `hub: ${hub.origin} (from ${hub.source})`;
176
198
  }
177
199
  function flowLine(outcome) {
178
- if (outcome.ok === "dry-run")
179
- return `${outcome.slug}: would send`;
180
200
  if (outcome.ok === false)
181
201
  return `${outcome.slug}: failed — ${outcome.failure}`;
202
+ const placeholder = outcome.placeholder === true
203
+ ? ` — placeholder copy: replace every {brace} in flows/${outcome.slug}.json`
204
+ : "";
205
+ if (outcome.ok === "dry-run")
206
+ return `${outcome.slug}: would send${placeholder}`;
182
207
  const draft = outcome.status === "draft" ? " (status draft — not shown to visitors)" : "";
183
- return `${outcome.slug}: ${outcome.result}${draft}`;
208
+ return `${outcome.slug}: ${outcome.result}${draft}${placeholder}`;
184
209
  }
185
210
  /** The whole report, one line per row — what `seedFlows` printed as it went. */
186
211
  export function formatSeedFlowsReport(result) {
@@ -224,6 +249,7 @@ export async function seedFlows(opts) {
224
249
  const outcomes = flows.map((flow) => ({
225
250
  slug: flow.slug,
226
251
  ok: "dry-run",
252
+ ...(flow.placeholder && { placeholder: true }),
227
253
  }));
228
254
  onLine(hubLine(hub));
229
255
  outcomes.forEach((outcome) => onLine(flowLine(outcome)));
@@ -282,6 +308,26 @@ The hub's operator must first turn seeding on (\`AILK_SPOKE_FLOW_SEED=true\`
282
308
  on the hub). Until it is on and this command has run, links to these flows
283
309
  (\`/flows/<slug>\`) reach no flow.
284
310
 
311
+ ## The starter's placeholder flows
312
+
313
+ The \`signal-collectors\` module ships two declared flows here:
314
+ \`flows/diagnostic.json\` (an email step, then two questions) and
315
+ \`flows/join-waitlist.json\` (an email and an optional name). Every visible
316
+ string in both is a \`{brace}\` placeholder: replace each one with your own
317
+ copy. Both go live on the hub with the first seed, so each line this command
318
+ prints for them says they still carry placeholder copy.
319
+
320
+ A flow is reached at \`/flows/<slug>\`. To give one its own page pair, run:
321
+
322
+ npx create-ailk add-route magnet --path /diagnostic --flow diagnostic
323
+ npx create-ailk add-route magnet --path /waitlist --flow join-waitlist
324
+
325
+ A job application comes with the \`careers\` surface. Contact, newsletter and
326
+ job application are the hub's own standard magnets: the hub seeds them in
327
+ every workspace and refuses a spoke's copy of each, so they need no file
328
+ here. If the hub's Admin already owns a magnet under one of these two slugs,
329
+ the hub refuses the seed (\`slug_owned\`); rename the file and its \`slug\`.
330
+
285
331
  ## Who owns what
286
332
 
287
333
  The next seed that differs overwrites the hub's copy of a flow's config, so