create-ailk 0.1.0 → 0.1.1
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.
- package/README.md +124 -3
- package/component-catalog.md +295 -0
- package/dist/cli.js +92 -16
- package/dist/component-catalog.d.ts +11 -0
- package/dist/component-catalog.js +39 -0
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +24 -0
- package/dist/derive-architecture.d.ts +13 -0
- package/dist/derive-architecture.js +238 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +19 -2
- package/dist/lib/extract-module-bundle.d.ts +16 -0
- package/dist/lib/extract-module-bundle.js +184 -0
- package/dist/lib/fetch-module.d.ts +55 -0
- package/dist/lib/fetch-module.js +125 -0
- package/dist/lib/module-license-gate.d.ts +48 -0
- package/dist/lib/module-license-gate.js +67 -0
- package/dist/lib/module-tier.d.ts +35 -0
- package/dist/lib/module-tier.js +37 -0
- package/dist/module-architecture.d.ts +175 -0
- package/dist/module-architecture.js +663 -0
- package/dist/parse-args.d.ts +47 -1
- package/dist/parse-args.js +143 -2
- package/dist/programmatic.d.ts +160 -3
- package/dist/programmatic.js +196 -7
- package/dist/surfaces.d.ts +211 -0
- package/dist/surfaces.js +532 -0
- package/dist/sync-routes.d.ts +92 -0
- package/dist/sync-routes.js +350 -0
- package/package.json +22 -14
- package/templates/.claude/rules/architecture.md +6 -2
- package/templates/.claude/skills/README.md +9 -9
- package/templates/.claude/skills/add-schema/SKILL.md +14 -7
- package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
- package/templates/.claude/skills/provision-config/SKILL.md +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
- package/templates/.env.example +126 -2
- package/templates/CLAUDE.md +11 -5
- package/templates/README.md +1 -1
- package/templates/apps/api/.env.example +38 -0
- package/templates/apps/api/CLAUDE.md +75 -1
- package/templates/apps/api/LICENSE +201 -0
- package/templates/apps/api/api/index.ts +6 -141
- package/templates/apps/api/jest.config.cjs +16 -0
- package/templates/apps/api/package.json +22 -6
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
- package/templates/apps/api/src/__tests__/server.test.ts +46 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
- package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
- package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
- package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
- package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
- package/templates/apps/api/src/bin/seed-presets.ts +58 -0
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
- package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
- package/templates/apps/api/src/cli.ts +5 -11
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/config/modules.ts +43 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +231 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
- package/templates/apps/api/src/lib/site.ts +16 -0
- package/templates/apps/api/src/lib/stripe.ts +22 -0
- package/templates/apps/api/src/lib/tenant-db.ts +225 -0
- package/templates/apps/api/src/lib/ws-token.ts +91 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
- package/templates/apps/api/src/middleware/auth.ts +43 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
- package/templates/apps/api/src/middleware/tenant.ts +99 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
- package/templates/apps/api/src/openapi/spec.ts +312 -3
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
- package/templates/apps/api/src/routes/aeo/index.ts +107 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
- package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
- package/templates/apps/api/src/routes/billing/index.ts +36 -0
- package/templates/apps/api/src/routes/billing/portal.ts +182 -0
- package/templates/apps/api/src/routes/billing/read.ts +75 -0
- package/templates/apps/api/src/routes/billing/usage.ts +153 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
- package/templates/apps/api/src/routes/content/create.ts +1 -0
- package/templates/apps/api/src/routes/content/delete.ts +1 -0
- package/templates/apps/api/src/routes/content/index.ts +42 -6
- package/templates/apps/api/src/routes/content/update.ts +1 -0
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
- package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
- package/templates/apps/api/src/routes/flows/README.md +147 -0
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
- package/templates/apps/api/src/routes/flows/index.ts +202 -0
- package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
- package/templates/apps/api/src/routes/leads/index.ts +43 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +547 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +742 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +364 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
- package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +105 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
- package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +98 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
- package/templates/apps/api/src/routes/schedule/index.ts +249 -0
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
- package/templates/apps/api/src/routes/slack/actions.ts +177 -0
- package/templates/apps/api/src/routes/slack/index.ts +38 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
- package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
- package/templates/apps/api/src/routes/webhooks/README.md +35 -12
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
- package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
- package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
- package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
- package/templates/apps/api/src/server.ts +293 -7
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
- package/templates/apps/api/src/services/abandon-email.ts +307 -0
- package/templates/apps/api/src/services/aeo-score.ts +288 -0
- package/templates/apps/api/src/services/answer-distributions.ts +138 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
- package/templates/apps/api/src/services/entitlements.ts +102 -0
- package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
- package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
- package/templates/apps/api/src/services/flow-engine.ts +1278 -0
- package/templates/apps/api/src/services/lead-promotion.ts +176 -0
- package/templates/apps/api/src/services/lead-routing.ts +3 -3
- package/templates/apps/api/src/services/listing-config.ts +102 -0
- package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
- package/templates/apps/api/src/services/playbook-compile.ts +398 -0
- package/templates/apps/api/src/services/playbook-render.ts +263 -0
- package/templates/apps/api/src/services/project-listing-decision.ts +490 -0
- package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
- package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
- package/templates/apps/api/src/services/project-listings.ts +1124 -0
- package/templates/apps/api/src/services/recommender-capture.ts +835 -0
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
- package/templates/apps/api/src/services/scheduling/index.ts +63 -0
- package/templates/apps/api/src/services/scheduling/types.ts +88 -0
- package/templates/apps/api/src/services/slack-notify.ts +240 -0
- package/templates/apps/api/src/services/tenant-context.ts +451 -0
- package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
- package/templates/apps/api/src/services/usage-credits.ts +340 -0
- package/templates/apps/api/src/services/usage-metering.ts +699 -0
- package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
- package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
- package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
- package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
- package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
- package/templates/apps/api/src/vercel-handler.ts +165 -0
- package/templates/apps/api/tsconfig.json +0 -12
- package/templates/apps/mcp/CLAUDE.md +19 -9
- package/templates/apps/mcp/LICENSE +201 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
- package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
- package/templates/apps/mcp/package.json +6 -4
- package/templates/apps/mcp/src/config/modules.ts +37 -0
- package/templates/apps/mcp/src/server.ts +109 -18
- package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
- package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
- package/templates/apps/mcp/src/tools/create_page.ts +1 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
- package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
- package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
- package/templates/apps/mcp/src/tools/index.ts +25 -0
- package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
- package/templates/apps/mcp/src/tools/purchase.ts +3 -0
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
- package/templates/apps/mcp/src/tools/update_page.ts +1 -0
- package/templates/apps/mcp/tsconfig.json +1 -9
- package/templates/apps/web/.env.example +44 -0
- package/templates/apps/web/CLAUDE.md +28 -0
- package/templates/apps/web/LICENSE +201 -0
- package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
- package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
- package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
- package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
- package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
- package/templates/apps/web/__tests__/setup.ts +22 -32
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/layout.tsx +115 -16
- package/templates/apps/web/app/[locale]/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
- package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
- package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
- package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/globals.css +35 -0
- package/templates/apps/web/app/llms.txt/route.ts +2 -1
- package/templates/apps/web/app/og/route.tsx +10 -2
- package/templates/apps/web/auth.ts +8 -0
- package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
- package/templates/apps/web/content/en/blog/README.txt +25 -0
- package/templates/apps/web/content/en/docs/index.mdx +28 -0
- package/templates/apps/web/jest.config.cjs +84 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
- package/templates/apps/web/lib/account-config.ts +13 -0
- package/templates/apps/web/lib/server-api.ts +85 -0
- package/templates/apps/web/lib/site-brand.tsx +118 -0
- package/templates/apps/web/messages/en.json +275 -7
- package/templates/apps/web/middleware.ts +11 -3
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/apps/web/package.json +16 -7
- package/templates/apps/web/postcss.config.mjs +23 -0
- package/templates/apps/web/public/.well-known/security.txt +3 -3
- package/templates/apps/web/src/lib/routes.ts +15 -2
- package/templates/apps/web/tsconfig.json +1 -16
- package/templates/content/_site.mdx +24 -25
- package/templates/content/en/AboutPage/about.mdx +24 -14
- package/templates/content/en/ContactPage/contact.mdx +23 -14
- package/templates/content/en/FAQPage/faq.mdx +10 -392
- package/templates/content/en/HomePage/home.mdx +14 -285
- package/templates/database/CHANGELOG.md +382 -0
- package/templates/database/LICENSE +201 -0
- package/templates/database/README.md +3 -1
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
- package/templates/database/__tests__/flows-presets.test.ts +173 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
- package/templates/database/__tests__/seed.test.ts +88 -0
- package/templates/database/__tests__/user-cascade.test.ts +19 -4
- package/templates/database/auth/schema.prisma +521 -0
- package/templates/database/config/schema.prisma +234 -0
- package/templates/database/content/schema.prisma +263 -0
- package/templates/database/flows/listing-diagnostic.ts +182 -0
- package/templates/database/flows/presets.ts +315 -0
- package/templates/database/inbox/__tests__/schema.test.ts +4 -0
- package/templates/database/inbox/schema.prisma +476 -3
- package/templates/database/jest.config.cjs +8 -0
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
- package/templates/database/ops/schema.prisma +27 -0
- package/templates/database/package.json +42 -4
- package/templates/database/prisma.config.ts +1 -1
- package/templates/database/schema.prisma +19 -435
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/database/seed-test.ts +14 -4
- package/templates/database/seed.ts +148 -0
- package/templates/database/tenancy.ts +45 -0
- package/templates/database/tsconfig.flows.json +15 -0
- package/templates/database/tsconfig.json +7 -1
- package/templates/database/tsconfig.tenancy.json +14 -0
- package/templates/package.json +30 -55
- package/templates/pnpm-workspace.yaml +0 -3
- package/templates/project.yaml +1 -1
- package/templates/tsconfig.json +1 -37
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
- package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
- package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
- package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
- package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
- package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
- package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
- package/templates/apps/web/__tests__/page.test.tsx +0 -230
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
- package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
- package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
- package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
- package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
- package/templates/apps/web/content/en/blog/index.mdx +0 -15
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
- package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +0 -85
- package/templates/content/ar/HomePage/home.mdx +0 -123
- package/templates/content/ar/_site.mdx +0 -35
- package/templates/content/de/_site.mdx +0 -29
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.schema.json +0 -82
- package/templates/content/en/BookCall/book-call.mdx +0 -19
- package/templates/content/en/BookCall/book-call.schema.json +0 -121
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
- package/templates/content/en/CareersList/careers.mdx +0 -24
- package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
- package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
- package/templates/content/en/ClientsList/clients.mdx +0 -38
- package/templates/content/en/ContactPage/contact.schema.json +0 -100
- package/templates/content/en/ContactPoint/primary.mdx +0 -14
- package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
- package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
- package/templates/content/en/CoursesList/courses.mdx +0 -22
- package/templates/content/en/DownloadsList/downloads.mdx +0 -22
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
- package/templates/content/en/EventsList/events.mdx +0 -26
- package/templates/content/en/FAQPage/faq.schema.json +0 -231
- package/templates/content/en/GlossaryList/glossary.mdx +0 -19
- package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
- package/templates/content/en/HomePage/home.schema.json +0 -88
- package/templates/content/en/HomePage/launch-service.mdx +0 -475
- package/templates/content/en/HomePage/launch-service.schema.json +0 -138
- package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
- package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
- package/templates/content/en/Integration/github-actions.mdx +0 -31
- package/templates/content/en/Integration/openai-api.mdx +0 -26
- package/templates/content/en/Integration/vercel.mdx +0 -32
- package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
- package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
- package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
- package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
- package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
- package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
- package/templates/content/en/LandingPage/developers.mdx +0 -19
- package/templates/content/en/LandingPage/developers.schema.json +0 -109
- package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
- package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
- package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
- package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
- package/templates/content/en/LandingPage/platform.mdx +0 -369
- package/templates/content/en/LandingPage/platform.schema.json +0 -166
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
- package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
- package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
- package/templates/content/en/Organization/site-owner.mdx +0 -16
- package/templates/content/en/Partner/prisma.mdx +0 -31
- package/templates/content/en/Partner/supabase.mdx +0 -30
- package/templates/content/en/Partner/vercel.mdx +0 -28
- package/templates/content/en/PartnersList/partners.mdx +0 -22
- package/templates/content/en/Person/founder.mdx +0 -18
- package/templates/content/en/Pricing/pricing.mdx +0 -538
- package/templates/content/en/Pricing/pricing.schema.json +0 -209
- package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
- package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
- package/templates/content/en/Product/ailk.mdx +0 -19
- package/templates/content/en/Product/ailk.schema.json +0 -94
- package/templates/content/en/Product/api-launch-kit.mdx +0 -26
- package/templates/content/en/Product/launch-kit.mdx +0 -28
- package/templates/content/en/ProductsList/products.mdx +0 -26
- package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
- package/templates/content/en/ProjectsList/projects.mdx +0 -25
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
- package/templates/content/en/ResourcesHub/resources.mdx +0 -22
- package/templates/content/en/Service/consulting.mdx +0 -22
- package/templates/content/en/Service/implementation.mdx +0 -30
- package/templates/content/en/Service/launch-service.mdx +0 -232
- package/templates/content/en/Service/launch.mdx +0 -228
- package/templates/content/en/ServicesList/services.mdx +0 -84
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
- package/templates/content/en/TeamList/team.mdx +0 -63
- package/templates/content/en/TeamMember/founder.mdx +0 -72
- package/templates/content/en/TermsPage/terms.mdx +0 -19
- package/templates/content/en/TermsPage/terms.schema.json +0 -99
- package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
- package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
- package/templates/content/en-XA/AboutPage/about.mdx +0 -8
- package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
- package/templates/content/en-XA/HomePage/home.mdx +0 -8
- package/templates/content/es/_site.mdx +0 -30
- package/templates/content/fr/_site.mdx +0 -30
- package/templates/content/ja/_site.mdx +0 -30
- package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
- package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
- package/templates/content/pt-BR/HomePage/home.mdx +0 -23
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file flows-listing-diagnostic.test.ts
|
|
3
|
+
* @description The shared scoring RULE (#5001 AC2/AC3, reworked by #5036 D4):
|
|
4
|
+
* a register built from one listing's own questions scores its weakest path
|
|
5
|
+
* `curious`, its strongest `now`, and a mixed path where the 0–12 total says
|
|
6
|
+
* it does — evaluated through the real `evaluateWaitlistScoring`, never a
|
|
7
|
+
* reimplementation of the ladder.
|
|
8
|
+
*
|
|
9
|
+
* The load-bearing test here is "a reordered register scores an answer
|
|
10
|
+
* identically": #5036 D4 moved resolution from option POSITION to option
|
|
11
|
+
* VALUE, so reordering a founder's Q3/Q4 options and rebuilding the register
|
|
12
|
+
* leaves an already-stored answer scoring exactly what it always meant. Under
|
|
13
|
+
* the old `q{n}:{i}of{k}` encoding that assertion was not even expressible.
|
|
14
|
+
*
|
|
15
|
+
* The register must also parse against `waitlistScoringSchema` (the grammar
|
|
16
|
+
* `upsertWaitlistExperiment` validates on every write) AND survive
|
|
17
|
+
* `waitlistExperimentConfigSchema`'s config-level cross-validation, which is
|
|
18
|
+
* the only place the "every `optionWeights` key must be a declared option of
|
|
19
|
+
* the question it names" rule fires
|
|
20
|
+
* (docs/lessons/2026-09-07-shared-register-needs-a-shared-value-universe.md).
|
|
21
|
+
* The projection's REAL config is asserted the same way in
|
|
22
|
+
* `apps/api/src/services/__tests__/project-listing-flow.test.ts`; the minimal
|
|
23
|
+
* wrapper below exists so a register that could never be persisted fails in
|
|
24
|
+
* this package rather than one layer away.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
evaluateWaitlistScoring,
|
|
29
|
+
projectListingQuestionsSchema,
|
|
30
|
+
waitlistExperimentConfigSchema,
|
|
31
|
+
waitlistScoringSchema,
|
|
32
|
+
type ProjectListingQuestions,
|
|
33
|
+
} from "@working-theory/validation";
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
buildListingDiagnosticScoring,
|
|
37
|
+
listingDiagnosticOptionWeights,
|
|
38
|
+
listingOptionPoints,
|
|
39
|
+
LISTING_DIAGNOSTIC_Q5_WEIGHTS,
|
|
40
|
+
LISTING_DIAGNOSTIC_Q6_WEIGHTS,
|
|
41
|
+
} from "../flows/listing-diagnostic";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A listing's template instance: Q3/Q4 at their 4-option maximum with
|
|
45
|
+
* founder-authored values, Q5's three fixed urgency values, Q6's four fixed
|
|
46
|
+
* bands. Parsed through the real schema so the fixture cannot drift from the
|
|
47
|
+
* contract the projection reads.
|
|
48
|
+
*/
|
|
49
|
+
const QUESTIONS: ProjectListingQuestions = projectListingQuestionsSchema.parse({
|
|
50
|
+
q2: {
|
|
51
|
+
prompt: "What brings you here?",
|
|
52
|
+
options: [
|
|
53
|
+
{ value: "shipping-now", label: "I'm shipping something now" },
|
|
54
|
+
{ value: "just-curious", label: "Just curious" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
q3: {
|
|
58
|
+
prompt: "How painful is this problem today?",
|
|
59
|
+
options: [
|
|
60
|
+
{ value: "none", label: "Not a problem" },
|
|
61
|
+
{ value: "mild", label: "Mildly annoying" },
|
|
62
|
+
{ value: "real", label: "A real drag" },
|
|
63
|
+
{ value: "blocking", label: "It's blocking me" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
q4: {
|
|
67
|
+
prompt: "How close is this to what you need?",
|
|
68
|
+
options: [
|
|
69
|
+
{ value: "off", label: "Not what I need" },
|
|
70
|
+
{ value: "adjacent", label: "Adjacent" },
|
|
71
|
+
{ value: "close", label: "Close" },
|
|
72
|
+
{ value: "exact", label: "Exactly it" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
q5: {
|
|
76
|
+
prompt: "When would you use this?",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: "urgent", label: "Right away" },
|
|
79
|
+
{ value: "planning", label: "Next quarter" },
|
|
80
|
+
{ value: "exploring", label: "Just looking" },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
q6: {
|
|
84
|
+
prompt: "What would you pay?",
|
|
85
|
+
options: [
|
|
86
|
+
{ value: "low", label: "Nothing yet" },
|
|
87
|
+
{ value: "medium", label: "A little" },
|
|
88
|
+
{ value: "high", label: "A real budget" },
|
|
89
|
+
{ value: "critical", label: "Whatever it takes" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
q7: { prompt: "Anything else we should know?" },
|
|
93
|
+
extras: [],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const SCORING = buildListingDiagnosticScoring(QUESTIONS);
|
|
97
|
+
|
|
98
|
+
/** The `optionWeights` map the register declares for one scored question. */
|
|
99
|
+
function weightsOf(
|
|
100
|
+
scoring: ReturnType<typeof buildListingDiagnosticScoring>,
|
|
101
|
+
question: string,
|
|
102
|
+
): Record<string, number> {
|
|
103
|
+
const point = scoring.points.find((p) => p.question === question);
|
|
104
|
+
if (!point || point.kind !== "optionWeights") {
|
|
105
|
+
throw new Error(`no optionWeights point for "${question}"`);
|
|
106
|
+
}
|
|
107
|
+
return point.weights;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The minimal experiment config that carries a register — enough to fire
|
|
112
|
+
* `waitlistExperimentConfigSchema`'s weights-key cross-validation, and
|
|
113
|
+
* nothing more. The question set declares THIS listing's own options, which
|
|
114
|
+
* is precisely what value-keyed scoring makes possible.
|
|
115
|
+
*/
|
|
116
|
+
function experimentConfigFor(questions: ProjectListingQuestions): unknown {
|
|
117
|
+
const scored = (["q3", "q4", "q5", "q6"] as const).map((id) => ({
|
|
118
|
+
id,
|
|
119
|
+
prompt: { en: questions[id].prompt },
|
|
120
|
+
answerType: "single" as const,
|
|
121
|
+
options: questions[id].options.map((o) => ({
|
|
122
|
+
value: o.value,
|
|
123
|
+
label: { en: o.label },
|
|
124
|
+
})),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
key: "orbit-notes",
|
|
129
|
+
mode: "qualifying",
|
|
130
|
+
status: "live",
|
|
131
|
+
waitlists: [{ key: "listing_orbit_notes", flowSlug: "orbit-notes" }],
|
|
132
|
+
questionSet: {
|
|
133
|
+
version: 1,
|
|
134
|
+
questions: [
|
|
135
|
+
{
|
|
136
|
+
id: "email",
|
|
137
|
+
prompt: { en: "Email" },
|
|
138
|
+
answerType: "email",
|
|
139
|
+
mapsTo: "email",
|
|
140
|
+
},
|
|
141
|
+
...scored,
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
scoring: buildListingDiagnosticScoring(questions),
|
|
145
|
+
tracking: {
|
|
146
|
+
version: 1,
|
|
147
|
+
utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
|
|
148
|
+
waitlists: { listing_orbit_notes: { campaign: "orbit-notes", channels: ["direct"] } },
|
|
149
|
+
},
|
|
150
|
+
stripeConfigRef: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe("listingOptionPoints — the founder's declared order → 0..3", () => {
|
|
155
|
+
it("spans 0..3 whatever the arity, weakest first", () => {
|
|
156
|
+
expect([0, 1, 2].map((i) => listingOptionPoints(i, 3))).toEqual([0, 2, 3]);
|
|
157
|
+
expect([0, 1, 2, 3].map((i) => listingOptionPoints(i, 4))).toEqual([0, 1, 2, 3]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("yields 0 for a question with no spread to score", () => {
|
|
161
|
+
expect(listingOptionPoints(0, 1)).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("listingDiagnosticOptionWeights — keyed by value, weighted by order", () => {
|
|
166
|
+
it("keys the founder's own values and weights them by their declared order", () => {
|
|
167
|
+
expect(
|
|
168
|
+
listingDiagnosticOptionWeights([
|
|
169
|
+
{ value: "none" },
|
|
170
|
+
{ value: "mild" },
|
|
171
|
+
{ value: "real" },
|
|
172
|
+
{ value: "blocking" },
|
|
173
|
+
]),
|
|
174
|
+
).toEqual({ none: 0, mild: 1, real: 2, blocking: 3 });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("still spans 0..3 at three options", () => {
|
|
178
|
+
expect(
|
|
179
|
+
listingDiagnosticOptionWeights([{ value: "x" }, { value: "y" }, { value: "z" }]),
|
|
180
|
+
).toEqual({ x: 0, y: 2, z: 3 });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("the fixed-value questions", () => {
|
|
185
|
+
it("scores Q5 STRONGEST-first — `urgent` 3, `exploring` 0", () => {
|
|
186
|
+
// #4998 pins Q5's values urgency-descending, so the positional rule
|
|
187
|
+
// scored `urgent` 0 (docs/lessons/2026-09-07-q5-fixed-order-is-
|
|
188
|
+
// strongest-first.md). Value-keyed weights say what is meant instead.
|
|
189
|
+
expect(LISTING_DIAGNOSTIC_Q5_WEIGHTS).toEqual({
|
|
190
|
+
urgent: 3,
|
|
191
|
+
planning: 2,
|
|
192
|
+
exploring: 0,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("keeps Q6 weakest-first — `low` 0, `critical` 3", () => {
|
|
197
|
+
expect(LISTING_DIAGNOSTIC_Q6_WEIGHTS).toEqual({
|
|
198
|
+
low: 0,
|
|
199
|
+
medium: 1,
|
|
200
|
+
high: 2,
|
|
201
|
+
critical: 3,
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("leaves the per-question spread at 0..3, so the total stays 0..12", () => {
|
|
206
|
+
for (const question of ["q3", "q4", "q5", "q6"]) {
|
|
207
|
+
const values = Object.values(weightsOf(SCORING, question));
|
|
208
|
+
expect(Math.min(...values)).toBe(0);
|
|
209
|
+
expect(Math.max(...values)).toBe(3);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("buildListingDiagnosticScoring — the register", () => {
|
|
215
|
+
it("parses against waitlistScoringSchema", () => {
|
|
216
|
+
expect(waitlistScoringSchema.safeParse(SCORING).success).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("parses against waitlistExperimentConfigSchema at the config level", () => {
|
|
220
|
+
// The weights-key cross-validation only fires here — the question set
|
|
221
|
+
// declares this listing's OWN options, and the register keys on the same
|
|
222
|
+
// values, so no value universe is needed.
|
|
223
|
+
const parsed = waitlistExperimentConfigSchema.safeParse(experimentConfigFor(QUESTIONS));
|
|
224
|
+
expect(parsed.success).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("scores Q3–Q6 only — Q1/Q2/Q7 and the extras carry no points", () => {
|
|
228
|
+
expect(SCORING.points.map((p) => p.question)).toEqual(["q3", "q4", "q5", "q6"]);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("keys every weight on the founder's own option values, and nothing else", () => {
|
|
232
|
+
for (const question of ["q3", "q4", "q5", "q6"] as const) {
|
|
233
|
+
expect(Object.keys(weightsOf(SCORING, question)).sort()).toEqual(
|
|
234
|
+
QUESTIONS[question].options.map((o) => o.value).sort(),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("gives two listings the same RULE — same shape, same tiers, same fixed weights", () => {
|
|
240
|
+
const other = buildListingDiagnosticScoring(
|
|
241
|
+
projectListingQuestionsSchema.parse({
|
|
242
|
+
...QUESTIONS,
|
|
243
|
+
q3: {
|
|
244
|
+
prompt: "How badly does this hurt?",
|
|
245
|
+
options: [
|
|
246
|
+
{ value: "x", label: "Not at all" },
|
|
247
|
+
{ value: "y", label: "Somewhat" },
|
|
248
|
+
{ value: "z", label: "Badly" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(other.points.map((p) => p.name)).toEqual(SCORING.points.map((p) => p.name));
|
|
255
|
+
expect(other.scores).toEqual(SCORING.scores);
|
|
256
|
+
expect(other.tiers).toEqual(SCORING.tiers);
|
|
257
|
+
expect(other.default).toEqual(SCORING.default);
|
|
258
|
+
// Only the founder-authored questions' KEYS differ.
|
|
259
|
+
expect(weightsOf(other, "q5")).toEqual(weightsOf(SCORING, "q5"));
|
|
260
|
+
expect(weightsOf(other, "q6")).toEqual(weightsOf(SCORING, "q6"));
|
|
261
|
+
expect(weightsOf(other, "q3")).toEqual({ x: 0, y: 2, z: 3 });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── The three paths (#5001 AC2) ────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
it("scores the WEAKEST path 0 → curious", () => {
|
|
267
|
+
const result = evaluateWaitlistScoring(SCORING, {
|
|
268
|
+
q3: "none",
|
|
269
|
+
q4: "off",
|
|
270
|
+
q5: "exploring",
|
|
271
|
+
q6: "low",
|
|
272
|
+
});
|
|
273
|
+
expect(result.score).toBe(0);
|
|
274
|
+
expect(result.tier).toBe("curious");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("scores the STRONGEST path 12 → now", () => {
|
|
278
|
+
const result = evaluateWaitlistScoring(SCORING, {
|
|
279
|
+
q3: "blocking",
|
|
280
|
+
q4: "exact",
|
|
281
|
+
q5: "urgent",
|
|
282
|
+
q6: "critical",
|
|
283
|
+
});
|
|
284
|
+
expect(result.score).toBe(12);
|
|
285
|
+
expect(result.tier).toBe("now");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("scores a MIXED path by the sum of its answers", () => {
|
|
289
|
+
// q3 `real` 2 · q4 `adjacent` 1 · q5 `urgent` 3 · q6 `medium` 1 = 7 →
|
|
290
|
+
// soon (7–9).
|
|
291
|
+
const result = evaluateWaitlistScoring(SCORING, {
|
|
292
|
+
q3: "real",
|
|
293
|
+
q4: "adjacent",
|
|
294
|
+
q5: "urgent",
|
|
295
|
+
q6: "medium",
|
|
296
|
+
});
|
|
297
|
+
expect(result.score).toBe(7);
|
|
298
|
+
expect(result.tier).toBe("soon");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it.each([
|
|
302
|
+
[{ q3: "none", q4: "off", q5: "exploring", q6: "critical" }, 3, "curious"],
|
|
303
|
+
[{ q3: "mild", q4: "adjacent", q5: "exploring", q6: "high" }, 4, "later"],
|
|
304
|
+
[{ q3: "real", q4: "close", q5: "exploring", q6: "high" }, 6, "later"],
|
|
305
|
+
[{ q3: "blocking", q4: "exact", q5: "exploring", q6: "critical" }, 9, "soon"],
|
|
306
|
+
[{ q3: "blocking", q4: "exact", q5: "planning", q6: "critical" }, 11, "now"],
|
|
307
|
+
])("puts total %j (%i) in the %s band", (answers, expectedScore, expectedTier) => {
|
|
308
|
+
const result = evaluateWaitlistScoring(SCORING, answers as Record<string, string>);
|
|
309
|
+
expect(result.score).toBe(expectedScore);
|
|
310
|
+
expect(result.tier).toBe(expectedTier);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("is total — an unanswered diagnostic still lands on a tier", () => {
|
|
314
|
+
const result = evaluateWaitlistScoring(SCORING, {});
|
|
315
|
+
expect(result.score).toBe(0);
|
|
316
|
+
expect(result.tier).toBe("curious");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ── #5036 D4: resolution by value, not position ────────────────────────
|
|
320
|
+
|
|
321
|
+
it("a reordered register scores an answer identically", () => {
|
|
322
|
+
// The founder swaps the two middle Q3 options and the two middle Q4
|
|
323
|
+
// options — a real edit to the register, which changes what those two
|
|
324
|
+
// options are worth. The ANSWERED options (`blocking`, the strongest Q3;
|
|
325
|
+
// `off`, the weakest Q4) keep the strength the founder declared, so the
|
|
326
|
+
// same stored answer must score exactly what it scored before.
|
|
327
|
+
//
|
|
328
|
+
// This test is not expressible under the old `q{n}:{i}of{k}` encoding:
|
|
329
|
+
// there was no founder value to answer with, and a stored `q3:3of4`
|
|
330
|
+
// token was a POSITION, so any reorder or insert silently repointed it at
|
|
331
|
+
// a different option. Resolving by value is what makes the answer survive
|
|
332
|
+
// an edit to the register.
|
|
333
|
+
const answers = { q3: "blocking", q4: "off", q5: "urgent", q6: "high" };
|
|
334
|
+
const before = evaluateWaitlistScoring(SCORING, answers);
|
|
335
|
+
|
|
336
|
+
const reordered = buildListingDiagnosticScoring(
|
|
337
|
+
projectListingQuestionsSchema.parse({
|
|
338
|
+
...QUESTIONS,
|
|
339
|
+
q3: {
|
|
340
|
+
prompt: QUESTIONS.q3.prompt,
|
|
341
|
+
options: [
|
|
342
|
+
{ value: "none", label: "Not a problem" },
|
|
343
|
+
{ value: "real", label: "A real drag" },
|
|
344
|
+
{ value: "mild", label: "Mildly annoying" },
|
|
345
|
+
{ value: "blocking", label: "It's blocking me" },
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
q4: {
|
|
349
|
+
prompt: QUESTIONS.q4.prompt,
|
|
350
|
+
options: [
|
|
351
|
+
{ value: "off", label: "Not what I need" },
|
|
352
|
+
{ value: "close", label: "Close" },
|
|
353
|
+
{ value: "adjacent", label: "Adjacent" },
|
|
354
|
+
{ value: "exact", label: "Exactly it" },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
const after = evaluateWaitlistScoring(reordered, answers);
|
|
360
|
+
|
|
361
|
+
expect(before.score).toBe(8);
|
|
362
|
+
expect(after.score).toBe(before.score);
|
|
363
|
+
expect(after.tier).toBe(before.tier);
|
|
364
|
+
|
|
365
|
+
// The reorder is not a no-op: the two moved options really did trade
|
|
366
|
+
// weights, and the register the answer was resolved against is a
|
|
367
|
+
// different object than the one it was stored under.
|
|
368
|
+
expect(weightsOf(reordered, "q3")).not.toEqual(weightsOf(SCORING, "q3"));
|
|
369
|
+
expect(weightsOf(reordered, "q4")).not.toEqual(weightsOf(SCORING, "q4"));
|
|
370
|
+
expect(weightsOf(reordered, "q3")).toEqual({ none: 0, real: 1, mild: 2, blocking: 3 });
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("still parses at the config level after a reorder", () => {
|
|
374
|
+
// A reordered register's keys are still exactly the (reordered) options
|
|
375
|
+
// the question set declares, so the cross-validation holds.
|
|
376
|
+
const reordered = projectListingQuestionsSchema.parse({
|
|
377
|
+
...QUESTIONS,
|
|
378
|
+
q3: {
|
|
379
|
+
prompt: QUESTIONS.q3.prompt,
|
|
380
|
+
options: [
|
|
381
|
+
{ value: "none", label: "Not a problem" },
|
|
382
|
+
{ value: "real", label: "A real drag" },
|
|
383
|
+
{ value: "mild", label: "Mildly annoying" },
|
|
384
|
+
{ value: "blocking", label: "It's blocking me" },
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
expect(
|
|
389
|
+
waitlistExperimentConfigSchema.safeParse(experimentConfigFor(reordered)).success,
|
|
390
|
+
).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file flows-presets.test.ts
|
|
3
|
+
* @description Parse-validity for the four static capture presets (#3958
|
|
4
|
+
* §3.5; the `roadmap` preset added by #3963 D2). This is the AC1 + AC3
|
|
5
|
+
* verification: each preset must parse against
|
|
6
|
+
* `flowConfigSchema` and every user-facing string must be a
|
|
7
|
+
* `LocalizedString` carrying the flow's `defaultLocale` key. A bare-string
|
|
8
|
+
* literal is not assignable to `LocalizedString` at the type layer — caught
|
|
9
|
+
* by `pnpm type-check`'s `tsc -b`, via `database/tsconfig.flows.json`'s
|
|
10
|
+
* flows-directory TypeScript include (built in the `db:generate` -> `build`
|
|
11
|
+
* chain; the root `tsconfig.json` project-references
|
|
12
|
+
* `database/tsconfig.build.json`, not `database/tsconfig.json`, so
|
|
13
|
+
* `tsconfig.flows.json` is what actually puts `flows/presets.ts` on the
|
|
14
|
+
* whole-monorepo `tsc -b` graph). `database/tsconfig.json`'s own matching
|
|
15
|
+
* include entry (added alongside) serves a SEPARATE consumer —
|
|
16
|
+
* `eslint.config.ts`'s typed-lint project discovery, so this file isn't an
|
|
17
|
+
* unlinted "orphan" — and is rejected here at the schema layer as a
|
|
18
|
+
* belt-and-suspenders runtime check.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { flowConfigSchema } from "@working-theory/validation";
|
|
22
|
+
|
|
23
|
+
import { FLOW_PRESETS } from "../flows/presets";
|
|
24
|
+
|
|
25
|
+
describe("FLOW_PRESETS", () => {
|
|
26
|
+
it("declares exactly the four static presets, by slug", () => {
|
|
27
|
+
expect(FLOW_PRESETS.map((p) => p.slug).sort()).toEqual([
|
|
28
|
+
"form",
|
|
29
|
+
"lead-magnet",
|
|
30
|
+
"roadmap",
|
|
31
|
+
"waitlist",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each(["form", "waitlist", "lead-magnet", "roadmap"])(
|
|
36
|
+
"the %s preset validates against flowConfigSchema",
|
|
37
|
+
(slug) => {
|
|
38
|
+
const preset = FLOW_PRESETS.find((p) => p.slug === slug);
|
|
39
|
+
expect(preset?.slug).toBe(slug);
|
|
40
|
+
|
|
41
|
+
const parsed = flowConfigSchema.safeParse(preset);
|
|
42
|
+
expect(parsed.success).toBe(true);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
it("every preset declares defaultLocale: en with a key present on every LocalizedString", () => {
|
|
47
|
+
for (const preset of FLOW_PRESETS) {
|
|
48
|
+
expect(preset.defaultLocale).toBe("en");
|
|
49
|
+
// A parse failure here means some LocalizedString is missing the
|
|
50
|
+
// defaultLocale key (flowConfigSchema's superRefine) — re-assert with
|
|
51
|
+
// full issue detail for a failing preset's diagnosis.
|
|
52
|
+
const parsed = flowConfigSchema.safeParse(preset);
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${preset.slug} failed validation: ${JSON.stringify(parsed.error.issues, null, 2)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("the lead-magnet preset's qualifier step carries no segments band map (spec §7 item 4)", () => {
|
|
62
|
+
const preset = FLOW_PRESETS.find((p) => p.slug === "lead-magnet");
|
|
63
|
+
const qualifier = preset?.steps.find((s) => s.key === "qualifier");
|
|
64
|
+
expect(qualifier?.type).toBe("segmentation");
|
|
65
|
+
if (qualifier?.type === "segmentation") {
|
|
66
|
+
expect(qualifier.segments).toBeUndefined();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("every preset declares exactly one field mapped to mapsTo: email — some steps collapse name+email into one 'contact' step, roadmap splits them (spec #3963 D2)", () => {
|
|
71
|
+
for (const preset of FLOW_PRESETS) {
|
|
72
|
+
const emailFields = preset.steps.flatMap((step) =>
|
|
73
|
+
step.type === "field"
|
|
74
|
+
? step.fields.filter((f) => f.mapsTo === "email")
|
|
75
|
+
: [],
|
|
76
|
+
);
|
|
77
|
+
expect(emailFields).toHaveLength(1);
|
|
78
|
+
expect(emailFields[0]?.name).toBe("email");
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("no field step in any preset declares copy.cta — the FlowStepper default fallback (#4708) is what renders their submit control", () => {
|
|
83
|
+
// #4708's drift guard, the other half of which is
|
|
84
|
+
// packages/ui/src/sections/__tests__/FlowStepper.test.tsx's
|
|
85
|
+
// `roadmapShapedConfig()` / `noCtaConfig()` — packages/ui cannot import
|
|
86
|
+
// FLOW_PRESETS directly (database/flows is not a workspace package and
|
|
87
|
+
// packages/ui declares no dependency on it — a first-time cross-tier
|
|
88
|
+
// import architecture.yaml's boundary lint would refuse), so this
|
|
89
|
+
// in-package assertion is the closest mechanical proof that the shape
|
|
90
|
+
// the UI-side mirror configs cover is the shape these real presets
|
|
91
|
+
// actually carry. If a preset ever gains an explicit cta, this
|
|
92
|
+
// assertion needs updating — re-check the UI-side mirror still matches.
|
|
93
|
+
for (const preset of FLOW_PRESETS) {
|
|
94
|
+
for (const step of preset.steps) {
|
|
95
|
+
if (step.type !== "field") continue;
|
|
96
|
+
expect(step.copy.cta).toBeUndefined();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("rejects a preset carrying a bare-string title instead of a LocalizedString", () => {
|
|
102
|
+
const mutated = {
|
|
103
|
+
...FLOW_PRESETS[0],
|
|
104
|
+
steps: [
|
|
105
|
+
{
|
|
106
|
+
...FLOW_PRESETS[0]!.steps[0]!,
|
|
107
|
+
copy: { ...FLOW_PRESETS[0]!.steps[0]!.copy, title: "Get in touch" },
|
|
108
|
+
},
|
|
109
|
+
...FLOW_PRESETS[0]!.steps.slice(1),
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const parsed = flowConfigSchema.safeParse(mutated);
|
|
114
|
+
expect(parsed.success).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── FLOW_PRESETS — roadmap preset (#3963 D2) ────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe("roadmap preset (#3963 D2)", () => {
|
|
121
|
+
const preset = FLOW_PRESETS.find((p) => p.slug === "roadmap");
|
|
122
|
+
|
|
123
|
+
it("exists and validates against flowConfigSchema", () => {
|
|
124
|
+
expect(preset?.slug).toBe("roadmap");
|
|
125
|
+
expect(flowConfigSchema.safeParse(preset).success).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("declares a two-band segments map on its segmentation step", () => {
|
|
129
|
+
const stage = preset?.steps.find((s) => s.key === "stage");
|
|
130
|
+
expect(stage?.type).toBe("segmentation");
|
|
131
|
+
if (stage?.type === "segmentation") {
|
|
132
|
+
expect(stage.segments?.bands.map((b) => b.key).sort()).toEqual([
|
|
133
|
+
"early",
|
|
134
|
+
"scaling",
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("maps each band to a distinct thankYou slug (AC2 — two different segmentation answers derive different TY slugs)", () => {
|
|
140
|
+
expect(preset?.thankYou?.default).toBe("roadmap-thanks");
|
|
141
|
+
expect(preset?.thankYou?.bySegment).toEqual({
|
|
142
|
+
early: "roadmap-thanks",
|
|
143
|
+
scaling: "roadmap-thanks-qualified",
|
|
144
|
+
});
|
|
145
|
+
// The "scaling" (qualified) arm resolves to a DIFFERENT TY page than the
|
|
146
|
+
// "early" arm and the default — the divergence the AC requires.
|
|
147
|
+
expect(preset?.thankYou?.bySegment?.scaling).not.toBe(
|
|
148
|
+
preset?.thankYou?.default,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("maps each band to a distinct deliverables assetKey (AC2 — different asset grants)", () => {
|
|
153
|
+
const assets = preset?.deliverables?.assets ?? [];
|
|
154
|
+
const early = assets.find((a) => a.segment === "early");
|
|
155
|
+
const scaling = assets.find((a) => a.segment === "scaling");
|
|
156
|
+
expect(early?.assetKey).toBe("roadmap-early");
|
|
157
|
+
expect(scaling?.assetKey).toBe("roadmap-scaling");
|
|
158
|
+
expect(early?.assetKey).not.toBe(scaling?.assetKey);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("declares abandonEmail authored but dark (#3962 D2 — shipped off, no send)", () => {
|
|
162
|
+
expect(preset?.abandonEmail?.mode).toBe("dark");
|
|
163
|
+
expect(preset?.abandonEmail?.delayMinutes).toBeGreaterThanOrEqual(5);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("collects no phone/address fields (the acquisition-capture's §5 over-collection is deliberately not reproduced)", () => {
|
|
167
|
+
const fieldNames = (preset?.steps ?? []).flatMap((step) =>
|
|
168
|
+
step.type === "field" ? step.fields.map((f) => f.name) : [],
|
|
169
|
+
);
|
|
170
|
+
expect(fieldNames).not.toContain("phone");
|
|
171
|
+
expect(fieldNames).not.toContain("address");
|
|
172
|
+
});
|
|
173
|
+
});
|