create-ailk 0.1.1 → 0.3.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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -11,11 +11,11 @@ module.exports = {
11
11
  ],
12
12
  testMatch: ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.test.tsx"],
13
13
  testPathIgnorePatterns: ["/node_modules/", "/.next/", "/dist/"],
14
- // Ignore node_modules except fumadocs-* (ESM-only packages loaded via lib/source.ts → fumadocs-core/source).
14
+ // Ignore node_modules except @working-theory/* (their dist is ESM).
15
15
  // Mirrors the canonical jose carve-out used in apps/api, apps/mcp, and apps-paid/license-issuer.
16
16
  // Added: issue #1077
17
17
  transformIgnorePatterns: [
18
- "/node_modules/(?!(\\.pnpm/)?(fumadocs-[^/]*?@?[^/]*?|@working-theory(?:\\+[^/]*?@?[^/]*?|/[^/]+))(/|$))",
18
+ "/node_modules/(?!(\\.pnpm/)?@working-theory(?:\\+[^/]*?@?[^/]*?|/[^/]+)(/|$))",
19
19
  ],
20
20
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
21
21
  // Workspace packages publish ESM in dist/, but ts-jest runs in CommonJS.
@@ -27,12 +27,6 @@ module.exports = {
27
27
  // Map all *.css to an empty stub — layout.tsx imports ./globals.css (#4090 item 1)
28
28
  // and CSS is not parseable by ts-jest's CommonJS transform (apps-paid/marketing-web precedent, #1531).
29
29
  "\\.css$": "<rootDir>/__tests__/__mocks__/style-mock.js",
30
- // Fumadocs codegen output — produced by `pnpm --filter @working-theory/web build` into .source/
31
- // The server entry uses top-level await + ESM-only fumadocs-mdx/runtime/server;
32
- // map to a CJS stub so ts-jest can load modules that import lib/source.ts. (#1077)
33
- // Stub lives in lib/__mocks__/ (committed) not .source/__mocks__/ (gitignored).
34
- "^@/\\.source/server$": "<rootDir>/lib/__mocks__/source-server.js",
35
- "^@/\\.source$": "<rootDir>/.source/index.ts",
36
30
  "^@working-theory/page-renderer/testing$":
37
31
  "<rootDir>/node_modules/@working-theory/page-renderer/dist/testing.js",
38
32
  "^@working-theory/page-renderer$":
@@ -51,8 +45,6 @@ module.exports = {
51
45
  "^@working-theory/api-client$": "<rootDir>/node_modules/@working-theory/api-client/dist/index.js",
52
46
  "^@working-theory/content-adapters$":
53
47
  "<rootDir>/node_modules/@working-theory/content-adapters/dist/index.js",
54
- "^@working-theory/fumadocs-schema$":
55
- "<rootDir>/node_modules/@working-theory/fumadocs-schema/dist/index.js",
56
48
  "^@working-theory/license$": "<rootDir>/node_modules/@working-theory/license/dist/index.js",
57
49
  "^@working-theory/jsonld$": "<rootDir>/node_modules/@working-theory/jsonld/dist/index.js",
58
50
  "^@working-theory/metadata$": "<rootDir>/node_modules/@working-theory/metadata/dist/index.js",
@@ -66,6 +58,7 @@ module.exports = {
66
58
  "<rootDir>/node_modules/@working-theory/schema-validator/dist/index.js",
67
59
  "^@working-theory/sections-registry$":
68
60
  "<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
61
+
69
62
  "^@working-theory/templates/shells$":
70
63
  "<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
71
64
  "^@working-theory/templates$": "<rootDir>/node_modules/@working-theory/templates/dist/index.js",
@@ -82,7 +75,21 @@ module.exports = {
82
75
  // tree, where nothing imports it, so the unused mapping is inert there.
83
76
  "^@working-theory/ui/slices/scheduling$":
84
77
  "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/scheduling.js",
78
+ // #5366: the signal-collectors slice's re-export subpath. Same reasoning
79
+ // as the scheduling mapping above.
80
+ "^@working-theory/ui/slices/signal-collectors$":
81
+ "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/signal-collectors.js",
82
+ // #5364: the commerce slice's renderer-registration + CheckoutPriceCard
83
+ // re-export subpath. Same reasoning as the scheduling mapping above.
84
+ "^@working-theory/ui/slices/commerce$":
85
+ "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/commerce.js",
85
86
  "^@working-theory/theme$": "<rootDir>/node_modules/@working-theory/theme/dist/index.js",
87
+ // site-config publishes ESM in dist/; map to source so ts-jest compiles it
88
+ // through the same transformer as every sibling above. Only the
89
+ // client-safe /schema entry is mapped — architecture.yaml forbids apps/web
90
+ // from importing /server at all (#1567).
91
+ "^@working-theory/site-config/schema$":
92
+ "<rootDir>/node_modules/@working-theory/site-config/dist/schema/index.js",
86
93
  "^@working-theory/validation$": "<rootDir>/node_modules/@working-theory/validation/dist/index.js",
87
94
  "^@working-theory/web-i18n$": "<rootDir>/node_modules/@working-theory/web-i18n/dist/index.js",
88
95
  // #5025: pnpm keys next-intl's virtual-store path on its resolved peers,
@@ -120,9 +127,9 @@ module.exports = {
120
127
  },
121
128
  setupFilesAfterEnv: ["<rootDir>/__tests__/setup.ts"],
122
129
  transform: {
123
- // fumadocs-core (16.10.6+) ships `import.meta.env` in its dist JS
124
- // (e.g. dist/url-*.js), which the transformIgnorePatterns carve-out
125
- // above lets reach a transformer instead of being required raw.
130
+ // Some `@working-theory/*` dist JS ships `import.meta`, which the
131
+ // transformIgnorePatterns carve-out above lets reach a transformer
132
+ // instead of being required raw.
126
133
  // `import.meta` has no CJS form, so it must run through the shared
127
134
  // import.meta -> {} rewrite BEFORE ts-jest — ts-jest's own
128
135
  // astTransformers hook never fires for a node_modules JS file (see the
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Round trip: the real `content/_site.mdx` → the emitted CSS custom properties.
3
+ *
4
+ * This is the end-to-end claim the wiring makes — a site declares a theme and
5
+ * the built page renders in those colours — asserted on the actual repo file
6
+ * rather than a fixture, so deleting the `theme:` block would fail here.
7
+ */
8
+ import { mkdtempSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ import { PartialColorPaletteSchema } from "@working-theory/site-config/schema";
13
+ import { siteColorTokenMap, themeToCssVarsBothModes } from "@working-theory/theme";
14
+
15
+ import { loadSiteBrandTokens } from "../site-theme";
16
+
17
+ const REPO_CONTENT_ROOT = join(__dirname, "..", "..", "..", "..", "content");
18
+
19
+ /** The light-mode `:root { … }` block, without the dark one. */
20
+ function lightBlock(css: string): string {
21
+ return css.slice(0, css.indexOf(':root[data-theme="dark"]'));
22
+ }
23
+
24
+ /** The dark-mode `:root[data-theme="dark"] { … }` block. */
25
+ function darkBlock(css: string): string {
26
+ return css.slice(css.indexOf(':root[data-theme="dark"]'));
27
+ }
28
+
29
+ describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
30
+ const originalRoot = process.env.AILK_CONTENT_ROOT;
31
+
32
+ beforeEach(() => {
33
+ process.env.AILK_CONTENT_ROOT = REPO_CONTENT_ROOT;
34
+ });
35
+
36
+ afterEach(() => {
37
+ if (originalRoot === undefined) delete process.env.AILK_CONTENT_ROOT;
38
+ else process.env.AILK_CONTENT_ROOT = originalRoot;
39
+ });
40
+
41
+ it("translates the declared theme into the token overlay", () => {
42
+ expect(loadSiteBrandTokens()).toEqual({
43
+ colors: {
44
+ light: { bg: "#fdfcfa", fg: "#1c1917" },
45
+ dark: { bg: "#0c0a09" },
46
+ },
47
+ });
48
+ });
49
+
50
+ it("reaches the emitted CSS custom properties", () => {
51
+ const brand = loadSiteBrandTokens();
52
+ const css = themeToCssVarsBothModes(brand ? { brand } : {});
53
+
54
+ expect(lightBlock(css)).toContain("--color-bg: #fdfcfa;");
55
+ expect(lightBlock(css)).toContain("--color-fg: #1c1917;");
56
+ expect(darkBlock(css)).toContain("--color-bg: #0c0a09;");
57
+ });
58
+
59
+ it("changes exactly the declared variables and nothing else", () => {
60
+ const brand = loadSiteBrandTokens();
61
+ const themed = themeToCssVarsBothModes(brand ? { brand } : {});
62
+ const expected = themeToCssVarsBothModes()
63
+ // light block — the two declared light colours
64
+ .replace("--color-bg: #ffffff;", "--color-bg: #fdfcfa;")
65
+ .replace("--color-fg: #030712;", "--color-fg: #1c1917;")
66
+ // dark block — the one declared dark colour
67
+ .replace("--color-bg: #0a0a0a;", "--color-bg: #0c0a09;");
68
+
69
+ expect(themed).toBe(expected);
70
+ });
71
+
72
+ it("treats a bare `theme:` key as no theme, rather than throwing", () => {
73
+ // YAML parses a dangling `theme:` to null. It is what commenting out the
74
+ // block's children leaves behind, it means the same as an absent key, and
75
+ // it must not escape as a ZodError out of an async Server Component.
76
+ const root = mkdtempSync(join(tmpdir(), "ailk-site-theme-"));
77
+ writeFileSync(
78
+ join(root, "_site.mdx"),
79
+ ["---", "siteName: Test", "theme:", "---", ""].join("\n"),
80
+ "utf8",
81
+ );
82
+ process.env.AILK_CONTENT_ROOT = root;
83
+
84
+ expect(loadSiteBrandTokens()).toBeUndefined();
85
+ });
86
+
87
+ it("returns undefined — and so emits the default CSS — when no config is readable", () => {
88
+ process.env.AILK_CONTENT_ROOT = join(__dirname, "no-such-content-root");
89
+
90
+ const brand = loadSiteBrandTokens();
91
+ expect(brand).toBeUndefined();
92
+ expect(themeToCssVarsBothModes(brand ? { brand } : {})).toBe(
93
+ themeToCssVarsBothModes(),
94
+ );
95
+ });
96
+ });
97
+
98
+ /**
99
+ * The twelve customer-facing colour slots live in `@working-theory/site-config`
100
+ * and the map from them to token names lives in `@working-theory/theme`;
101
+ * neither package may import the other, so nothing inside either can notice
102
+ * the two lists drifting. `apps/web` imports both, so the cross-check lives
103
+ * here — without it, a slot added to the palette schema would parse cleanly
104
+ * and then throw "is not a theme colour" at render.
105
+ */
106
+ describe("the two vocabularies agree on which slots exist", () => {
107
+ it("siteColorTokenMap covers exactly the schema's colour slots", () => {
108
+ expect(Object.keys(siteColorTokenMap).sort()).toEqual(
109
+ Object.keys(PartialColorPaletteSchema.shape).sort(),
110
+ );
111
+ });
112
+ });
@@ -55,8 +55,11 @@ interface SiteFrontmatter {
55
55
  * levels up from cwd — `apps/web` in dev/build, so `../../content` lands on
56
56
  * the site/scaffold root's `content/` directory. Duplicated locally because
57
57
  * that resolver isn't part of `@working-theory/page-renderer`'s public API.
58
+ *
59
+ * Exported so `lib/site-theme.ts` resolves the same root rather than keeping
60
+ * a third copy of this four-line rule.
58
61
  */
59
- function resolveContentRoot(): string {
62
+ export function resolveContentRoot(): string {
60
63
  const override = process.env.AILK_CONTENT_ROOT;
61
64
  if (override && override.length > 0) return override;
62
65
  return join(process.cwd(), "..", "..", "content");
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Site theme — reads the `theme:` block out of `content/_site.mdx` and
3
+ * translates it into the token overlay `themeToCssVarsBothModes` accepts.
4
+ *
5
+ * The read is a local frontmatter parse rather than
6
+ * `@working-theory/site-config/server`'s `loadSiteConfig`, for the same
7
+ * reason `site-brand.tsx` reads locally (see its header, and #4157 D1) and
8
+ * one more: `architecture.yaml` forbids `apps/web` from importing that
9
+ * subpath at all, because it pulls `node:fs` into a tree that also carries
10
+ * edge routes (#1567). The `theme:` block is still validated against the
11
+ * canonical schema — `SiteThemeSchema` from the package's client-safe
12
+ * `/schema` entry — so the shape this app accepts and the shape the
13
+ * dashboard will write are one definition, not two.
14
+ *
15
+ * The translation itself lives in `@working-theory/theme`
16
+ * (`siteThemeToBrandTokens`) — the token vocabulary is that package's own.
17
+ *
18
+ * Returns `undefined` when the site declares no theme, so the layout passes
19
+ * no `brand` at all and the resolver emits byte-for-byte the CSS it emitted
20
+ * before this wiring existed.
21
+ */
22
+
23
+ import { readFileSync } from "node:fs";
24
+ import { join } from "node:path";
25
+
26
+ import { SiteThemeSchema } from "@working-theory/site-config/schema";
27
+ import {
28
+ siteThemeToBrandTokens,
29
+ type BrandTokenOverrides,
30
+ } from "@working-theory/theme";
31
+ import { parse as parseYaml } from "yaml";
32
+
33
+ import { resolveContentRoot } from "./site-brand";
34
+
35
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
36
+
37
+ const SOURCE = "content/_site.mdx theme";
38
+
39
+ /**
40
+ * The `theme:` value as it appears in the frontmatter, or `undefined` when
41
+ * `_site.mdx` is absent, unreadable, or declares no theme. A site with no
42
+ * readable config has no theme to apply, and the default palette is the
43
+ * correct result — the same fallback `site-brand.tsx` takes.
44
+ *
45
+ * "Declares no theme" covers a bare `theme:` key with nothing under it, which
46
+ * YAML parses to `null`. That is the shape the most natural edit produces —
47
+ * commenting out the block's children leaves the key dangling — and it means
48
+ * exactly what an absent key means, so it takes the same path rather than
49
+ * reaching the schema and throwing out of a Server Component.
50
+ */
51
+ function readThemeBlock(): unknown {
52
+ try {
53
+ const raw = readFileSync(join(resolveContentRoot(), "_site.mdx"), "utf8");
54
+ const match = FRONTMATTER_RE.exec(raw);
55
+ if (!match) return undefined;
56
+
57
+ const parsed: unknown = parseYaml(match[1] ?? "");
58
+ if (!parsed || typeof parsed !== "object") return undefined;
59
+
60
+ return (parsed as Record<string, unknown>).theme ?? undefined;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ export function loadSiteBrandTokens(): BrandTokenOverrides | undefined {
67
+ const block = readThemeBlock();
68
+ if (block === undefined) return undefined;
69
+
70
+ // Validation failures and unsupported colours BOTH throw, deliberately: a
71
+ // customer who set a colour and saw no change would have been lied to.
72
+ const theme = SiteThemeSchema.parse(block);
73
+ return siteThemeToBrandTokens(theme, SOURCE);
74
+ }
@@ -3,7 +3,6 @@ import { join } from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import bundleAnalyzer from "@next/bundle-analyzer";
5
5
  import { withSentryConfig } from "@sentry/nextjs";
6
- import { createMDX } from "fumadocs-mdx/next";
7
6
  import createNextIntlPlugin from "next-intl/plugin";
8
7
 
9
8
  // @next/bundle-analyzer — enabled when ANALYZE=true (set by ci-bundle.yml).
@@ -110,7 +109,6 @@ const nextConfig = {
110
109
  "@working-theory/analytics",
111
110
  "@working-theory/api-client",
112
111
  "@working-theory/content-adapters",
113
- "@working-theory/fumadocs-schema",
114
112
  "@working-theory/jsonld",
115
113
  "@working-theory/metadata",
116
114
  "@working-theory/page-renderer",
@@ -168,6 +166,6 @@ const sentryBuildOptions = {
168
166
  };
169
167
 
170
168
  export default withSentryConfig(
171
- withBundleAnalyzer(withNextIntl(createMDX()(nextConfig))),
169
+ withBundleAnalyzer(withNextIntl(nextConfig)),
172
170
  sentryBuildOptions,
173
171
  );
@@ -6,14 +6,11 @@
6
6
  "license": "Apache-2.0",
7
7
  "scripts": {
8
8
  "dev": "next dev",
9
- "prebuild": "fumadocs-mdx",
10
9
  "build": "next build --webpack",
11
10
  "postbuild": "tsx scripts/assert-content-bundled.ts",
12
11
  "start": "next start",
13
- "pretypecheck": "fumadocs-mdx",
14
12
  "type-check": "tsc --noEmit",
15
13
  "lint": "next lint",
16
- "pretest": "fumadocs-mdx",
17
14
  "test": "jest",
18
15
  "test:watch": "jest --watch"
19
16
  },
@@ -24,21 +21,19 @@
24
21
  "@working-theory/api-client": "workspace:*",
25
22
  "@working-theory/content-adapters": "workspace:*",
26
23
  "@working-theory/database": "workspace:*",
27
- "@working-theory/fumadocs-schema": "workspace:*",
28
24
  "@working-theory/jsonld": "workspace:*",
29
25
  "@working-theory/metadata": "workspace:*",
30
26
  "@working-theory/page-renderer": "workspace:*",
31
27
  "@working-theory/schema": "workspace:*",
32
28
  "@working-theory/schema-pack-registry": "workspace:*",
33
29
  "@working-theory/schema-validator": "workspace:*",
30
+ "@working-theory/site-config": "workspace:*",
34
31
  "@working-theory/templates": "workspace:*",
35
32
  "@working-theory/theme": "workspace:*",
36
33
  "@working-theory/ui": "workspace:*",
37
34
  "@working-theory/validation": "workspace:*",
38
35
  "@working-theory/web-i18n": "workspace:*",
39
36
  "better-auth": "1.6.22",
40
- "fumadocs-core": "^16.10.4",
41
- "fumadocs-mdx": "^15.0.12",
42
37
  "lucide-react": "^0.577.0",
43
38
  "next": "^16.3.3",
44
39
  "next-intl": "^3.26.3",
@@ -1,4 +1,7 @@
1
- <svg width="1080" height="1080" viewBox="0 0 1080 1080" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect width="1080" height="1080" rx="190" fill="#F0B100"/>
3
- <path d="M127.624 704.255C127.624 704.255 303.232 444.079 606.336 428.823C606.336 428.823 827.464 405.543 824.36 645.295C824.36 645.295 824.344 832.999 659.872 916.079C507.8 992.887 380.56 892.023 383.664 757.023C386.552 631.327 467.136 458.695 824.392 199.167L792.552 155.463L952.384 137.359L893.416 288.399L861.864 246.503C861.864 246.503 529.792 463.231 475.216 676.335C475.216 676.335 432.544 848.583 558.992 852.527C689.056 856.591 794.88 637.543 696.856 535.383C613.632 448.639 369.888 498.079 163.504 733.167L127.616 704.263L127.624 704.255Z" fill="white"/>
1
+ <svg width="1520" height="1520" viewBox="0 0 1520 1520" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="AI Launch Kit loop logomark">
2
+ <title>AI Launch Kit favicon (the loop logomark on the #0656E9 tile)</title>
3
+ <rect x="0" y="0" width="1520" height="1520" rx="267" fill="#0656E9"/>
4
+ <svg x="258.4" y="270.1" width="1003.2" height="979.9" viewBox="127.6 137.2 824.8 805.6" fill="#FFFFFF">
5
+ <path d="M127.624 704.255C127.624 704.255 303.232 444.079 606.336 428.823C606.336 428.823 827.464 405.543 824.36 645.295C824.36 645.295 824.344 832.999 659.872 916.079C507.8 992.887 380.56 892.023 383.664 757.023C386.552 631.327 467.136 458.695 824.392 199.167L792.552 155.463L952.384 137.359L893.416 288.399L861.864 246.503C861.864 246.503 529.792 463.231 475.216 676.335C475.216 676.335 432.544 848.583 558.992 852.527C689.056 856.591 794.88 637.543 696.856 535.383C613.632 448.639 369.888 498.079 163.504 733.167L127.616 704.263L127.624 704.255Z" fill="#FFFFFF"/>
6
+ </svg>
4
7
  </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-196.50 -128.00 1248.00 143.50" width="1248" height="144">
2
+ <!-- AI Launch Kit — horizontal lockup from the portfolio logo builder (mark: loop; wordmark: custom lettering). -->
3
+ <g transform="scale(100)" fill="#2F2E2F"><g transform="translate(-1.9050 -1.2206) scale(0.0016343825665859565) translate(-127.0 -137.0)"><path d="M127.624 704.255C127.624 704.255 303.232 444.079 606.336 428.823C606.336 428.823 827.464 405.543 824.36 645.295C824.36 645.295 824.344 832.999 659.872 916.079C507.8 992.887 380.56 892.023 383.664 757.023C386.552 631.327 467.136 458.695 824.392 199.167L792.552 155.463L952.384 137.359L893.416 288.399L861.864 246.503C861.864 246.503 529.792 463.231 475.216 676.335C475.216 676.335 432.544 848.583 558.992 852.527C689.056 856.591 794.88 637.543 696.856 535.383C613.632 448.639 369.888 498.079 163.504 733.167L127.616 704.263L127.624 704.255Z" fill="#2F2E2F"/></g><g><g transform="translate(0.0000 0) scale(1.12)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -4.7020750267431675 -1.4599045865638782)"><path d="M 2017.5 402.472656 L 2069.75 282.953125 L 2117.78125 403.007812 Z M 2058.75 183.148438 L 1880.675781 584.136719 L 1942.238281 584.136719 L 2008.234375 422.691406 L 2125.605469 423.199219 L 2190.234375 584.136719 L 2271.277344 584.136719 L 2099.449219 183.226562 Z M 2058.75 183.148438 "/></g></g><g transform="translate(1.2171 0) scale(1.12)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -7.51705209867612 -1.835802888450144)"><path d="M1740.19 194.304L1719.36 191.411V202.211V419.908H1760.75V202.211V191.411L1740.19 194.304Z"/></g></g><g transform="translate(2.0699 0) scale(1.12)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -5.830939525076691 -1.4599045865638782)"><path d="M 2402.425781 183.285156 C 2396.121094 184.144531 2381.960938 186.203125 2370.179688 188.808594 L 2369.75 188.917969 L 2369.320312 188.871094 C 2368.386719 188.792969 2347.890625 187.152344 2332.242188 182.320312 L 2332.367188 584.121094 L 2560.800781 582.847656 L 2560.800781 553.59375 L 2403.714844 559.222656 Z M 2402.425781 183.285156 "/></g></g><g transform="translate(2.7492 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -4.7020750267431675 -1.4599045865638782)"><path d="M 2017.5 402.472656 L 2069.75 282.953125 L 2117.78125 403.007812 Z M 2058.75 183.148438 L 1880.675781 584.136719 L 1942.238281 584.136719 L 2008.234375 422.691406 L 2125.605469 423.199219 L 2190.234375 584.136719 L 2271.277344 584.136719 L 2099.449219 183.226562 Z M 2058.75 183.148438 "/></g></g><g transform="translate(3.8551 0) scale(1)"><g transform="matrix(0.008439053643785967 0 0 0.008439053643785967 -4.32856421446149 -1.661401981511421)"><path d="M588.43 79.8509L582.5 78.3809H582.4V152.431C582.4 170.651 578.49 184.301 560.57 185.691C535.83 187.611 534.33 168.701 534.33 152.431V78.3809H512.93V154.271C512.93 180.851 526.97 196.901 553.55 196.901C580.13 196.901 594.34 180.851 594.34 154.271V78.3809H594.29L588.43 79.8509Z"/></g></g><g transform="translate(4.7219 0) scale(1)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -8.030530574367127 -1.835802888450144)"><path d="M2004.42 193.996L1983.67 191.122V201.903V362.052H1981.67L1873.96 191.064H1836.85V419.909H1878.31V249.847H1880.2L1988.03 419.909H2024.9V201.903V191.122L2004.42 193.996Z"/></g></g><g transform="translate(5.7438 0) scale(1)"><g transform="matrix(0.008439053643785967 0 0 0.008439053643785967 -6.325890417670046 -1.661401981511421)"><path d="M836.41 164.341C835.23 168.751 829.47 176.661 826.1 179.451C821.03 183.641 815.16 185.671 808.28 185.381C799.06 185.021 790.21 178.941 786.62 175.401C774.31 163.261 763.42 126.371 785.6 101.151C790.38 95.7213 799.06 90.4313 808.28 90.0713C815.16 89.7813 821.04 91.8113 826.1 96.0013C829.47 98.8013 835.23 106.701 836.41 111.111L850.58 110.991L856.37 95.5913C854.69 93.3213 842.2 80.1513 813.29 78.3613H811.99C806.12 78.3613 800.73 78.8913 795.85 79.7713C774.34 83.6413 757.22 100.051 752.05 121.281C750.58 127.301 749.55 133.771 749.57 140.021C749.57 141.261 749.62 142.501 749.71 143.731C751.72 171.321 773.66 193.211 801.13 196.451C802.11 196.571 803.52 196.671 805.04 196.751C814.56 197.281 824.14 196.051 833.09 192.741C847.38 187.471 854.38 179.421 855.59 177.781L850.57 164.441L836.4 164.321L836.41 164.341Z"/></g></g><g transform="translate(6.8247 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -7.397806839178519 -1.4599045865638782)"><path d="M 3259.496094 184.34375 L 3259.21875 356.253906 L 3141.863281 363.183594 L 3028.496094 355.238281 L 3028.773438 185.34375 L 2994.425781 190.695312 L 2959.082031 185.328125 L 2959.082031 584.074219 L 2991.449219 583.722656 L 3028.789062 584.074219 L 3029.003906 381.808594 L 3142.355469 380.472656 L 3258.726562 381.808594 L 3259.496094 583.109375 L 3293.84375 582.753906 L 3329.21875 583.109375 L 3329.21875 184.359375 L 3294.855469 189.714844 Z M 3259.496094 184.34375 "/></g></g><g transform="translate(8.3998 0) scale(1.12)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -6.531551848288504 -1.835802888450144)"><path d="M1582.59 274.514L1651.05 192.742H1636.78L1536.82 312.159V192.761L1515.7 195.828L1493.99 192.742V419.909H1536.82V329.69L1553.7 309.247L1614.7 419.909H1664.03L1582.59 274.514Z"/></g></g><g transform="translate(9.3625 0) scale(1)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -7.51705209867612 -1.835802888450144)"><path d="M1740.19 194.304L1719.36 191.411V202.211V419.908H1760.75V202.211V191.411L1740.19 194.304Z"/></g></g><g transform="translate(9.6336 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -6.402239906111586 -1.4599045865638782)"><path d="M 2691.488281 583.628906 L 2758.25 583.628906 L 2758.25 204.410156 L 2888.722656 218.15625 L 2888.832031 183.714844 L 2560.910156 183.714844 L 2560.800781 217.679688 L 2691.488281 204.425781 Z M 2691.488281 583.628906 "/></g></g></g></g>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <!-- loop logomark in the portfolio square frame (fills 90% of the side) -->
3
+ <g transform="translate(5.000 6.090) scale(0.108959) translate(-127.0 -137.0)" fill="#2F2E2F"><path d="M127.624 704.255C127.624 704.255 303.232 444.079 606.336 428.823C606.336 428.823 827.464 405.543 824.36 645.295C824.36 645.295 824.344 832.999 659.872 916.079C507.8 992.887 380.56 892.023 383.664 757.023C386.552 631.327 467.136 458.695 824.392 199.167L792.552 155.463L952.384 137.359L893.416 288.399L861.864 246.503C861.864 246.503 529.792 463.231 475.216 676.335C475.216 676.335 432.544 848.583 558.992 852.527C689.056 856.591 794.88 637.543 696.856 535.383C613.632 448.639 369.888 498.079 163.504 733.167L127.616 704.263L127.624 704.255Z" fill="#2F2E2F"/></g>
4
+ </svg>
@@ -13,7 +13,7 @@
13
13
  "type": "image/png"
14
14
  }
15
15
  ],
16
- "theme_color": "#F0B100",
17
- "background_color": "#ffffff",
16
+ "theme_color": "#0656E9",
17
+ "background_color": "#0656E9",
18
18
  "display": "standalone"
19
19
  }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -118.50 1057.50 124.50" width="1058" height="125">
2
+ <!-- AI Launch Kit — wordmark from the portfolio logo builder (custom lettering). -->
3
+ <g transform="scale(100)" fill="#2F2E2F"><g><g transform="translate(0.0000 0) scale(1.12)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -4.7020750267431675 -1.4599045865638782)"><path d="M 2017.5 402.472656 L 2069.75 282.953125 L 2117.78125 403.007812 Z M 2058.75 183.148438 L 1880.675781 584.136719 L 1942.238281 584.136719 L 2008.234375 422.691406 L 2125.605469 423.199219 L 2190.234375 584.136719 L 2271.277344 584.136719 L 2099.449219 183.226562 Z M 2058.75 183.148438 "/></g></g><g transform="translate(1.2171 0) scale(1.12)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -7.51705209867612 -1.835802888450144)"><path d="M1740.19 194.304L1719.36 191.411V202.211V419.908H1760.75V202.211V191.411L1740.19 194.304Z"/></g></g><g transform="translate(2.0699 0) scale(1.12)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -5.830939525076691 -1.4599045865638782)"><path d="M 2402.425781 183.285156 C 2396.121094 184.144531 2381.960938 186.203125 2370.179688 188.808594 L 2369.75 188.917969 L 2369.320312 188.871094 C 2368.386719 188.792969 2347.890625 187.152344 2332.242188 182.320312 L 2332.367188 584.121094 L 2560.800781 582.847656 L 2560.800781 553.59375 L 2403.714844 559.222656 Z M 2402.425781 183.285156 "/></g></g><g transform="translate(2.7492 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -4.7020750267431675 -1.4599045865638782)"><path d="M 2017.5 402.472656 L 2069.75 282.953125 L 2117.78125 403.007812 Z M 2058.75 183.148438 L 1880.675781 584.136719 L 1942.238281 584.136719 L 2008.234375 422.691406 L 2125.605469 423.199219 L 2190.234375 584.136719 L 2271.277344 584.136719 L 2099.449219 183.226562 Z M 2058.75 183.148438 "/></g></g><g transform="translate(3.8551 0) scale(1)"><g transform="matrix(0.008439053643785967 0 0 0.008439053643785967 -4.32856421446149 -1.661401981511421)"><path d="M588.43 79.8509L582.5 78.3809H582.4V152.431C582.4 170.651 578.49 184.301 560.57 185.691C535.83 187.611 534.33 168.701 534.33 152.431V78.3809H512.93V154.271C512.93 180.851 526.97 196.901 553.55 196.901C580.13 196.901 594.34 180.851 594.34 154.271V78.3809H594.29L588.43 79.8509Z"/></g></g><g transform="translate(4.7219 0) scale(1)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -8.030530574367127 -1.835802888450144)"><path d="M2004.42 193.996L1983.67 191.122V201.903V362.052H1981.67L1873.96 191.064H1836.85V419.909H1878.31V249.847H1880.2L1988.03 419.909H2024.9V201.903V191.122L2004.42 193.996Z"/></g></g><g transform="translate(5.7438 0) scale(1)"><g transform="matrix(0.008439053643785967 0 0 0.008439053643785967 -6.325890417670046 -1.661401981511421)"><path d="M836.41 164.341C835.23 168.751 829.47 176.661 826.1 179.451C821.03 183.641 815.16 185.671 808.28 185.381C799.06 185.021 790.21 178.941 786.62 175.401C774.31 163.261 763.42 126.371 785.6 101.151C790.38 95.7213 799.06 90.4313 808.28 90.0713C815.16 89.7813 821.04 91.8113 826.1 96.0013C829.47 98.8013 835.23 106.701 836.41 111.111L850.58 110.991L856.37 95.5913C854.69 93.3213 842.2 80.1513 813.29 78.3613H811.99C806.12 78.3613 800.73 78.8913 795.85 79.7713C774.34 83.6413 757.22 100.051 752.05 121.281C750.58 127.301 749.55 133.771 749.57 140.021C749.57 141.261 749.62 142.501 749.71 143.731C751.72 171.321 773.66 193.211 801.13 196.451C802.11 196.571 803.52 196.671 805.04 196.751C814.56 197.281 824.14 196.051 833.09 192.741C847.38 187.471 854.38 179.421 855.59 177.781L850.57 164.441L836.4 164.321L836.41 164.341Z"/></g></g><g transform="translate(6.8247 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -7.397806839178519 -1.4599045865638782)"><path d="M 3259.496094 184.34375 L 3259.21875 356.253906 L 3141.863281 363.183594 L 3028.496094 355.238281 L 3028.773438 185.34375 L 2994.425781 190.695312 L 2959.082031 185.328125 L 2959.082031 584.074219 L 2991.449219 583.722656 L 3028.789062 584.074219 L 3029.003906 381.808594 L 3142.355469 380.472656 L 3258.726562 381.808594 L 3259.496094 583.109375 L 3293.84375 582.753906 L 3329.21875 583.109375 L 3329.21875 184.359375 L 3294.855469 189.714844 Z M 3259.496094 184.34375 "/></g></g><g transform="translate(8.3998 0) scale(1.12)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -6.531551848288504 -1.835802888450144)"><path d="M1582.59 274.514L1651.05 192.742H1636.78L1536.82 312.159V192.761L1515.7 195.828L1493.99 192.742V419.909H1536.82V329.69L1553.7 309.247L1614.7 419.909H1664.03L1582.59 274.514Z"/></g></g><g transform="translate(9.3625 0) scale(1)"><g transform="matrix(0.00437188995905497 0 0 0.00437188995905497 -7.51705209867612 -1.835802888450144)"><path d="M1740.19 194.304L1719.36 191.411V202.211V419.908H1760.75V202.211V191.411L1740.19 194.304Z"/></g></g><g transform="translate(9.6336 0) scale(1)"><g transform="matrix(0.002500025961207091 0 0 0.002500025961207091 -6.402239906111586 -1.4599045865638782)"><path d="M 2691.488281 583.628906 L 2758.25 583.628906 L 2758.25 204.410156 L 2888.722656 218.15625 L 2888.832031 183.714844 L 2560.910156 183.714844 L 2560.800781 217.679688 L 2691.488281 204.425781 Z M 2691.488281 583.628906 "/></g></g></g></g>
4
+ </svg>
@@ -14,6 +14,13 @@ siteIdentity:
14
14
  sameAs: []
15
15
  analytics:
16
16
  vercelAnalytics: false
17
+ theme:
18
+ colors:
19
+ light:
20
+ bg: "#fdfcfa"
21
+ text: "#1c1917"
22
+ dark:
23
+ bg: "#0c0a09"
17
24
  ---
18
25
 
19
26
  <!--
@@ -31,6 +38,11 @@ your LinkedIn company page, your GitHub org, your X profile. Answer engines use
31
38
  it to reconcile your site with the rest of your presence, so it is worth
32
39
  filling in.
33
40
 
41
+ `theme.colors` is the site's colour overlay: each key names a customer-facing
42
+ colour role (`bg`, `text`, `primary`, `accent`, …) per light/dark mode, and the
43
+ build turns it into the CSS custom properties every component reads. Declare
44
+ only what you change — an absent role keeps the platform default.
45
+
34
46
  For the agent-operator workflow, use the `set_site_identity` MCP tool rather
35
47
  than editing this file directly — it validates input and writes with optimistic
36
48
  locking.
@@ -1,5 +1,99 @@
1
1
  # @working-theory/database
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#5412](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/5412) [`e94f08c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/e94f08cccfe6cb9d096b6746a092c1b6921677cf) Thanks [@claudio-planck](https://github.com/claudio-planck)! - The founder application collects first name, last name and phone on step one, and records email verification when the emailed resume link is opened ([#5407](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5407)).
8
+
9
+ **Migration: `20260914120000_listing_founder_identity`.** Five nullable columns on `project_listings` — `founder_first_name`, `founder_last_name`, `founder_phone`, `email_verify_nonce_hash` and `email_verified_at` — with no backfill and no change to any existing column. `founder_name` stays NOT NULL and stays the single-line DISPLAY name every existing reader reads; the parts sit beside it, exactly as [#5279](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5279) put the six address parts beside `address`. Composition runs one way only, so no existing row's `founder_name` is split by guessing.
10
+
11
+ The platform's default contact screen now asks first name, last name, email, phone and consent. Phone is required by default and a site relaxes it with `questions: [{ id: "founder_phone", required: false }]`. A site that still asks for one "Your name" line keeps parsing untouched: the required `founderName` contract row is satisfied either by a question writing it or by the two parts that compose it.
12
+
13
+ `POST /v1/project-listings` accepts `founderFirstName` / `founderLastName` / `founderPhone` and composes `founder_name` server-side; a body carrying only `founderName`, as every caller before this release sends, is accepted unchanged.
14
+
15
+ New anonymous, credential-gated route `POST /v1/project-listings/drafts/:id/verify-email`, reached from `projectListings.verifyEmail(id, resumeToken, verifyNonce)`. It takes two credentials, and only the second carries the proof: the resume token is in `start`'s own 201 body, so it says only that you opened the draft, while the verification NONCE is minted at `start`, stored as a digest, and handed to nothing but the resume-email builder — it reaches the outside world only as the `?verify=` parameter of the emailed link. The route stamps `email_verified_at` write-once by compare-and-swap, so repeat openings answer the first stamp; a wrong token, a wrong nonce and an unknown draft all return the same `404 not_found` the other token-gated routes return.
16
+
17
+ `ListingApplicationFlow` calls it once when it mounts with a `resume.verifyNonce`, and a failure never blocks the founder. A resume link with no nonce still resumes the draft and records nothing — which is also what a site that has not switched the resume email on gets, permanently, since no nonce is ever delivered there.
18
+
19
+ `start`'s request and response shapes are unchanged: the nonce is never in a response body and never in a log line.
20
+
21
+ `founder_phone` and `email_verified_at` are omitted from the public listing read. The operator sees both on the tenant dashboard row and in the decision/review payload, and the weekly CSV sweep's outcome log now says whether the address it mailed had been proved.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [[`941db5d`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/941db5d317b7332c3c5bb4134d0cc18f80678781), [`cbe99c2`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/cbe99c228200b4720b2f36af50c822a4bcca2275), [`e94f08c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/e94f08cccfe6cb9d096b6746a092c1b6921677cf), [`f42b96b`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/f42b96b7489b5dae503b6c32e2045a14f382c583), [`4d28c33`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/4d28c33978dde5cd33540813ec6317436a42a877)]:
26
+ - @working-theory/ids@0.7.0
27
+ - @working-theory/validation@0.22.0
28
+
29
+ ## 0.21.0
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies []:
34
+ - @working-theory/validation@0.21.0
35
+
36
+ ## 0.20.0
37
+
38
+ ### Minor Changes
39
+
40
+ - [#5282](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/5282) [`49e6ba4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/49e6ba470a0ff4a0e6d4f7d14147232e90a12259) Thanks [@claudio-planck](https://github.com/claudio-planck)! - Consent purposes and grants ([#5275](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5275)): a site declares its consent purposes once, any form renders them as real checkboxes with inline policy links, and every answer produces a durable record carrying the exact wording the person read.
41
+
42
+ `@working-theory/validation` gains `consent-config.ts` — the per-site consent registry contract (`siteConsentConfigSchema`, `SITE_CONSENT_CONFIG_DEFAULTS`, `consentConfigFor`, `resolveConsentStatement`), following `SiteListingConfig`: a Zod-validated blob, platform defaults, and an unparseable blob that resolves to those defaults rather than to a failed form. A statement's links are markdown-shaped with a KEY target (`[Privacy Policy](privacy)`) resolved against the site's policy map, so a config string can never carry a URL or a scheme. `fieldInputTypeSchema` gains the `consent` kind, carrying `purposes: string[]` (ids only; `options`/`minLength` are refused on it). `projectListingStartSchema` keeps `consent: z.literal(true)` and gains an optional `consents: Record<purposeId, boolean>` and `locale`, so every existing caller (agent submission included) still parses. The listing application's contact step compiles its consent control as the `consent` kind (an optional `consentPurposes` list, defaulting to the platform's `service_terms`) instead of a `select` carrying one fake option.
43
+
44
+ `@working-theory/ui` gains two blocks: `ConsentStatement` (parses `[Label](key)` tokens; an unresolvable key renders as plain text, never a dead link; no code path renders an href taken from the statement string) and `ConsentGroup` (one `Checkbox` per purpose, every box unticked on first render, each named and described by its statement via `aria-labelledby`/`aria-describedby`, posting under a flat `<field>.<purposeId>` name). `ContactFormFields` accepts `type: "consent"`; `FlowStepper` gates advance on required purposes only and sends one `"true"`/`"false"` per purpose (a decline is a value, not an absent key); both take an optional `consentConfig` that defaults to the platform registry. `ListingApplicationFlow` now sends the founder's real grants (`consents` + `locale`) instead of a hardcoded `consent: true`.
45
+
46
+ `@working-theory/database` gains the append-only `ConsentGrant` model (`consent_grants`): subject as a type + id pair of plain scalars (no cross-namespace relation), declines stored as `granted: false`, the resolved statement denormalized with its hash and served locale, no `updatedAt` and no unique constraint on subject + purpose. The migration backfills one row per pre-existing listing marked wording-unrecoverable, stamped at the listing's own `consent_at`, and never touches `project_listings`.
47
+
48
+ Additive: a flow, form, or site declaring no consent config renders and parses exactly as before. The one deliberate behavior change is the listing application's consent control.
49
+
50
+ - [#5294](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/5294) [`cf6b325`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/cf6b325298e92d22ab7edfe8538f5ca17f54ebfd) Thanks [@claudio-planck](https://github.com/claudio-planck)! - The founder application becomes data ([#5285](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5285)): a published field contract, questions and screens as records, and override by exception.
51
+
52
+ `@working-theory/validation` gains `listing-application-contract.ts` — the fields the platform actually READS, each with the call site that reads it, which of them a published listing cannot do without, and the control a question writing one must declare. Two beliefs the design carried did not survive that grounding and are recorded in the module header: there is no `FAQPage` emission for a listing in this tree, and no resume-link email. `listing-application.ts` gains the question record (an id, a control, copy, options, validation, and either a `writesTo` naming a contract field or a `siteAnswer` destination carrying a visibility flag), the screen record (its own copy, an ordered list of question ids, and its own side-panel declaration), merge-by-id so a site rewording one question inherits every other default including ones added later, and a parse-time invariant: exactly one question writes each required contract field, with zero and two both named failures. The compiler emits ONE step per screen carrying every field its questions contribute — the generalization of the `contact` and `faqs` cases, not a third mechanism. `listing-application-defaults.ts` ships a complete default question set and a grouped arrangement following combine-selection, separate-composition; it deliberately reverses the ships-no-copy discipline and says why.
53
+
54
+ `@working-theory/ui` reaches two capabilities that already existed and were thrown away in one line. `ListingApplicationFlow` returned the live tile unconditionally on every step, ignoring the view `FlowStepper` handed it; a screen declares its own side panel now, and a screen declaring none renders a full-width question with `FlowFrame`'s side region collapsed. `FlowFrame` gains a side cell with the same padding, measure and centring the active cell's step card has, so side content no longer hand-rolls its own inset, and its side region gains `md:flex-1` — without it the region carried `md:w-1/2` against an active region carrying `flex-1`, so a caller's gap came out of the question column alone. `FlowStepper` measures the step card and sizes the side cell to the tallest card observed, never shrinking, capped at the VISIBLE slot height rather than the frame's declared `h-dvh` — a page mounting the frame below a header makes it taller than the viewport.
55
+
56
+ `@working-theory/database` gains the bounded site-answers store on `ProjectListing`, and the logo columns become writable from the application.
57
+
58
+ Additive, with two boundaries stated precisely. The flat `steps` config parses and compiles to the byte-identical `FlowConfig` — verified old-against-new on a fifteen-step config exercising every step kind — and `steps` is now optional, with a config declaring both shapes, or declaring `steps` as an explicit `undefined`, a named parse failure. What is NOT unchanged is the rendered frame: the side-cell container, the shared gap and the measured panel height are fixes to `FlowFrame` and `FlowStepper`, so they reach every consumer of those two components by design, not only the listing application.
59
+
60
+ A site that overrides one question's copy and declares no `screens` inherits every other default and renders new defaults added later. A site that declares its own `screens` inherits new defaults into its question set but does not render them — declared screens are that site's statement of what it asks, and inserting a screen into a live form is not something an upstream release should do. A new default that writes a REQUIRED contract field still fails that site's config at parse, by name.
61
+
62
+ The three fields the application gained — the two logo URLs and the requested window — are OPT-IN: they are excluded from the default collected-field mask, so adding them cannot widen what an anonymous caller may write on a site that never asked for them. A site that wants them names them.
63
+
64
+ - [#5298](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/5298) [`57b0b04`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/57b0b047eb3f04d40f0712a893c89415b742c7f5) Thanks [@claudio-planck](https://github.com/claudio-planck)! - A verified owner link on a listing ([#5295](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5295)): a founder who signs in can read their own submitted listings, and nobody else's.
65
+
66
+ `@working-theory/database` gains `ProjectListing.ownerUserId` / `ownerLinkedAt` — plain nullable scalars with no cross-namespace relation, exactly the shape `Lead.promotedUserId` / `promotedAt` already has (ADR 0009 invariant [#1](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/1)). A listing carried `founderEmail` and `leadId` and no user link, so the only join available for "the listings belonging to this signed-in person" was a session's email against `founderEmail`. That string is typed into an anonymous form and proves nothing: two people who type the same address are two people, and the join hands one of them the other's submission. These columns are the ownership claim a read may actually join on.
67
+
68
+ The link is stamped only when all three of these hold: the reconciling user's own email is VERIFIED; that verified address matches the row's `founderEmail`; and the row's site resolves to one of that user's own tenant workspaces. Any one failing writes nothing and returns a count rather than throwing — the reconciliation path would otherwise fail an unrelated request. The email comparison is exact equality against the same normalization the write path applies, deliberately not Prisma's `mode: "insensitive"`, which compiles to `ILIKE` and would make `_` and `%` in the caller's own address wildcards that capture a neighbour's row.
69
+
70
+ A fourth condition has no counterpart in the lead service: the match must be UNAMBIGUOUS. Two strangers can type the same address into the anonymous form, and when they do nothing says which of the two un-linked rows is whose — so a row is claimed only when it is the only un-linked row carrying that address at its site, and an ambiguous group stays null. The cost is a founder who legitimately submitted two applications at one site, which the concurrent limit allows: neither is linked. That is the deliberate trade, because nothing in the data separates their second application from a stranger's first. The write also repeats `ownerUserId: null`, making it a compare-and-swap, so two concurrent reconciliation runs cannot transfer a row away from whoever claimed it first.
71
+
72
+ Candidacy is also restricted to submitted rows — every stored status except `draft`. A draft is addressed by its resume token, which is cleared at submit, so the token and the owner link hand off to each other cleanly and a draft needs no link. It is also a security boundary: draft creation is anonymous with the quota enforced at submit, so a claimable draft would let one unauthenticated request either disclose a stranger's text or, under the ambiguity refusal, permanently block someone from claiming their own listing.
73
+
74
+ `@working-theory/validation` gains `projectListingMeResponseSchema` — an array of the existing founder-facing `projectListingDraftSchema`, reused rather than forked because the audience is the same person that shape already serves. Nothing existing changes.
75
+
76
+ No existing row is backfilled, now or later. Every row predates verification, so deriving a link from `founderEmail` would assert the thing nobody proved. A listing submitted by someone who never signs in stays null for the life of the row, which is a legitimate resting state rather than a backlog.
77
+
78
+ Additive throughout. Both columns are nullable with no default; the new schema export is new; the operator's `owner|admin` floor on the existing tenant-scoped listing routes is unchanged, and the founder reads their own rows through the owner link rather than through a relaxed floor.
79
+
80
+ - [#5283](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/5283) [`a713ea7`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/a713ea78c12c1dc2519b80831deed197a4f227b4) Thanks [@claudio-planck](https://github.com/claudio-planck)! - A structured `address` field type in the flow vocabulary, and structured address storage for listings ([#5279](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/5279)).
81
+
82
+ `AddressFieldset` has shipped since [#4395](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/4395) and `StepFlow` has rendered it ever since, but `fieldInputTypeSchema` exposed six input types and `address` was not one of them — so a flow config could not name the composite, and the founder application collected a whole published business address in a single free-text box capped at 300 characters.
83
+
84
+ `fieldInputTypeSchema` gains `"address"`. The two cross-field rules already beneath that enum cover the new member without a new clause: `options` is select-only and `minLength` is text/textarea-only, so an address field declaring either is a named parse failure rather than a silently discarded setting. `flowAddressValueSchema` and `composeAddressLine` are new exports — the first declares the six-part value an address field submits, the second composes the single-line display string from those parts. There is deliberately no function that runs the conversion the other way.
85
+
86
+ `FlowStepper` reads, validates, gates, seeds and submits the composite against the six fixed keys `AddressFieldset` posts under, which is the handling `StepFlow` has carried since [#4395](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/4395). Required means every required sub-control has a value rather than that a key named `address` is non-empty. `ContactFormFields` already rendered the type and is unchanged, as are `AddressFieldset` and `ADDRESS_COUNTRIES`.
87
+
88
+ `ProjectListing` keeps `address` as the single-line display string and gains six nullable columns for the parts beside it. A new application composes its display string from the parts, server-side. An existing row keeps its display string exactly as the founder typed it and its parts stay null — no stored address is ever parsed, split, or rewritten, and the migration does not backfill.
89
+
90
+ Every change is additive: a flow config that declares no address field parses, renders and submits exactly as it did before, and a listing carrying only the legacy free-text address reads unchanged.
91
+
92
+ ### Patch Changes
93
+
94
+ - Updated dependencies [[`49e6ba4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/49e6ba470a0ff4a0e6d4f7d14147232e90a12259), [`cf6b325`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/cf6b325298e92d22ab7edfe8538f5ca17f54ebfd), [`767a35c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/767a35ca83155f774667ea3f7a10eb0ce1c094ab), [`57b0b04`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/57b0b047eb3f04d40f0712a893c89415b742c7f5), [`65989b4`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/65989b4dc6915bf37608475ed6409f98cd5945e2), [`a713ea7`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/a713ea78c12c1dc2519b80831deed197a4f227b4)]:
95
+ - @working-theory/validation@0.20.0
96
+
3
97
  ## 0.19.0
4
98
 
5
99
  ### Minor Changes