create-ailk 0.1.0 → 0.2.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.
- package/README.md +124 -3
- package/component-catalog.md +456 -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 +12 -5
- 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 +78 -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 +241 -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 +558 -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 +869 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -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-answers.test.ts +707 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -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 +120 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/me.ts +76 -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 +106 -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 +307 -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__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -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__/listing-promotion.test.ts +684 -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/consent.ts +236 -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 +1296 -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/listing-promotion.ts +342 -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 +510 -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 +1472 -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 +127 -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 +90 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -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 +121 -0
- package/templates/apps/web/lib/site-theme.ts +74 -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 +17 -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 +36 -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 +443 -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 +574 -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/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -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/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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project-listing-flow.ts
|
|
3
|
+
* @description The approve-time projection (#5001): a `ProjectListing` row's
|
|
4
|
+
* `questions` block becomes a live diagnostic — one `FlowDefinition` the
|
|
5
|
+
* visitor walks at `/v1/flows/{slug}` and one `WaitlistExperiment` keyed
|
|
6
|
+
* `{slug}` that scores the walk through the shared scoring rule
|
|
7
|
+
* (`@working-theory/database/flows/listing-diagnostic`).
|
|
8
|
+
*
|
|
9
|
+
* `generateListingFlow` writes through the two existing validated paths —
|
|
10
|
+
* `upsertFlowDefinition` (`flow-engine.ts`) and `upsertWaitlistExperiment`
|
|
11
|
+
* (`waitlist-experiments.ts`) — never a second ad-hoc Prisma write, and
|
|
12
|
+
* never `seedWaitlistExperiments`/`materializeWaitlistFlows`. Both underlying
|
|
13
|
+
* writes upsert on `[siteId, slug]` / `[siteId, key]`, and the `experimentKey`
|
|
14
|
+
* write-back is the same value every time, so re-approving a listing is a
|
|
15
|
+
* no-op rather than a duplicate.
|
|
16
|
+
*
|
|
17
|
+
* ONE vocabulary for the scored questions, since #5036 D4: the flow renders
|
|
18
|
+
* the founder's own option LABELS under the founder's own option VALUES, the
|
|
19
|
+
* register keys its `optionWeights` on those same values
|
|
20
|
+
* (`buildListingDiagnosticScoring`), and the experiment's `questionSet`
|
|
21
|
+
* declares that same per-listing option list — which is what satisfies
|
|
22
|
+
* `waitlistExperimentConfigSchema`'s "every weights key must be a declared
|
|
23
|
+
* option of the question it names" cross-validation. The old
|
|
24
|
+
* `q{n}:{i}of{k}` positional token, and the finite value universe it forced
|
|
25
|
+
* the question set to declare, are both gone. Only the EXTRAS keep a
|
|
26
|
+
* positional encoding (`x1:0of3`); they are unscored, so nothing resolves
|
|
27
|
+
* them by value.
|
|
28
|
+
*
|
|
29
|
+
* The tier NEVER reaches the visitor: the flow's terminal `message` step
|
|
30
|
+
* carries the confirmation line and the founder's own one-liner, and the
|
|
31
|
+
* config declares no `thankYou` / `deliverables` block, so nothing on the
|
|
32
|
+
* visitor's path can render a scoring outcome.
|
|
33
|
+
*
|
|
34
|
+
* PII boundary: logs and returned errors carry only `{ siteId, listingId,
|
|
35
|
+
* slug }` — never `founderEmail`, `founderName`, or `questions` (the same
|
|
36
|
+
* discipline `project-listings.ts` and `lead-routing.ts` document).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { buildListingDiagnosticScoring } from "@working-theory/database/flows/listing-diagnostic";
|
|
40
|
+
import type {
|
|
41
|
+
FlowConfig,
|
|
42
|
+
FlowStep,
|
|
43
|
+
ProjectListingQuestions,
|
|
44
|
+
WaitlistQuestion,
|
|
45
|
+
} from "@working-theory/validation";
|
|
46
|
+
|
|
47
|
+
import { prisma } from "../lib/prisma.js";
|
|
48
|
+
|
|
49
|
+
import { upsertFlowDefinition } from "./flow-engine.js";
|
|
50
|
+
import { upsertWaitlistExperiment } from "./waitlist-experiments.js";
|
|
51
|
+
|
|
52
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** The subset of the row the projection reads. */
|
|
55
|
+
export type ListingFlowSource = {
|
|
56
|
+
id: string;
|
|
57
|
+
siteId: string;
|
|
58
|
+
slug: string;
|
|
59
|
+
name: string;
|
|
60
|
+
oneLiner: string;
|
|
61
|
+
questions: ProjectListingQuestions;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type GenerateListingFlowError = {
|
|
65
|
+
kind: "invalid_input" | "flows_not_configured";
|
|
66
|
+
message: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type GenerateListingFlowResult =
|
|
70
|
+
| { ok: true; flowSlug: string; experimentKey: string }
|
|
71
|
+
| { ok: false; error: GenerateListingFlowError };
|
|
72
|
+
|
|
73
|
+
const DEFAULT_LOCALE = "en";
|
|
74
|
+
const en = (s: string) => ({ [DEFAULT_LOCALE]: s });
|
|
75
|
+
|
|
76
|
+
/** At most three extras (`projectListingQuestionsSchema`). */
|
|
77
|
+
type ExtraIndex = 1 | 2 | 3;
|
|
78
|
+
|
|
79
|
+
// ─── Identifier derivation ──────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The register-internal waitlist key for a listing slug. A listing slug is
|
|
83
|
+
* kebab AND may start with a digit (`^[a-z0-9-]{3,48}$`, #4998), while
|
|
84
|
+
* `flowIdentifierSchema` demands `^[a-z][a-z0-9_]*$` — so the `listing_`
|
|
85
|
+
* prefix is load-bearing, not decoration. 8 + 48 = 56 ≤ the 64-char cap.
|
|
86
|
+
*/
|
|
87
|
+
export function listingWaitlistKey(slug: string): string {
|
|
88
|
+
return `listing_${slug.replace(/-/g, "_")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `extra_1` … `extra_3` — the flow field name and question id for one extra. */
|
|
92
|
+
function extraFieldName(index: ExtraIndex): string {
|
|
93
|
+
return `extra_${index}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* An extra's option value — `x1:0of3`. Extras carry no points and no
|
|
98
|
+
* register resolves them, so the positional encoding survives #5036 D4's move
|
|
99
|
+
* to value-keyed scoring: nothing depends on an extra's value MEANING
|
|
100
|
+
* anything, only on it being stable and listing-independent for the CSV and
|
|
101
|
+
* any later reader.
|
|
102
|
+
*/
|
|
103
|
+
function extraOptionValue(index: ExtraIndex, optionIndex: number, arity: number): string {
|
|
104
|
+
return `x${index}:${optionIndex}of${arity}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── The flow (what the visitor walks) ──────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One option as every optioned question declares it — the founder's own
|
|
111
|
+
* `{ value, label }` pair. Since #5036 D4 the scored Q3–Q6 carry exactly the
|
|
112
|
+
* same shape as the unscored Q2: no positional rewrite in between.
|
|
113
|
+
*/
|
|
114
|
+
type OptionPair = { value: string; label: string };
|
|
115
|
+
|
|
116
|
+
/** One single-select `field` step — the shape every diagnostic question takes. */
|
|
117
|
+
function selectStep(
|
|
118
|
+
key: string,
|
|
119
|
+
fieldName: string,
|
|
120
|
+
prompt: string,
|
|
121
|
+
options: readonly OptionPair[],
|
|
122
|
+
): FlowStep {
|
|
123
|
+
return {
|
|
124
|
+
key,
|
|
125
|
+
type: "field",
|
|
126
|
+
copy: { title: en(prompt) },
|
|
127
|
+
fields: [
|
|
128
|
+
{
|
|
129
|
+
name: fieldName,
|
|
130
|
+
inputType: "select",
|
|
131
|
+
required: true,
|
|
132
|
+
options: options.map((o) => ({ value: o.value, label: en(o.label) })),
|
|
133
|
+
copy: { label: en(prompt) },
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The six-category step order (#5001): contact → segment → problem → fit →
|
|
141
|
+
* urgency → price → open → extras, then the confirmation.
|
|
142
|
+
*/
|
|
143
|
+
function buildFlowSteps(listing: ListingFlowSource): FlowStep[] {
|
|
144
|
+
const { questions } = listing;
|
|
145
|
+
|
|
146
|
+
const steps: FlowStep[] = [
|
|
147
|
+
{
|
|
148
|
+
key: "contact",
|
|
149
|
+
type: "field",
|
|
150
|
+
copy: { title: en(`Join the ${listing.name} waitlist`) },
|
|
151
|
+
fields: [
|
|
152
|
+
{
|
|
153
|
+
name: "name",
|
|
154
|
+
inputType: "text",
|
|
155
|
+
mapsTo: "name",
|
|
156
|
+
copy: { label: en("Name") },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "email",
|
|
160
|
+
inputType: "email",
|
|
161
|
+
required: true,
|
|
162
|
+
mapsTo: "email",
|
|
163
|
+
copy: { label: en("Email") },
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
// Every optioned question — the unscored Q2 (including the fixed
|
|
168
|
+
// `just-curious` bail-out its schema pins last) and the scored Q3–Q6
|
|
169
|
+
// alike — renders under the founder's OWN option values, so the raw value
|
|
170
|
+
// in the session state the weekly CSV reads is the same value the
|
|
171
|
+
// register scores.
|
|
172
|
+
selectStep("segment", "q2", questions.q2.prompt, questions.q2.options),
|
|
173
|
+
selectStep("problem", "q3", questions.q3.prompt, questions.q3.options),
|
|
174
|
+
selectStep("fit", "q4", questions.q4.prompt, questions.q4.options),
|
|
175
|
+
selectStep("urgency", "q5", questions.q5.prompt, questions.q5.options),
|
|
176
|
+
selectStep("price", "q6", questions.q6.prompt, questions.q6.options),
|
|
177
|
+
{
|
|
178
|
+
key: "open",
|
|
179
|
+
type: "field",
|
|
180
|
+
copy: { title: en(questions.q7.prompt) },
|
|
181
|
+
fields: [
|
|
182
|
+
{
|
|
183
|
+
name: "q7",
|
|
184
|
+
inputType: "textarea",
|
|
185
|
+
copy: { label: en(questions.q7.prompt) },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
if (questions.extras.length > 0) {
|
|
192
|
+
steps.push({
|
|
193
|
+
key: "extras",
|
|
194
|
+
type: "field",
|
|
195
|
+
copy: { title: en("A few more questions") },
|
|
196
|
+
fields: questions.extras.map((extra, i) => {
|
|
197
|
+
const index = (i + 1) as ExtraIndex;
|
|
198
|
+
return {
|
|
199
|
+
name: extraFieldName(index),
|
|
200
|
+
inputType: "select" as const,
|
|
201
|
+
required: true,
|
|
202
|
+
options: extra.options.map((option, optionIndex) => ({
|
|
203
|
+
value: extraOptionValue(index, optionIndex, extra.options.length),
|
|
204
|
+
label: en(option.label),
|
|
205
|
+
})),
|
|
206
|
+
copy: { label: en(extra.prompt) },
|
|
207
|
+
};
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Confirmation only — the founder's own line, never the tier.
|
|
213
|
+
steps.push({
|
|
214
|
+
key: "done",
|
|
215
|
+
type: "message",
|
|
216
|
+
copy: {
|
|
217
|
+
title: en(`You're on the ${listing.name} waitlist`),
|
|
218
|
+
...(listing.oneLiner.trim().length > 0 && { body: en(listing.oneLiner) }),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return steps;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildFlowConfig(listing: ListingFlowSource): FlowConfig {
|
|
226
|
+
return {
|
|
227
|
+
slug: listing.slug,
|
|
228
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
229
|
+
steps: buildFlowSteps(listing),
|
|
230
|
+
experiment: {
|
|
231
|
+
key: listing.slug,
|
|
232
|
+
waitlist: listingWaitlistKey(listing.slug),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── The experiment (what scores the walk) ──────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The register's question set — every question mirrors the flow exactly,
|
|
241
|
+
* Q3–Q6 included: since #5036 D4 the register scores this listing's OWN
|
|
242
|
+
* option values, so the question set declares this listing's own options and
|
|
243
|
+
* the config-level cross-validation passes without a value universe.
|
|
244
|
+
*/
|
|
245
|
+
function buildQuestionSet(listing: ListingFlowSource): WaitlistQuestion[] {
|
|
246
|
+
const { questions } = listing;
|
|
247
|
+
|
|
248
|
+
const scored = (["q3", "q4", "q5", "q6"] as const).map((id) => ({
|
|
249
|
+
id,
|
|
250
|
+
prompt: en(questions[id].prompt),
|
|
251
|
+
answerType: "single" as const,
|
|
252
|
+
options: questions[id].options.map((o) => ({ value: o.value, label: en(o.label) })),
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
return [
|
|
256
|
+
{
|
|
257
|
+
id: "name",
|
|
258
|
+
prompt: en("Name"),
|
|
259
|
+
answerType: "text" as const,
|
|
260
|
+
required: false,
|
|
261
|
+
mapsTo: "name" as const,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "email",
|
|
265
|
+
prompt: en("Email"),
|
|
266
|
+
answerType: "email" as const,
|
|
267
|
+
mapsTo: "email" as const,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "q2",
|
|
271
|
+
prompt: en(questions.q2.prompt),
|
|
272
|
+
answerType: "single" as const,
|
|
273
|
+
options: questions.q2.options.map((o) => ({ value: o.value, label: en(o.label) })),
|
|
274
|
+
},
|
|
275
|
+
...scored,
|
|
276
|
+
{
|
|
277
|
+
id: "q7",
|
|
278
|
+
prompt: en(questions.q7.prompt),
|
|
279
|
+
answerType: "text" as const,
|
|
280
|
+
required: false,
|
|
281
|
+
},
|
|
282
|
+
...questions.extras.map((extra, i) => {
|
|
283
|
+
const index = (i + 1) as ExtraIndex;
|
|
284
|
+
return {
|
|
285
|
+
id: extraFieldName(index),
|
|
286
|
+
prompt: en(extra.prompt),
|
|
287
|
+
answerType: "single" as const,
|
|
288
|
+
options: extra.options.map((option, optionIndex) => ({
|
|
289
|
+
value: extraOptionValue(index, optionIndex, extra.options.length),
|
|
290
|
+
label: en(option.label),
|
|
291
|
+
})),
|
|
292
|
+
};
|
|
293
|
+
}),
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildExperimentConfig(listing: ListingFlowSource): unknown {
|
|
298
|
+
const waitlistKey = listingWaitlistKey(listing.slug);
|
|
299
|
+
return {
|
|
300
|
+
key: listing.slug,
|
|
301
|
+
mode: "qualifying",
|
|
302
|
+
status: "live",
|
|
303
|
+
waitlists: [{ key: waitlistKey, flowSlug: listing.slug }],
|
|
304
|
+
questionSet: { version: 1, questions: buildQuestionSet(listing) },
|
|
305
|
+
// The shared scoring RULE, applied to this listing's own option values
|
|
306
|
+
// — same point names, same total, same tier floors for every listing;
|
|
307
|
+
// only the Q3/Q4 weight keys differ, because those keys are the founder's
|
|
308
|
+
// own values (#5036 D4). Embedded verbatim:
|
|
309
|
+
// `waitlistExperimentConfigSchema` carries scoring by value and declares
|
|
310
|
+
// no by-reference slot.
|
|
311
|
+
scoring: buildListingDiagnosticScoring(listing.questions),
|
|
312
|
+
tracking: {
|
|
313
|
+
version: 1,
|
|
314
|
+
utm: { source: "utm_source", medium: "utm_medium", campaign: "utm_campaign" },
|
|
315
|
+
waitlists: { [waitlistKey]: { campaign: listing.slug, channels: ["direct"] } },
|
|
316
|
+
},
|
|
317
|
+
stripeConfigRef: null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── The inverse (what the weekly CSV reads) ────────────────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* `field name -> option value -> the founder's own label`, for every optioned
|
|
325
|
+
* question the projection emits. The weekly CSV (`listing-csv-sweep.ts`)
|
|
326
|
+
* renders a signup's stored answer through this so the founder reads their own
|
|
327
|
+
* wording rather than a bare value.
|
|
328
|
+
*
|
|
329
|
+
* Q2–Q6 collapse to a plain value→label map since #5036 D4: the flow renders
|
|
330
|
+
* the founder's own values, so the inverse is the founder's own option list
|
|
331
|
+
* read the other way round. Only the extras still need the positional
|
|
332
|
+
* `extraOptionValue` — derived from the SAME call the projection emits, so
|
|
333
|
+
* the two can never disagree about what a stored value means.
|
|
334
|
+
*/
|
|
335
|
+
export function listingAnswerLabels(
|
|
336
|
+
questions: ProjectListingQuestions,
|
|
337
|
+
): Record<string, Record<string, string>> {
|
|
338
|
+
const labels: Record<string, Record<string, string>> = {};
|
|
339
|
+
|
|
340
|
+
for (const question of ["q2", "q3", "q4", "q5", "q6"] as const) {
|
|
341
|
+
labels[question] = Object.fromEntries(
|
|
342
|
+
questions[question].options.map((option) => [option.value, option.label]),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
questions.extras.forEach((extra, i) => {
|
|
347
|
+
const index = (i + 1) as ExtraIndex;
|
|
348
|
+
labels[extraFieldName(index)] = Object.fromEntries(
|
|
349
|
+
extra.options.map((option, optionIndex) => [
|
|
350
|
+
extraOptionValue(index, optionIndex, extra.options.length),
|
|
351
|
+
option.label,
|
|
352
|
+
]),
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return labels;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── generateListingFlow ────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Project an approved listing into its live diagnostic. Idempotent: both
|
|
363
|
+
* writes upsert on their natural key and the `experimentKey` write-back is
|
|
364
|
+
* stable, so a second approve changes nothing.
|
|
365
|
+
*
|
|
366
|
+
* Both joins are stated rather than inferred (#5036 D6d). The definition
|
|
367
|
+
* carries the listing's id, so a session started on this flow names the
|
|
368
|
+
* listing directly instead of relying on the flow's slug matching it; and the
|
|
369
|
+
* listing stores the experiment's KEY, which is the string `WaitlistScore`
|
|
370
|
+
* itself holds, so reading a listing's submissions is one hop rather than an
|
|
371
|
+
* id-to-key lookup. ADR 0009 forbids a relation across the namespace
|
|
372
|
+
* boundary, so both are plain strings — which is exactly why each has to be
|
|
373
|
+
* the string the other side actually stores.
|
|
374
|
+
*
|
|
375
|
+
* The FLOW is written first: an experiment row whose `waitlists[].flowSlug`
|
|
376
|
+
* resolves to nothing would leave `GET /v1/flows/{slug}` answering
|
|
377
|
+
* `flow_not_found` while the dashboard already showed a live experiment. The
|
|
378
|
+
* reverse ordering (a servable flow whose experiment has not landed yet)
|
|
379
|
+
* degrades to "collected, not yet scored" — recoverable on the next approve.
|
|
380
|
+
*/
|
|
381
|
+
export async function generateListingFlow(
|
|
382
|
+
listing: ListingFlowSource,
|
|
383
|
+
): Promise<GenerateListingFlowResult> {
|
|
384
|
+
const flow = await upsertFlowDefinition(
|
|
385
|
+
listing.siteId,
|
|
386
|
+
buildFlowConfig(listing),
|
|
387
|
+
listing.id,
|
|
388
|
+
);
|
|
389
|
+
if (!flow.ok) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
error: {
|
|
393
|
+
kind: flow.error.kind === "flows_not_configured" ? "flows_not_configured" : "invalid_input",
|
|
394
|
+
message: flow.error.message,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const experiment = await upsertWaitlistExperiment(
|
|
400
|
+
listing.siteId,
|
|
401
|
+
buildExperimentConfig(listing),
|
|
402
|
+
);
|
|
403
|
+
if (!experiment.ok) {
|
|
404
|
+
return {
|
|
405
|
+
ok: false,
|
|
406
|
+
error: {
|
|
407
|
+
kind:
|
|
408
|
+
experiment.error.kind === "flows_not_configured"
|
|
409
|
+
? "flows_not_configured"
|
|
410
|
+
: "invalid_input",
|
|
411
|
+
message: experiment.error.message,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// The experiment's key IS the listing's slug (`buildExperimentConfig`), and
|
|
417
|
+
// it is what `WaitlistScore.experimentKey` stores.
|
|
418
|
+
const experimentKey = listing.slug;
|
|
419
|
+
|
|
420
|
+
await prisma.projectListing.update({
|
|
421
|
+
where: { id: listing.id },
|
|
422
|
+
data: { experimentKey },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return { ok: true, flowSlug: listing.slug, experimentKey };
|
|
426
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project-listing-issue.ts
|
|
3
|
+
* @description The approve-time GitHub issue filing (#5000): an approved
|
|
4
|
+
* `ProjectListing` becomes a work order filed on the SITE's own repo
|
|
5
|
+
* (`AILK_GITHUB_REPO`), in the shape that repo's issue-claiming pipeline
|
|
6
|
+
* reads (`.claude/skills/file-issue/references/issue.md`'s canonical Issue
|
|
7
|
+
* body — `## What` / `## Why` / `## Acceptance Criteria` / `## Scope` /
|
|
8
|
+
* `## Grounding` / the `<!-- dep-graph: -->` block).
|
|
9
|
+
*
|
|
10
|
+
* `renderListingIssue` is pure — no fetch, no Prisma, no config read — so
|
|
11
|
+
* the shape is unit-testable against a fixture row with no network double.
|
|
12
|
+
* `fileListingIssue` is the effectful half: skip (env absent, or `issueUrl`
|
|
13
|
+
* already set) -> POST -> write `issueUrl` back on 201 -> log + return on
|
|
14
|
+
* ANY failure. It never throws; `project-listing-decision.ts`'s
|
|
15
|
+
* `sendListingDecisionEmail` calls it fire-and-forget after the founder
|
|
16
|
+
* email, with its own `.catch` boundary on top of the try/catch below, so a
|
|
17
|
+
* filing failure can never reach the approve path that has already
|
|
18
|
+
* committed.
|
|
19
|
+
*
|
|
20
|
+
* Injection hardening, mirroring `slack-notify.ts`'s `escapeMrkdwn` for the
|
|
21
|
+
* same class of anonymous-input risk (PR #4999 security review): every
|
|
22
|
+
* founder-authored scalar field (name/oneLiner/audience/problem/founderName/
|
|
23
|
+
* founderCompany/url) is (1) collapsed to a single line — an embedded
|
|
24
|
+
* newline could otherwise open a line with `## ` and forge a section a
|
|
25
|
+
* naive line-based consumer parser reads as real (e.g. a fake `## Scope`
|
|
26
|
+
* re-pointing what a worker builds), and (2) rendered as inline code, which
|
|
27
|
+
* GitHub never parses for an `@mention` or `#issue` auto-link. The
|
|
28
|
+
* `questions` block is embedded as fenced JSON instead — `JSON.stringify`
|
|
29
|
+
* already escapes any embedded newline to the two-character `\n` sequence,
|
|
30
|
+
* so no extra sanitization is needed there, and it stays byte-for-byte the
|
|
31
|
+
* stored value (#5000 AC2 — "the questions block verbatim").
|
|
32
|
+
*
|
|
33
|
+
* PII boundary: `founderEmail` lives on `ProjectListing` but is never
|
|
34
|
+
* referenced here — the row has it; this file never renders or logs it.
|
|
35
|
+
*/
|
|
36
|
+
import { logger } from "@working-theory/observability";
|
|
37
|
+
import type { ProjectListing } from "@working-theory/validation";
|
|
38
|
+
|
|
39
|
+
import { config } from "../config/index.js";
|
|
40
|
+
import { prisma } from "../lib/prisma.js";
|
|
41
|
+
|
|
42
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
43
|
+
const ISSUE_LABELS = ["status/ready"] as const;
|
|
44
|
+
|
|
45
|
+
const SCOPE_PATHS = [
|
|
46
|
+
"apps/web/app/[locale]/projects/[slug]/**",
|
|
47
|
+
"apps/web/src/content/projects/**",
|
|
48
|
+
"apps/web/src/content/diagnostic/**",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export type RenderListingIssueOptions = {
|
|
52
|
+
/** `owner/name` of the repo the issue is filed ON — the site's own repo, never this one. */
|
|
53
|
+
targetRepo: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type RenderedListingIssue = {
|
|
57
|
+
title: string;
|
|
58
|
+
body: string;
|
|
59
|
+
labels: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Collapses an embedded line ending (CR and/or LF) to a space — see the
|
|
64
|
+
* module doc's injection note — and strips backticks. Without stripping
|
|
65
|
+
* backticks, a founder-authored value containing one (e.g. `` Bob` @someone ``)
|
|
66
|
+
* closes `code()`'s single-backtick span early, leaving everything after it
|
|
67
|
+
* as live, unfenced markdown GitHub WILL autolink into a real `@mention`
|
|
68
|
+
* (PR #5011 security review, Finding 1) — the same failure mode `escapeMrkdwn`
|
|
69
|
+
* exists to prevent for Slack, just triggered by a different delimiter.
|
|
70
|
+
* A backtick is not meaningful content in any of these fields (a name, a
|
|
71
|
+
* one-liner, a URL, …), so substituting a straight quote is a lossless-enough
|
|
72
|
+
* trade for closing the injection outright, rather than computing a
|
|
73
|
+
* CommonMark-safe variable-length fence.
|
|
74
|
+
*/
|
|
75
|
+
function singleLine(value: string): string {
|
|
76
|
+
return value.replace(/[\r\n]+/g, " ").replace(/`/g, "'").trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What an absent field renders as. */
|
|
80
|
+
const ABSENT = "—";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Inline code: verbatim AND immune to GitHub's `@mention`/`#issue` markdown
|
|
84
|
+
* parsing.
|
|
85
|
+
*
|
|
86
|
+
* `null` is accepted because every application field became nullable at rest
|
|
87
|
+
* in #5036 D1 — a half-filled draft is a legitimate row. An issue is only ever
|
|
88
|
+
* filed from an APPROVED listing, which `submit` proved complete, so the null
|
|
89
|
+
* branch discharges a type-level possibility rather than a reachable state.
|
|
90
|
+
*/
|
|
91
|
+
function code(value: string | null): string {
|
|
92
|
+
return value === null ? ABSENT : `\`${singleLine(value)}\``;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Renders the build-issue title/body/labels from an approved listing row.
|
|
97
|
+
* Pure — every value comes from `listing` and `targetRepo`, nothing is read
|
|
98
|
+
* from config or the network.
|
|
99
|
+
*/
|
|
100
|
+
export function renderListingIssue(
|
|
101
|
+
listing: ProjectListing,
|
|
102
|
+
{ targetRepo }: RenderListingIssueOptions,
|
|
103
|
+
): RenderedListingIssue {
|
|
104
|
+
// `slug` is null only on a draft, and a draft never reaches this renderer.
|
|
105
|
+
const slug = listing.slug ?? listing.id;
|
|
106
|
+
const title = `feat(web): /projects/${slug} listing + diagnostic (listing ${listing.id})`;
|
|
107
|
+
|
|
108
|
+
const rowLines = [
|
|
109
|
+
`- id: ${code(listing.id)}`,
|
|
110
|
+
`- slug: ${code(listing.slug)}`,
|
|
111
|
+
`- name: ${code(listing.name)}`,
|
|
112
|
+
`- oneLiner: ${code(listing.oneLiner)}`,
|
|
113
|
+
`- problem: ${code(listing.problem)}`,
|
|
114
|
+
`- url: ${listing.url ? code(listing.url) : ABSENT}`,
|
|
115
|
+
`- product: ${code(listing.product)}`,
|
|
116
|
+
`- commitment: ${code(listing.commitment)}`,
|
|
117
|
+
`- teamComposition: ${code(listing.teamComposition)}`,
|
|
118
|
+
`- address: ${code(listing.address)}`,
|
|
119
|
+
`- anythingElse: ${listing.anythingElse ? code(listing.anythingElse) : ABSENT}`,
|
|
120
|
+
`- founderName: ${code(listing.founderName)}`,
|
|
121
|
+
`- founderCompany: ${listing.founderCompany ? code(listing.founderCompany) : ABSENT}`,
|
|
122
|
+
].join("\n");
|
|
123
|
+
|
|
124
|
+
const questionsJson = JSON.stringify(listing.questions, null, 2);
|
|
125
|
+
// #5036 — the three FAQ entries the detail page renders. Emitted as FENCED
|
|
126
|
+
// JSON rather than as `- ` bullets, the same treatment `questions` already
|
|
127
|
+
// gets: `answer` is 1000 characters of anonymous free text with no
|
|
128
|
+
// control-character rule, and a bullet list is exactly the shape
|
|
129
|
+
// docs/lessons/project-listings/07 describes — anonymous text landing in a
|
|
130
|
+
// document a downstream tool parses by heading. Inside a fence it forges
|
|
131
|
+
// nothing.
|
|
132
|
+
const faqsJson = JSON.stringify(listing.faqs ?? [], null, 2);
|
|
133
|
+
const scopeLines = SCOPE_PATHS.map((p) => `- ${p}`).join("\n");
|
|
134
|
+
|
|
135
|
+
const body = `<!-- ailk-listing-issue: repo=${targetRepo} listing=${listing.id} -->
|
|
136
|
+
|
|
137
|
+
## What
|
|
138
|
+
|
|
139
|
+
Build the public listing page and its diagnostic flow for ${code(listing.name)}:
|
|
140
|
+
|
|
141
|
+
- The page at \`/projects/${slug}\`, rendered from this row via the public read (\`GET /v1/project-listings/public\`).
|
|
142
|
+
- The diagnostic at \`/flows/${slug}\`, registered from the flow generated on approve.
|
|
143
|
+
|
|
144
|
+
**Row**
|
|
145
|
+
|
|
146
|
+
${rowLines}
|
|
147
|
+
|
|
148
|
+
**FAQs**
|
|
149
|
+
|
|
150
|
+
\`\`\`\`json
|
|
151
|
+
${faqsJson}
|
|
152
|
+
\`\`\`\`
|
|
153
|
+
|
|
154
|
+
**Questions**
|
|
155
|
+
|
|
156
|
+
\`\`\`\`json
|
|
157
|
+
${questionsJson}
|
|
158
|
+
\`\`\`\`
|
|
159
|
+
|
|
160
|
+
## Why
|
|
161
|
+
|
|
162
|
+
An approved application must be a complete work order a worker can build from without the operator touching it again.
|
|
163
|
+
|
|
164
|
+
## Acceptance Criteria
|
|
165
|
+
|
|
166
|
+
- [ ] page renders from the row
|
|
167
|
+
- [ ] diagnostic flow live at /flows/${slug}
|
|
168
|
+
- [ ] waitlist joins land under the listing's experiment
|
|
169
|
+
- [ ] /projects tile appears in the D6 order
|
|
170
|
+
|
|
171
|
+
## Scope
|
|
172
|
+
|
|
173
|
+
${scopeLines}
|
|
174
|
+
|
|
175
|
+
## Grounding
|
|
176
|
+
|
|
177
|
+
- **Governance:** \`internal\`
|
|
178
|
+
- **Nature:** \`feature\`
|
|
179
|
+
- **Resourced:** \`n/a\`
|
|
180
|
+
|
|
181
|
+
<!-- dep-graph:
|
|
182
|
+
blocked-by:
|
|
183
|
+
blocks:
|
|
184
|
+
priority:
|
|
185
|
+
scope-paths: ${SCOPE_PATHS.join(", ")}
|
|
186
|
+
-->
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
return { title, body, labels: [...ISSUE_LABELS] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Files the build issue for an approved listing, writing `issueUrl` back on
|
|
194
|
+
* success. No-op (`github-skipped`) when `AILK_GITHUB_REPO`/`AILK_GITHUB_TOKEN`
|
|
195
|
+
* are absent, or when the row already carries `issueUrl` (idempotent — a
|
|
196
|
+
* second approve, or a stray re-call, never double-files). Never throws: any
|
|
197
|
+
* failure (network, non-201, a malformed response) is logged — id + status
|
|
198
|
+
* only, never the token, never a response body that could carry one — and
|
|
199
|
+
* the row is left with `issueUrl: null` for a later retry via the PATCH
|
|
200
|
+
* route.
|
|
201
|
+
*/
|
|
202
|
+
export async function fileListingIssue(listing: ProjectListing): Promise<void> {
|
|
203
|
+
const targetRepo = config.AILK_GITHUB_REPO;
|
|
204
|
+
const token = config.AILK_GITHUB_TOKEN;
|
|
205
|
+
if (!targetRepo || !token) {
|
|
206
|
+
logger.info({ listingId: listing.id }, "github-skipped");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (listing.issueUrl) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const { title, body, labels } = renderListingIssue(listing, { targetRepo });
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(`${GITHUB_API_BASE}/repos/${targetRepo}/issues`, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
authorization: `Bearer ${token}`,
|
|
220
|
+
accept: "application/vnd.github+json",
|
|
221
|
+
"content-type": "application/json",
|
|
222
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({ title, body, labels }),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (res.status !== 201) {
|
|
228
|
+
logger.warn({ listingId: listing.id, status: res.status }, "listing issue filing failed");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const json = (await res.json()) as { html_url?: unknown };
|
|
233
|
+
if (typeof json.html_url !== "string" || json.html_url.length === 0) {
|
|
234
|
+
logger.warn(
|
|
235
|
+
{ listingId: listing.id, status: res.status },
|
|
236
|
+
"listing issue filing returned no html_url",
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await prisma.projectListing.update({
|
|
242
|
+
where: { id: listing.id },
|
|
243
|
+
data: { issueUrl: json.html_url },
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logger.warn(
|
|
247
|
+
{ listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
|
|
248
|
+
"listing issue filing threw",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|