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,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project-listing-decision.ts
|
|
3
|
+
* @description The one Approve/Reject transition for a `ProjectListing`
|
|
4
|
+
* (#4999) — the SAME function `PATCH /v1/project-listings/:id` (tenant-scoped,
|
|
5
|
+
* via `routes/project-listings/patch.ts`) and `POST /v1/slack/actions` (the
|
|
6
|
+
* Slack button click, no tenant context) both call, so the stamp + backlink
|
|
7
|
+
* logic exists in exactly one place.
|
|
8
|
+
*
|
|
9
|
+
* `siteId` is optional: the tenant-scoped PATCH route passes it (so a foreign
|
|
10
|
+
* listing id 404s, matching every other tenant-scoped read/write in this
|
|
11
|
+
* service group); the Slack handler omits it (the button's `value` carries
|
|
12
|
+
* only the listing id — no tenant context to check).
|
|
13
|
+
*
|
|
14
|
+
* Approve stamps `windowEndsAt = decidedAt + the row's windowDays`, the
|
|
15
|
+
* applicant's requested window already clamped at create against the site's
|
|
16
|
+
* `SiteListingConfig` (#5033 D4). The ninety days this used to hardcode were
|
|
17
|
+
* one site's commercial terms; there is no duration constant here now. A row
|
|
18
|
+
* already decided (`status` already `active` or `rejected`) is idempotent —
|
|
19
|
+
* `{ ok: true, changed: false }`, the row untouched, no second stamp — the
|
|
20
|
+
* "second click" AC (#4999): both the PATCH route (an operator re-submitting
|
|
21
|
+
* the same decision) and the Slack route (a stale button click after the
|
|
22
|
+
* message has already updated) see the same 200/no-op shape. Only a listing
|
|
23
|
+
* still `requested` can transition; anything else (an already-decided row
|
|
24
|
+
* asking for a DIFFERENT decision, or a `draft` asking to be decided) is
|
|
25
|
+
* `invalid_transition`.
|
|
26
|
+
*
|
|
27
|
+
* `value` is a strict `ProjectListing` (safe to pass straight through
|
|
28
|
+
* `routes/project-listings/respond.ts#sendValidated`, which re-parses
|
|
29
|
+
* against `projectListingSchema.strict()`); `slackChannel`/`slackTs` live
|
|
30
|
+
* OUTSIDE it, in the sibling `slack` field, so an unexpected key never
|
|
31
|
+
* reaches that contract check. The row shape/select and the `waitlistCount`
|
|
32
|
+
* join still mirror `services/project-listings.ts` rather than importing it,
|
|
33
|
+
* but the one piece that MUST NOT drift — the computed `closed` state — is
|
|
34
|
+
* now imported from there (`computeListingStatus`), so both modules resolve a
|
|
35
|
+
* row's lifecycle status through the same function.
|
|
36
|
+
*
|
|
37
|
+
* Also exports `sendListingDecisionEmail` — the founder email dispatch
|
|
38
|
+
* `routes/slack/actions.ts` (on a changed Slack decision) and
|
|
39
|
+
* `routes/project-listings/patch.ts` (on a changed PATCH decision) both
|
|
40
|
+
* call, so the founder is emailed exactly once regardless of which channel
|
|
41
|
+
* decided. See that function's own doc comment for the env-var reuse this
|
|
42
|
+
* issue makes (no new email-specific env var beyond the three Slack ones).
|
|
43
|
+
* On an APPROVE, it also fires `fileListingIssue` (#5000,
|
|
44
|
+
* `project-listing-issue.ts`) fire-and-forget after the email — the one
|
|
45
|
+
* work-order GitHub issue for the row, on the site's own repo. Both
|
|
46
|
+
* callers reach it through this one function, so the filing (like the
|
|
47
|
+
* email) fires exactly once per changed decision, never per caller.
|
|
48
|
+
*
|
|
49
|
+
* Also exports `onListingDecided` — the approve-path side-effect seam (#5001):
|
|
50
|
+
* a `requested` → `active` transition projects the row into its live
|
|
51
|
+
* diagnostic (`generateListingFlow`). `decideListing` calls it after a CHANGED
|
|
52
|
+
* approve, and since #5033 collapsed the lifecycle that is the ONLY caller —
|
|
53
|
+
* `requested → active | rejected` is the only transition there is, and both
|
|
54
|
+
* the PATCH route and the Slack click reach it through `decideListing`.
|
|
55
|
+
* Fail-soft by construction — a projection failure is logged
|
|
56
|
+
* and swallowed; the decision has already committed. #5000 (the build issue)
|
|
57
|
+
* joins the same seam.
|
|
58
|
+
*
|
|
59
|
+
* PII boundary: `decideListing` never logs (the DB mutation only).
|
|
60
|
+
* `sendListingDecisionEmail` logs `listingId`/`decision`/an error KIND only —
|
|
61
|
+
* never founderEmail/founderName/questions (mirrors
|
|
62
|
+
* `services/project-listings.ts` and `services/lead-routing.ts`'s
|
|
63
|
+
* discipline).
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import type { Prisma } from "@working-theory/database";
|
|
67
|
+
import { sendListingApprovedEmail, sendListingRejectedEmail } from "@working-theory/email";
|
|
68
|
+
import { logger } from "@working-theory/observability";
|
|
69
|
+
import {
|
|
70
|
+
clampWindowDays,
|
|
71
|
+
projectListingQuestionsSchema,
|
|
72
|
+
type ProjectListing,
|
|
73
|
+
type ProjectListingFaqs,
|
|
74
|
+
type ProjectListingQuestions,
|
|
75
|
+
} from "@working-theory/validation";
|
|
76
|
+
|
|
77
|
+
import { config } from "../config/index.js";
|
|
78
|
+
import { prisma } from "../lib/prisma.js";
|
|
79
|
+
|
|
80
|
+
import { listingConfigFor } from "./listing-config.js";
|
|
81
|
+
import { generateListingFlow } from "./project-listing-flow.js";
|
|
82
|
+
import { fileListingIssue } from "./project-listing-issue.js";
|
|
83
|
+
import {
|
|
84
|
+
addressPartsFromRow,
|
|
85
|
+
computeListingStatus,
|
|
86
|
+
siteAnswersFromRow,
|
|
87
|
+
} from "./project-listings.js";
|
|
88
|
+
|
|
89
|
+
export type ListingDecision = "active" | "rejected";
|
|
90
|
+
|
|
91
|
+
/** Days to milliseconds. A unit conversion, not a policy duration. */
|
|
92
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
93
|
+
|
|
94
|
+
const SELECT = {
|
|
95
|
+
id: true,
|
|
96
|
+
siteId: true,
|
|
97
|
+
leadId: true,
|
|
98
|
+
slug: true,
|
|
99
|
+
name: true,
|
|
100
|
+
oneLiner: true,
|
|
101
|
+
problem: true,
|
|
102
|
+
url: true,
|
|
103
|
+
product: true,
|
|
104
|
+
commitment: true,
|
|
105
|
+
teamComposition: true,
|
|
106
|
+
faqs: true,
|
|
107
|
+
anythingElse: true,
|
|
108
|
+
address: true,
|
|
109
|
+
// #5279 — the structured parts beside the display string.
|
|
110
|
+
addressLine1: true,
|
|
111
|
+
addressLine2: true,
|
|
112
|
+
addressCity: true,
|
|
113
|
+
addressRegion: true,
|
|
114
|
+
addressPostalCode: true,
|
|
115
|
+
addressCountry: true,
|
|
116
|
+
questions: true,
|
|
117
|
+
// #5285 — the operator's review payload carries EVERY site answer, the
|
|
118
|
+
// `operator-only` ones included. That visibility is what it is for: an
|
|
119
|
+
// answer a site asked for its own triage reaches the reviewer and stops
|
|
120
|
+
// short of the public read, which narrows through `publicSiteAnswers`.
|
|
121
|
+
siteAnswers: true,
|
|
122
|
+
founderName: true,
|
|
123
|
+
founderEmail: true,
|
|
124
|
+
founderCompany: true,
|
|
125
|
+
headline: true,
|
|
126
|
+
logoMarkUrl: true,
|
|
127
|
+
logoWordmarkUrl: true,
|
|
128
|
+
featured: true,
|
|
129
|
+
status: true,
|
|
130
|
+
consentAt: true,
|
|
131
|
+
decidedAt: true,
|
|
132
|
+
decidedBy: true,
|
|
133
|
+
issueUrl: true,
|
|
134
|
+
experimentKey: true,
|
|
135
|
+
windowDays: true,
|
|
136
|
+
windowEndsAt: true,
|
|
137
|
+
slackChannel: true,
|
|
138
|
+
slackTs: true,
|
|
139
|
+
createdAt: true,
|
|
140
|
+
updatedAt: true,
|
|
141
|
+
} as const satisfies Prisma.ProjectListingSelect;
|
|
142
|
+
|
|
143
|
+
type Row = Prisma.ProjectListingGetPayload<{ select: typeof SELECT }>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mirrors `project-listings.ts#attachWaitlistCounts`'s join shape for the
|
|
147
|
+
* single row this module ever handles.
|
|
148
|
+
*
|
|
149
|
+
* Since #5036 D6d the listing stores the experiment's KEY, so this is one
|
|
150
|
+
* count against `WaitlistScore.experimentKey` — the `WaitlistExperiment` read
|
|
151
|
+
* that existed only to translate an id into that key is gone from both
|
|
152
|
+
* modules.
|
|
153
|
+
*/
|
|
154
|
+
async function waitlistCountFor(row: Row): Promise<number> {
|
|
155
|
+
if (!row.experimentKey) return 0;
|
|
156
|
+
return prisma.waitlistScore.count({
|
|
157
|
+
where: { siteId: row.siteId, experimentKey: row.experimentKey },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toListing(row: Row, waitlistCount: number): ProjectListing {
|
|
162
|
+
return {
|
|
163
|
+
id: row.id,
|
|
164
|
+
siteId: row.siteId,
|
|
165
|
+
leadId: row.leadId,
|
|
166
|
+
slug: row.slug,
|
|
167
|
+
name: row.name,
|
|
168
|
+
headline: row.headline,
|
|
169
|
+
oneLiner: row.oneLiner,
|
|
170
|
+
problem: row.problem,
|
|
171
|
+
url: row.url,
|
|
172
|
+
product: row.product as ProjectListing["product"],
|
|
173
|
+
commitment: row.commitment as ProjectListing["commitment"],
|
|
174
|
+
teamComposition: row.teamComposition as ProjectListing["teamComposition"],
|
|
175
|
+
faqs: row.faqs as unknown as ProjectListingFaqs | null,
|
|
176
|
+
anythingElse: row.anythingElse,
|
|
177
|
+
address: row.address,
|
|
178
|
+
// #5279 — one reader of the parts, shared with `project-listings.ts`, so
|
|
179
|
+
// the two row renderers cannot drift on what "has parts" means.
|
|
180
|
+
addressParts: addressPartsFromRow(row),
|
|
181
|
+
founderName: row.founderName,
|
|
182
|
+
founderEmail: row.founderEmail,
|
|
183
|
+
founderCompany: row.founderCompany,
|
|
184
|
+
questions: row.questions as unknown as ProjectListingQuestions | null,
|
|
185
|
+
siteAnswers: siteAnswersFromRow(row.siteAnswers),
|
|
186
|
+
logoMarkUrl: row.logoMarkUrl,
|
|
187
|
+
logoWordmarkUrl: row.logoWordmarkUrl,
|
|
188
|
+
featured: row.featured,
|
|
189
|
+
status: computeListingStatus(row.status, row.windowEndsAt, new Date(), row.featured),
|
|
190
|
+
consentAt: row.consentAt.toISOString(),
|
|
191
|
+
decidedAt: row.decidedAt ? row.decidedAt.toISOString() : null,
|
|
192
|
+
decidedBy: row.decidedBy,
|
|
193
|
+
issueUrl: row.issueUrl,
|
|
194
|
+
experimentKey: row.experimentKey,
|
|
195
|
+
windowEndsAt: row.windowEndsAt ? row.windowEndsAt.toISOString() : null,
|
|
196
|
+
waitlistCount,
|
|
197
|
+
createdAt: row.createdAt.toISOString(),
|
|
198
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type DecideListingInput = {
|
|
203
|
+
id: string;
|
|
204
|
+
decision: ListingDecision;
|
|
205
|
+
decidedBy: string;
|
|
206
|
+
/** Tenant scope for the PATCH route; omitted by the Slack handler (no tenant context on a button click). */
|
|
207
|
+
siteId?: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type DecideListingResult =
|
|
211
|
+
| {
|
|
212
|
+
ok: true;
|
|
213
|
+
changed: boolean;
|
|
214
|
+
value: ProjectListing;
|
|
215
|
+
/** The row's posted-message coordinates, for the Slack route's `chat.update` — never part of `value`. */
|
|
216
|
+
slack: { channel: string | null; ts: string | null };
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
ok: false;
|
|
220
|
+
error: { kind: "not_found" | "invalid_transition"; message: string };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const DECIDED_STATUSES: ReadonlySet<string> = new Set(["active", "rejected"]);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The window, in whole days, to stamp on an approve. The row's own
|
|
227
|
+
* `windowDays` (the applicant's request, clamped at create against the site's
|
|
228
|
+
* terms) when it has one; otherwise the site's configured default clamped the
|
|
229
|
+
* same way, for a row that was already pending when the column shipped.
|
|
230
|
+
* `null` when the row does not exist — the caller's fresh read reports that.
|
|
231
|
+
*/
|
|
232
|
+
async function windowDaysFor(id: string, siteId: string | undefined): Promise<number | null> {
|
|
233
|
+
const row = await prisma.projectListing.findFirst({
|
|
234
|
+
where: siteId !== undefined ? { id, siteId } : { id },
|
|
235
|
+
select: { siteId: true, windowDays: true },
|
|
236
|
+
});
|
|
237
|
+
if (!row) return null;
|
|
238
|
+
if (row.windowDays !== null) return row.windowDays;
|
|
239
|
+
return clampWindowDays(undefined, await listingConfigFor(row.siteId));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The transition write is a GUARDED `updateMany` — `where` includes
|
|
244
|
+
* `status: "applied"`, so the write only lands if the row is STILL `applied`
|
|
245
|
+
* at the moment Postgres evaluates it (row-level locking makes this atomic;
|
|
246
|
+
* mirrors the compare-and-swap shape `routes/webhooks/stripe.ts#stampFlowCheckout`
|
|
247
|
+
* uses on `FlowCheckout.status`). `count === 1` means THIS call made the
|
|
248
|
+
* transition; `count === 0` means it didn't — either the row was never
|
|
249
|
+
* `applied` to begin with, or a CONCURRENT call already decided it between
|
|
250
|
+
* this call's own read and write (security review, PR #4999: a plain
|
|
251
|
+
* read-then-write here would let two racing calls — e.g. Slack's own retry,
|
|
252
|
+
* or a Slack click racing an operator's PATCH — both observe `applied` and
|
|
253
|
+
* both "succeed", double-sending the founder email and possibly leaving the
|
|
254
|
+
* row in whichever decision wrote last). Either way, a fresh read after the
|
|
255
|
+
* guarded write tells the caller which: already decided (idempotent
|
|
256
|
+
* `changed: false`) or a genuinely invalid transition (`live`/`closed`).
|
|
257
|
+
*/
|
|
258
|
+
export async function decideListing({
|
|
259
|
+
id,
|
|
260
|
+
decision,
|
|
261
|
+
decidedBy,
|
|
262
|
+
siteId,
|
|
263
|
+
}: DecideListingInput): Promise<DecideListingResult> {
|
|
264
|
+
const decidedAt = new Date();
|
|
265
|
+
|
|
266
|
+
// #5033 — the window this listing was granted, resolved BEFORE the guarded
|
|
267
|
+
// write because a `updateMany` cannot read a column it is also setting.
|
|
268
|
+
// `windowDays` is immutable once stamped at create, so reading it outside
|
|
269
|
+
// the guard is not part of the compare-and-swap. A row that predates the
|
|
270
|
+
// column (an application still pending when this shipped) has none, and
|
|
271
|
+
// takes the site's configured default instead.
|
|
272
|
+
const windowDays = await windowDaysFor(id, siteId);
|
|
273
|
+
|
|
274
|
+
const updateResult = await prisma.projectListing.updateMany({
|
|
275
|
+
where: siteId !== undefined ? { id, siteId, status: "requested" } : { id, status: "requested" },
|
|
276
|
+
data: {
|
|
277
|
+
status: decision,
|
|
278
|
+
decidedAt,
|
|
279
|
+
decidedBy,
|
|
280
|
+
...(decision === "active" &&
|
|
281
|
+
windowDays !== null && {
|
|
282
|
+
windowEndsAt: new Date(decidedAt.getTime() + windowDays * MS_PER_DAY),
|
|
283
|
+
}),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const current = await prisma.projectListing.findFirst({
|
|
288
|
+
where: siteId !== undefined ? { id, siteId } : { id },
|
|
289
|
+
select: SELECT,
|
|
290
|
+
});
|
|
291
|
+
if (!current) {
|
|
292
|
+
return { ok: false, error: { kind: "not_found", message: "Project listing not found" } };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (updateResult.count === 1) {
|
|
296
|
+
// #5001 — the approve's downstream effects (the diagnostic projection).
|
|
297
|
+
// Never throws; a failure is logged and the decision stands. Re-read
|
|
298
|
+
// afterwards so `value.experimentKey` reflects the projection.
|
|
299
|
+
await onListingDecided(current.siteId, current.id, decision);
|
|
300
|
+
const projected =
|
|
301
|
+
(await prisma.projectListing.findFirst({ where: { id: current.id }, select: SELECT })) ?? current;
|
|
302
|
+
const waitlistCount = await waitlistCountFor(projected);
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
changed: true,
|
|
306
|
+
value: toListing(projected, waitlistCount),
|
|
307
|
+
slack: { channel: projected.slackChannel, ts: projected.slackTs },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The guarded write matched nothing — the row wasn't `applied` when
|
|
312
|
+
// Postgres evaluated the guard. Read its CURRENT status to tell an
|
|
313
|
+
// already-decided idempotent no-op from a genuinely invalid transition.
|
|
314
|
+
if (DECIDED_STATUSES.has(current.status)) {
|
|
315
|
+
const waitlistCount = await waitlistCountFor(current);
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
changed: false,
|
|
319
|
+
value: toListing(current, waitlistCount),
|
|
320
|
+
slack: { channel: current.slackChannel, ts: current.slackTs },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
error: {
|
|
327
|
+
kind: "invalid_transition",
|
|
328
|
+
message: `cannot transition from '${current.status}' to '${decision}'`,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Sends the founder's approve/reject email for a just-decided listing
|
|
335
|
+
* (#4999 step 5).
|
|
336
|
+
*
|
|
337
|
+
* Reasonable-call env-var reuse (this issue's footprint is the three Slack
|
|
338
|
+
* vars only — no new email-specific var): `fromAddress` reads the existing
|
|
339
|
+
* `AILK_LEAD_NOTIFICATION_FROM` (already the site's configured "From:" for
|
|
340
|
+
* lead-facing system email, per `services/lead-routing.ts`); the Working
|
|
341
|
+
* Session and diagnostic links are derived from the existing
|
|
342
|
+
* `config.FRONTEND_URL` + fixed paths. The diagnostic page itself is #5001's
|
|
343
|
+
* build — this link is a forward reference to a route that doesn't exist
|
|
344
|
+
* yet. `brand` is left unset (the template's clean, unbranded default) since
|
|
345
|
+
* no per-site branding source is wired into `apps/api` today.
|
|
346
|
+
*
|
|
347
|
+
* Never throws; a missing `AILK_LEAD_NOTIFICATION_FROM` no-ops (logs
|
|
348
|
+
* `listing-email-skipped`, id + decision only) rather than failing the
|
|
349
|
+
* decision that has already landed.
|
|
350
|
+
*
|
|
351
|
+
* On an APPROVE, also fires `fileListingIssue` (#5000) after the email —
|
|
352
|
+
* fire-and-forget, `.catch`-guarded on top of that function's own internal
|
|
353
|
+
* try/catch, so neither a slow GitHub call nor a filing failure can delay or
|
|
354
|
+
* reach the approve path that has already committed. Runs regardless of the
|
|
355
|
+
* email's own outcome (a Resend failure and a GitHub failure are independent
|
|
356
|
+
* best-effort side effects of the same committed decision).
|
|
357
|
+
*/
|
|
358
|
+
export async function sendListingDecisionEmail(
|
|
359
|
+
listing: ProjectListing,
|
|
360
|
+
decision: ListingDecision,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
const fromAddress = process.env["AILK_LEAD_NOTIFICATION_FROM"];
|
|
363
|
+
if (!fromAddress) {
|
|
364
|
+
logger.info({ listingId: listing.id, decision }, "listing-email-skipped");
|
|
365
|
+
fileIssueOnApprove(listing, decision);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const workingSession = `${config.FRONTEND_URL}/working-session`;
|
|
370
|
+
// #5036 D1 — `name` and `slug` are nullable at rest so a half-filled draft
|
|
371
|
+
// is storable, but a decision only ever lands on a SUBMITTED row, which
|
|
372
|
+
// proved both. The fallbacks discharge the type, they do not paper over a
|
|
373
|
+
// reachable state.
|
|
374
|
+
const projectName = listing.name ?? listing.founderName;
|
|
375
|
+
const diagnosticSlug = listing.slug ?? listing.id;
|
|
376
|
+
|
|
377
|
+
const result =
|
|
378
|
+
decision === "active"
|
|
379
|
+
? await sendListingApprovedEmail({
|
|
380
|
+
to: listing.founderEmail,
|
|
381
|
+
fromAddress,
|
|
382
|
+
founderName: listing.founderName,
|
|
383
|
+
projectName,
|
|
384
|
+
links: { workingSession },
|
|
385
|
+
})
|
|
386
|
+
: await sendListingRejectedEmail({
|
|
387
|
+
to: listing.founderEmail,
|
|
388
|
+
fromAddress,
|
|
389
|
+
founderName: listing.founderName,
|
|
390
|
+
projectName,
|
|
391
|
+
links: { workingSession, diagnostic: `${config.FRONTEND_URL}/diagnostic/${diagnosticSlug}` },
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (!result.ok) {
|
|
395
|
+
logger.warn(
|
|
396
|
+
{ listingId: listing.id, decision, errorKind: result.error.kind },
|
|
397
|
+
"listing email send failed",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fileIssueOnApprove(listing, decision);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Fire-and-forget entry to `fileListingIssue` (#5000) — a no-op on
|
|
406
|
+
* `rejected`. `fileListingIssue` already never throws (its own try/catch
|
|
407
|
+
* around the fetch + DB write); this `.catch` is a second boundary for the
|
|
408
|
+
* one thing outside that try — `renderListingIssue` running before it — so
|
|
409
|
+
* NOTHING from this call can ever propagate into `sendListingDecisionEmail`'s
|
|
410
|
+
* caller.
|
|
411
|
+
*/
|
|
412
|
+
function fileIssueOnApprove(listing: ProjectListing, decision: ListingDecision): void {
|
|
413
|
+
if (decision !== "active") return;
|
|
414
|
+
fileListingIssue(listing).catch((err: unknown) => {
|
|
415
|
+
logger.warn(
|
|
416
|
+
{ listingId: listing.id, errName: err instanceof Error ? err.name : "UnknownError" },
|
|
417
|
+
"listing issue filing rejected unexpectedly",
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ─── #5001 — the approve-path side-effect seam ─────────────────────────────
|
|
423
|
+
|
|
424
|
+
/** The status whose arrival triggers the projection — the approved state. */
|
|
425
|
+
const APPROVED = "active";
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Run the decision's downstream effects for one listing. Called by
|
|
429
|
+
* `patchProjectListing` immediately after a successful transition write;
|
|
430
|
+
* never throws.
|
|
431
|
+
*/
|
|
432
|
+
export async function onListingDecided(
|
|
433
|
+
siteId: string,
|
|
434
|
+
listingId: string,
|
|
435
|
+
toStatus: string,
|
|
436
|
+
): Promise<void> {
|
|
437
|
+
if (toStatus !== APPROVED) return;
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const row = await prisma.projectListing.findFirst({
|
|
441
|
+
where: { id: listingId, siteId },
|
|
442
|
+
select: {
|
|
443
|
+
id: true,
|
|
444
|
+
siteId: true,
|
|
445
|
+
slug: true,
|
|
446
|
+
name: true,
|
|
447
|
+
oneLiner: true,
|
|
448
|
+
questions: true,
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
if (!row) return;
|
|
452
|
+
|
|
453
|
+
// The stored `questions` blob is validated on write (#4998's POST route)
|
|
454
|
+
// — re-parsing here is defense in depth, and the one place a row that
|
|
455
|
+
// predates or drifts from the template is skipped rather than projected
|
|
456
|
+
// into a malformed flow.
|
|
457
|
+
// #5036 D1 — `slug`/`name`/`oneLiner` are nullable at rest so a half-filled
|
|
458
|
+
// draft is storable. A row can only reach `active` through `submit`, which
|
|
459
|
+
// asserts all three, so this discharges a type-level possibility rather
|
|
460
|
+
// than a reachable state — and skips rather than throwing, matching the
|
|
461
|
+
// fail-soft contract this whole seam carries.
|
|
462
|
+
if (row.slug === null || row.name === null || row.oneLiner === null) {
|
|
463
|
+
logger.warn(
|
|
464
|
+
{ siteId, listingId, kind: "incomplete_listing" },
|
|
465
|
+
"[project-listing-decision] skipping flow generation — an approved listing is missing submit-time fields",
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const questions = projectListingQuestionsSchema.safeParse(row.questions);
|
|
471
|
+
if (!questions.success) {
|
|
472
|
+
logger.warn(
|
|
473
|
+
{ siteId, listingId, slug: row.slug, kind: "invalid_questions" },
|
|
474
|
+
"[project-listing-decision] skipping flow generation — stored questions failed schema validation",
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const result = await generateListingFlow({
|
|
480
|
+
id: row.id,
|
|
481
|
+
siteId: row.siteId,
|
|
482
|
+
slug: row.slug,
|
|
483
|
+
name: row.name,
|
|
484
|
+
oneLiner: row.oneLiner,
|
|
485
|
+
questions: questions.data,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (!result.ok) {
|
|
489
|
+
logger.error(
|
|
490
|
+
{ siteId, listingId, slug: row.slug, kind: result.error.kind },
|
|
491
|
+
"[project-listing-decision] flow generation failed — the listing is approved but has no diagnostic",
|
|
492
|
+
);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
logger.info(
|
|
497
|
+
{ siteId, listingId, slug: row.slug, kind: "generated" },
|
|
498
|
+
"[project-listing-decision] diagnostic flow + experiment generated",
|
|
499
|
+
);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
logger.error(
|
|
502
|
+
{
|
|
503
|
+
siteId,
|
|
504
|
+
listingId,
|
|
505
|
+
errKind: err instanceof Error ? err.constructor.name : typeof err,
|
|
506
|
+
},
|
|
507
|
+
"[project-listing-decision] approve hook failed — the transition still succeeded",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|