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,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file playbook-compile.ts
|
|
3
|
+
* @description FEAT-074 U4a (#4715) — compiles a scored `targeting-maturity`
|
|
4
|
+
* signup into a person-scoped Playbook and persists it append-only.
|
|
5
|
+
*
|
|
6
|
+
* `compilePlaybook` is a PURE function (`rules/execution-substrate.md` — the
|
|
7
|
+
* compile is a payload, never a per-respondent model call): given a scored
|
|
8
|
+
* `WaitlistScore` row's facts (`{id, tier, ruleTrace}`), the play library,
|
|
9
|
+
* and the lead id, it returns `{source, markdown}` — no Prisma, no network,
|
|
10
|
+
* no clock, no filesystem read of its own. `source` validates against
|
|
11
|
+
* `vendor/playbook-method/playbook-source.schema.json` (U1) and passes
|
|
12
|
+
* `vendor/playbook-method/check-playbook.py` — see this file's own test
|
|
13
|
+
* suite, which runs the vendored checker against every golden.
|
|
14
|
+
*
|
|
15
|
+
* `compileAndStorePlaybook` is the thin, fail-soft Prisma-touching wrapper —
|
|
16
|
+
* the sibling to A2's `waitlist-scoring.ts` glue. It loads the vendored
|
|
17
|
+
* play library LAZILY, memoized, on first use inside its own try block (a
|
|
18
|
+
* `readFileSync`, not a static `import … from "*.json"` — `apps/api`'s
|
|
19
|
+
* `tsconfig.json` sets `rootDir: "./src"` + `composite: true`, and the
|
|
20
|
+
* vendored library lives at the repo root, outside that boundary;
|
|
21
|
+
* `apps-paid/design-reference-gallery/lib/corpus.ts` is this repo's own
|
|
22
|
+
* precedent for reading vendored repo-root content via `readFileSync`
|
|
23
|
+
* rather than a cross-boundary static import). Deliberately NOT at module
|
|
24
|
+
* scope (security-review finding M1, PR #4727): a missing/corrupt vendored
|
|
25
|
+
* file must degrade like any other compile failure (D4), never throw at
|
|
26
|
+
* import time where no catch in this file can reach it. `compilePlaybook`
|
|
27
|
+
* itself never touches the filesystem.
|
|
28
|
+
*
|
|
29
|
+
* The path is resolved via `process.cwd()`-relative candidate probing
|
|
30
|
+
* (`apps/api/src/lib/locales.ts`'s own `resolveProjectYamlPath` pattern),
|
|
31
|
+
* NOT `import.meta.url` — Jest's ts-jest preset here compiles to CommonJS
|
|
32
|
+
* (`jest.config.js`), which cannot execute `import.meta` (Node throws
|
|
33
|
+
* `SyntaxError: Cannot use 'import.meta' outside a module`); this repo's own
|
|
34
|
+
* `moduleNameMapper` comment on `@working-theory/database` names the exact
|
|
35
|
+
* same v7 prisma-client `import.meta.url` incompatibility and mocks around
|
|
36
|
+
* it. `process.cwd()` has no such split — it resolves correctly whether the
|
|
37
|
+
* process was started from `apps/api/` (dev/build) or from this package's
|
|
38
|
+
* own test run.
|
|
39
|
+
*
|
|
40
|
+
* The unlock map (`unlocks_next`) is LIBRARY-DECLARED, resolved from
|
|
41
|
+
* `levels[level].unlocks_next` — NEVER computed as `level + 1` (the
|
|
42
|
+
* method's map is not monotonic: Level 5 unlocks Level 7, Level 8 unlocks
|
|
43
|
+
* Level 7 — spec §2.1, U3 Clarifications).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { readFileSync } from "node:fs";
|
|
47
|
+
import { join } from "node:path";
|
|
48
|
+
|
|
49
|
+
import type { Prisma } from "@working-theory/database";
|
|
50
|
+
import { logger } from "@working-theory/observability";
|
|
51
|
+
|
|
52
|
+
import { prisma } from "../lib/prisma.js";
|
|
53
|
+
|
|
54
|
+
// ─── Play-library types (the vendored targeting-maturity.plays.json shape) ──
|
|
55
|
+
|
|
56
|
+
export interface PlaybookPlay {
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
text: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlaybookLevelEntry {
|
|
63
|
+
level: number;
|
|
64
|
+
name?: string;
|
|
65
|
+
use_if?: string;
|
|
66
|
+
cautions?: string[];
|
|
67
|
+
plays: PlaybookPlay[];
|
|
68
|
+
unlocks_next: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PlayLibrary {
|
|
72
|
+
version: string;
|
|
73
|
+
method: string;
|
|
74
|
+
/** A bare pointer object at level 0 in the authored shape (D4) — the Gate's
|
|
75
|
+
* own plays live in `levels[]` at `level: 0`; consulted only as a fallback
|
|
76
|
+
* when `levels[]` carries no level-0 entry (mirrors
|
|
77
|
+
* `check-playbook.py`'s `_library_entry_for_level`). */
|
|
78
|
+
gate?: { level: number; plays?: PlaybookPlay[]; unlocks_next?: number | null };
|
|
79
|
+
levels: PlaybookLevelEntry[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── The vendored play library — lazy-loaded, memoized (see header) ─────────
|
|
83
|
+
|
|
84
|
+
const VENDORED_LIBRARY_RELATIVE = "vendor/playbook-method/targeting-maturity.plays.json";
|
|
85
|
+
|
|
86
|
+
/** Probes `[cwd, cwd/.., cwd/../..]` for the vendored library — apps run
|
|
87
|
+
* from their own dir in dev/build (`apps/api/`) but this package's own
|
|
88
|
+
* tests may run from either `apps/api/` or the repo root (mirrors
|
|
89
|
+
* `apps/api/src/lib/locales.ts`'s `resolveProjectYamlPath`). Falls back to
|
|
90
|
+
* the cwd-relative path so a genuine miss's error names the expected
|
|
91
|
+
* location. */
|
|
92
|
+
export function resolveVendoredLibraryPath(cwd = process.cwd()): string {
|
|
93
|
+
const candidates = [
|
|
94
|
+
join(cwd, VENDORED_LIBRARY_RELATIVE),
|
|
95
|
+
join(cwd, "..", VENDORED_LIBRARY_RELATIVE),
|
|
96
|
+
join(cwd, "..", "..", VENDORED_LIBRARY_RELATIVE),
|
|
97
|
+
];
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
try {
|
|
100
|
+
readFileSync(candidate, "utf8");
|
|
101
|
+
return candidate;
|
|
102
|
+
} catch {
|
|
103
|
+
// continue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return candidates[0]!;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function loadVendoredPlayLibrary(): PlayLibrary {
|
|
110
|
+
return JSON.parse(readFileSync(resolveVendoredLibraryPath(), "utf8")) as PlayLibrary;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// LAZY + memoized (security-review finding M1, PR #4727): loading the
|
|
114
|
+
// library at MODULE SCOPE would let a missing/corrupt vendored file throw
|
|
115
|
+
// at import time — outside every try/catch this file adds, and outside the
|
|
116
|
+
// fail-soft contract this whole unit exists to uphold. On a bundler that
|
|
117
|
+
// cannot statically resolve the `process.cwd()`-relative repo-root path
|
|
118
|
+
// (e.g. a serverless target with no `functions.includeFiles` entry for
|
|
119
|
+
// `vendor/**`), that import-time throw would 500 EVERY request through this
|
|
120
|
+
// module's static import chain (`waitlist-scoring.ts` → `flow-engine.ts` →
|
|
121
|
+
// the route layer → the server entrypoint) — not merely fail to compile a
|
|
122
|
+
// Playbook. Deferring the read to first use inside `compileAndStorePlaybook`'s
|
|
123
|
+
// own try block (below) means a missing/corrupt file degrades exactly like
|
|
124
|
+
// any other compile failure (D4: logged, the score is unaffected) instead of
|
|
125
|
+
// taking the process down. Memoized so the read still happens at most once.
|
|
126
|
+
let vendoredPlayLibraryCache: PlayLibrary | undefined;
|
|
127
|
+
function vendoredPlayLibrary(): PlayLibrary {
|
|
128
|
+
if (!vendoredPlayLibraryCache) {
|
|
129
|
+
vendoredPlayLibraryCache = loadVendoredPlayLibrary();
|
|
130
|
+
}
|
|
131
|
+
return vendoredPlayLibraryCache;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The one experiment this compile hooks (D4) — every other experiment's
|
|
135
|
+
* `scoreCompletedSignup` completion never reaches this module. */
|
|
136
|
+
export const TARGETING_MATURITY_EXPERIMENT_KEY = "targeting-maturity";
|
|
137
|
+
|
|
138
|
+
// ─── compilePlaybook — the pure payload (D1) ────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface PlaybookSource {
|
|
141
|
+
artifact_type: "playbook";
|
|
142
|
+
scope: "person";
|
|
143
|
+
scope_ref: string;
|
|
144
|
+
method: string;
|
|
145
|
+
level: number;
|
|
146
|
+
plays: string[];
|
|
147
|
+
unlocks_next?: string[];
|
|
148
|
+
compiled_from: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The WaitlistScore facts `compilePlaybook` reads — a subset of the Prisma
|
|
152
|
+
* row, never the row itself (the pure function takes no Prisma type). */
|
|
153
|
+
export interface CompilePlaybookScoreInput {
|
|
154
|
+
/** `WaitlistScore.id` — becomes `source.compiled_from` (D2). */
|
|
155
|
+
id: string;
|
|
156
|
+
/** `WaitlistScore.tier` — a `level_N` string. */
|
|
157
|
+
tier: string;
|
|
158
|
+
/** `WaitlistScore.ruleTrace` — opaque at this boundary, mirroring
|
|
159
|
+
* `packages/validation`'s own `ruleTrace: z.unknown()` convention; parsed
|
|
160
|
+
* defensively for the Level-1 loop-back framing only (see
|
|
161
|
+
* `decidingRefNames`). */
|
|
162
|
+
ruleTrace: unknown;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface CompilePlaybookInput {
|
|
166
|
+
score: CompilePlaybookScoreInput;
|
|
167
|
+
plays: PlayLibrary;
|
|
168
|
+
leadId: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface CompilePlaybookResult {
|
|
172
|
+
source: PlaybookSource;
|
|
173
|
+
markdown: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A `compilePlaybook` input that cannot be compiled — the library declares
|
|
177
|
+
* no Level for the score's tier, or that Level has no plays (spec §5.3 — the
|
|
178
|
+
* play-library owner's defect, never a compile branch). */
|
|
179
|
+
export class PlaybookCompileError extends Error {}
|
|
180
|
+
|
|
181
|
+
const TIER_LEVEL_RE = /^level_(\d+)$/;
|
|
182
|
+
|
|
183
|
+
/** The library entry for `level` — `levels[]` first, the `gate` pointer as a
|
|
184
|
+
* level-0 fallback only (mirrors `check-playbook.py`'s `_library_entry_for_level`,
|
|
185
|
+
* the #8496 amendment). */
|
|
186
|
+
function levelEntryFor(library: PlayLibrary, level: number): PlaybookLevelEntry | undefined {
|
|
187
|
+
const fromLevels = library.levels.find((entry) => entry.level === level);
|
|
188
|
+
if (fromLevels) return fromLevels;
|
|
189
|
+
if (level === 0 && library.gate?.plays && library.gate.plays.length > 0) {
|
|
190
|
+
return {
|
|
191
|
+
level: 0,
|
|
192
|
+
plays: library.gate.plays,
|
|
193
|
+
unlocks_next: library.gate.unlocks_next ?? null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The `ref(<name>)` def names that DECIDED the matched tier — every
|
|
201
|
+
* `kind: "predicate"` trace entry whose `tier` equals the matched tier AND
|
|
202
|
+
* whose `result` is `true`, with every `ref(...)` segment in its `path`
|
|
203
|
+
* extracted (`RuleTraceEntry.path` embeds each traversed def as
|
|
204
|
+
* `ref(<name>)`, per `evaluatePredicateNode`'s own path-building — there is
|
|
205
|
+
* no separate `def` field to read instead). Duck-typed, not
|
|
206
|
+
* `ruleTraceEntrySchema`-parsed: `ruleTrace` is `unknown` at every boundary
|
|
207
|
+
* this function is reached from (Prisma `Json`, the dashboard's own
|
|
208
|
+
* `z.unknown()`), and this compile never needs the full shape — only
|
|
209
|
+
* whether a specific ref fired true for this tier.
|
|
210
|
+
*/
|
|
211
|
+
function decidingRefNames(ruleTrace: unknown, tier: string): Set<string> {
|
|
212
|
+
const names = new Set<string>();
|
|
213
|
+
if (!Array.isArray(ruleTrace)) return names;
|
|
214
|
+
for (const entry of ruleTrace) {
|
|
215
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
216
|
+
const e = entry as Record<string, unknown>;
|
|
217
|
+
if (e["kind"] !== "predicate" || e["tier"] !== tier || e["result"] !== true) continue;
|
|
218
|
+
const path = e["path"];
|
|
219
|
+
if (typeof path !== "string") continue;
|
|
220
|
+
for (const m of path.matchAll(/ref\(([^)]+)\)/g)) {
|
|
221
|
+
names.add(m[1]!);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return names;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function renderPlaybookMarkdown(args: {
|
|
228
|
+
source: PlaybookSource;
|
|
229
|
+
entry: PlaybookLevelEntry;
|
|
230
|
+
unlockEntry: PlaybookLevelEntry | undefined;
|
|
231
|
+
isLoopBackCohort: boolean;
|
|
232
|
+
}): string {
|
|
233
|
+
const { source, entry, unlockEntry, isLoopBackCohort } = args;
|
|
234
|
+
const heading = entry.name ? `Level ${source.level} — ${entry.name}` : `Level ${source.level}`;
|
|
235
|
+
const lines: string[] = [];
|
|
236
|
+
|
|
237
|
+
lines.push(`# Playbook — ${heading}`, "");
|
|
238
|
+
|
|
239
|
+
if (isLoopBackCohort) {
|
|
240
|
+
lines.push(
|
|
241
|
+
"> You reached Level 1 via the Level-6 loop-back — a confirmed-Fit " +
|
|
242
|
+
"cohort (not paying customers) put you here. Once this Level has " +
|
|
243
|
+
"given you a validated ICP filter, revisit at Level 7 or 8.",
|
|
244
|
+
"",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push(
|
|
249
|
+
"## §1. Identity",
|
|
250
|
+
"",
|
|
251
|
+
`**artifact_type:** \`${source.artifact_type}\``,
|
|
252
|
+
`**scope:** \`${source.scope}\``,
|
|
253
|
+
`**scope_ref:** \`${source.scope_ref}\``,
|
|
254
|
+
`**method:** \`${source.method}\``,
|
|
255
|
+
`**level:** \`${source.level}\``,
|
|
256
|
+
`**compiled_from:** \`${source.compiled_from}\``,
|
|
257
|
+
"",
|
|
258
|
+
"<!-- machine-region -->",
|
|
259
|
+
`<!-- playbook-source:\n${JSON.stringify(source)}\n-->`,
|
|
260
|
+
"<!-- /machine-region -->",
|
|
261
|
+
"",
|
|
262
|
+
"---",
|
|
263
|
+
"",
|
|
264
|
+
"## §2. Plays",
|
|
265
|
+
"",
|
|
266
|
+
`### Level ${source.level}`,
|
|
267
|
+
"",
|
|
268
|
+
);
|
|
269
|
+
for (const play of entry.plays) {
|
|
270
|
+
lines.push(`- \`${play.id}\` — ${play.title}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (unlockEntry && source.unlocks_next) {
|
|
274
|
+
lines.push(
|
|
275
|
+
"",
|
|
276
|
+
"---",
|
|
277
|
+
"",
|
|
278
|
+
"## §3. Unlocks next",
|
|
279
|
+
"",
|
|
280
|
+
`### Level ${unlockEntry.level}`,
|
|
281
|
+
"",
|
|
282
|
+
);
|
|
283
|
+
for (const play of unlockEntry.plays) {
|
|
284
|
+
lines.push(`- \`${play.id}\` — ${play.title}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines.join("\n") + "\n";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Compile a scored `targeting-maturity` signup into a person-scoped
|
|
293
|
+
* Playbook — pure, deterministic, no I/O (D1). Throws `PlaybookCompileError`
|
|
294
|
+
* when the library declares no Level for the score's tier, or that Level
|
|
295
|
+
* has no plays — the caller (`compileAndStorePlaybook`) is fail-soft over
|
|
296
|
+
* this throw (D4, spec §5.3).
|
|
297
|
+
*/
|
|
298
|
+
export function compilePlaybook(input: CompilePlaybookInput): CompilePlaybookResult {
|
|
299
|
+
const { score, plays, leadId } = input;
|
|
300
|
+
|
|
301
|
+
const match = TIER_LEVEL_RE.exec(score.tier);
|
|
302
|
+
if (!match) {
|
|
303
|
+
throw new PlaybookCompileError(
|
|
304
|
+
`cannot compile a Playbook for tier "${score.tier}" — not a level_N tier`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const level = Number(match[1]);
|
|
308
|
+
|
|
309
|
+
const entry = levelEntryFor(plays, level);
|
|
310
|
+
if (!entry || entry.plays.length === 0) {
|
|
311
|
+
throw new PlaybookCompileError(
|
|
312
|
+
`the play library declares no plays for Level ${level} — a library defect, not a compile branch (spec §5.3)`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let unlockEntry: PlaybookLevelEntry | undefined;
|
|
317
|
+
let unlockIds: string[] | undefined;
|
|
318
|
+
if (entry.unlocks_next !== null && entry.unlocks_next !== undefined) {
|
|
319
|
+
unlockEntry = levelEntryFor(plays, entry.unlocks_next);
|
|
320
|
+
if (unlockEntry && unlockEntry.plays.length > 0) {
|
|
321
|
+
unlockIds = unlockEntry.plays.map((play) => play.id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const source: PlaybookSource = {
|
|
326
|
+
artifact_type: "playbook",
|
|
327
|
+
scope: "person",
|
|
328
|
+
scope_ref: leadId,
|
|
329
|
+
method: plays.method,
|
|
330
|
+
level,
|
|
331
|
+
plays: entry.plays.map((play) => play.id),
|
|
332
|
+
...(unlockIds ? { unlocks_next: unlockIds } : {}),
|
|
333
|
+
compiled_from: score.id,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const isLoopBackCohort =
|
|
337
|
+
level === 1 && decidingRefNames(score.ruleTrace, score.tier).has("has_confirmed_fit_cohort");
|
|
338
|
+
|
|
339
|
+
const markdown = renderPlaybookMarkdown({ source, entry, unlockEntry, isLoopBackCohort });
|
|
340
|
+
|
|
341
|
+
return { source, markdown };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── compileAndStorePlaybook — the fail-soft Prisma-touching wrapper (D4) ───
|
|
345
|
+
|
|
346
|
+
export interface CompileAndStorePlaybookInput {
|
|
347
|
+
siteId: string;
|
|
348
|
+
leadId: string;
|
|
349
|
+
experimentKey: string;
|
|
350
|
+
scoreId: string;
|
|
351
|
+
tier: string;
|
|
352
|
+
ruleTrace: unknown;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Compile + append a `PlaybookInstance` row. Fail-soft over BOTH the pure
|
|
357
|
+
* compile (`PlaybookCompileError` or any other throw) and the Prisma write —
|
|
358
|
+
* a compile/store failure is logged and never fails the signup or the score
|
|
359
|
+
* (D4). The caller (`waitlist-scoring.ts`) awaits this unconditionally; it
|
|
360
|
+
* never throws.
|
|
361
|
+
*/
|
|
362
|
+
export async function compileAndStorePlaybook(
|
|
363
|
+
input: CompileAndStorePlaybookInput,
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
const { siteId, leadId, experimentKey, scoreId, tier, ruleTrace } = input;
|
|
366
|
+
try {
|
|
367
|
+
const { source, markdown } = compilePlaybook({
|
|
368
|
+
score: { id: scoreId, tier, ruleTrace },
|
|
369
|
+
plays: vendoredPlayLibrary(),
|
|
370
|
+
leadId,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await prisma.playbookInstance.create({
|
|
374
|
+
data: {
|
|
375
|
+
siteId,
|
|
376
|
+
leadId,
|
|
377
|
+
experimentKey,
|
|
378
|
+
scoreId,
|
|
379
|
+
tier,
|
|
380
|
+
level: source.level,
|
|
381
|
+
ruleTrace: (ruleTrace ?? null) as unknown as Prisma.InputJsonValue,
|
|
382
|
+
source: source as unknown as Prisma.InputJsonValue,
|
|
383
|
+
markdown,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
} catch (err) {
|
|
387
|
+
logger.error(
|
|
388
|
+
{
|
|
389
|
+
siteId,
|
|
390
|
+
experimentKey,
|
|
391
|
+
leadId,
|
|
392
|
+
kind: "playbook_compile_failed",
|
|
393
|
+
errKind: err instanceof Error ? err.constructor.name : typeof err,
|
|
394
|
+
},
|
|
395
|
+
"[playbook-compile] compile/store failed — the score is unaffected",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file playbook-render.ts
|
|
3
|
+
* @description FEAT-074 U4b (#4718) — renders a stored `PlaybookInstance` to
|
|
4
|
+
* a customer-facing PDF, on request.
|
|
5
|
+
*
|
|
6
|
+
* `renderPlaybookPdf({source, markdown, locale})` is a pure-TypeScript
|
|
7
|
+
* renderer (D2, spec §3): a small typed template lays out the Level/Gate
|
|
8
|
+
* heading, the Level's bound plays (title + body, resolved from `source`'s
|
|
9
|
+
* `plays`/`unlocks_next` id lists against the LIVE vendored play library —
|
|
10
|
+
* D1's "a template or renderer fix improves every already-issued Playbook"
|
|
11
|
+
* intent, so this deliberately reads the library fresh at render time
|
|
12
|
+
* rather than trusting anything frozen into the stored `markdown` snapshot),
|
|
13
|
+
* and the unlocks-next block. `markdown` is accepted per the spec's literal
|
|
14
|
+
* `{source, markdown, locale}` signature (matching `compilePlaybook`'s own
|
|
15
|
+
* `{source, markdown}` output pair, the exact shape `PlaybookInstance`
|
|
16
|
+
* persists) but is NOT parsed for content in this unit — `source`'s id
|
|
17
|
+
* lists plus a live library lookup already carry everything §2.1 requires
|
|
18
|
+
* (the Level label, the bound plays, the unlocks-next block); reserved for
|
|
19
|
+
* a future direct-render use.
|
|
20
|
+
*
|
|
21
|
+
* Renderer choice (D2, recorded per the dispatch's own requirement): PDF.js
|
|
22
|
+
* (mozilla's) sibling project `pdf-lib` (v1.17.1, MIT) — pure TypeScript,
|
|
23
|
+
* zero native dependencies (its own runtime deps —
|
|
24
|
+
* `@pdf-lib/standard-fonts`, `@pdf-lib/upng`, `pako`, `tslib` — are all pure
|
|
25
|
+
* JS/TS too), no headless browser, no postinstall download. It embeds the
|
|
26
|
+
* 14 PDF standard fonts (Helvetica here) with no external font files, and
|
|
27
|
+
* exposes `font.widthOfTextAtSize` for the manual word-wrap this file
|
|
28
|
+
* implements (pdf-lib has no built-in text-flow/wrap primitive — a small
|
|
29
|
+
* typed template has no need for one).
|
|
30
|
+
*
|
|
31
|
+
* A play id from `source.plays`/`source.unlocks_next` that the CURRENT
|
|
32
|
+
* vendored library no longer declares is a library defect, never a render
|
|
33
|
+
* branch — mirrors `playbook-compile.ts`'s own `PlaybookCompileError`
|
|
34
|
+
* posture ("a library defect, not a compile branch") applied symmetrically
|
|
35
|
+
* to render.
|
|
36
|
+
*
|
|
37
|
+
* Every CHROME label (the Level/Gate heading text, the "Plays"/"Unlocks
|
|
38
|
+
* next" section titles) is a `LocalizedString` resolved via
|
|
39
|
+
* `resolveLocalizedString` (`@working-theory/validation`, the ONE shared
|
|
40
|
+
* resolution rule — flow-i18n.ts's own header) against the request
|
|
41
|
+
* `locale`. Play TITLES/BODIES are read straight from the vendored library,
|
|
42
|
+
* which carries no `LocalizedString` shape of its own (English-only at the
|
|
43
|
+
* source, the same convention `compilePlaybook`'s stored `markdown` already
|
|
44
|
+
* uses) — the i18n requirement (spec §2.1) scopes to the renderer's own
|
|
45
|
+
* chrome, not to library content. `renderPlaybookPdf`'s fixed
|
|
46
|
+
* `{source, markdown, locale}` signature carries no separate flow
|
|
47
|
+
* `defaultLocale` field, so this file's label table declares `en` as its
|
|
48
|
+
* sole `defaultLocale` (a module constant, `LABEL_DEFAULT_LOCALE` below) —
|
|
49
|
+
* every label below currently declares only `en`, so this is presently a
|
|
50
|
+
* no-op fallback, kept for structural correctness against a future
|
|
51
|
+
* multi-locale label table.
|
|
52
|
+
*
|
|
53
|
+
* The Gate (Level 0) is labelled "Gate," never "Level 0" (spec §2.1's
|
|
54
|
+
* never-say-Level-0 rule — mirrors the operator dashboard's own
|
|
55
|
+
* `waitlistDashboard.level.gate` key, U4a D5, and the AC-1 Gate filename
|
|
56
|
+
* convention `playbook-gate.pdf`).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { resolveLocalizedString } from "@working-theory/validation";
|
|
60
|
+
import { PDFDocument, PageSizes, StandardFonts, type PDFFont, type PDFPage } from "pdf-lib";
|
|
61
|
+
|
|
62
|
+
import { loadVendoredPlayLibrary, type PlaybookPlay, type PlaybookSource, type PlayLibrary } from "./playbook-compile.js";
|
|
63
|
+
|
|
64
|
+
// ─── Chrome labels — the CUSTOMER-FACING structural text (spec §2.1) ──────
|
|
65
|
+
|
|
66
|
+
const LABEL_DEFAULT_LOCALE = "en";
|
|
67
|
+
|
|
68
|
+
const LABELS = {
|
|
69
|
+
documentTitle: { en: "Your Playbook" },
|
|
70
|
+
gate: { en: "Gate" },
|
|
71
|
+
/** `{n}` is a literal placeholder — substituted after resolution. */
|
|
72
|
+
levelLabel: { en: "Level {n}" },
|
|
73
|
+
plays: { en: "Plays" },
|
|
74
|
+
unlocksNext: { en: "Unlocks next" },
|
|
75
|
+
} as const satisfies Record<string, Record<string, string>>;
|
|
76
|
+
|
|
77
|
+
function label(key: keyof typeof LABELS, locale: string): string {
|
|
78
|
+
return resolveLocalizedString(LABELS[key], locale, LABEL_DEFAULT_LOCALE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Level 0 is "Gate," never "Level 0" (spec §2.1 — the never-say-Level-0 rule). */
|
|
82
|
+
function levelHeading(level: number, locale: string): string {
|
|
83
|
+
if (level === 0) return label("gate", locale);
|
|
84
|
+
return label("levelLabel", locale).replace("{n}", String(level));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── The vendored play library — a per-play id index ───────────────────────
|
|
88
|
+
|
|
89
|
+
interface IndexedPlay {
|
|
90
|
+
play: PlaybookPlay;
|
|
91
|
+
level: number;
|
|
92
|
+
levelName: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A `source.plays`/`source.unlocks_next` id the CURRENT vendored library no
|
|
96
|
+
* longer declares — a library defect, never a render branch (mirrors
|
|
97
|
+
* `PlaybookCompileError`'s identical posture in `playbook-compile.ts`). */
|
|
98
|
+
export class PlaybookRenderError extends Error {}
|
|
99
|
+
|
|
100
|
+
/** Flattens every Level's `plays[]` into one id-keyed index, carrying each
|
|
101
|
+
* play's owning Level number + name — so an unlocks-next play id resolves
|
|
102
|
+
* both its own title/text AND which Level it belongs to, with no need to
|
|
103
|
+
* parse a `tm-<level>.<n>` id-format convention the schema does not
|
|
104
|
+
* guarantee. */
|
|
105
|
+
function buildPlayIndex(library: PlayLibrary): Map<string, IndexedPlay> {
|
|
106
|
+
const index = new Map<string, IndexedPlay>();
|
|
107
|
+
for (const entry of library.levels) {
|
|
108
|
+
for (const play of entry.plays) {
|
|
109
|
+
index.set(play.id, { play, level: entry.level, levelName: entry.name });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return index;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolvePlays(index: Map<string, IndexedPlay>, ids: string[]): IndexedPlay[] {
|
|
116
|
+
return ids.map((id) => {
|
|
117
|
+
const entry = index.get(id);
|
|
118
|
+
if (!entry) {
|
|
119
|
+
throw new PlaybookRenderError(
|
|
120
|
+
`the vendored play library declares no play "${id}" — a library defect, never a render branch (mirrors playbook-compile.ts's PlaybookCompileError posture)`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return entry;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Word wrap + pagination (pdf-lib has no built-in text-flow primitive) ──
|
|
128
|
+
|
|
129
|
+
const PAGE_SIZE = PageSizes.Letter; // [612, 792]
|
|
130
|
+
const MARGIN = 54; // 0.75in
|
|
131
|
+
const CONTENT_WIDTH = PAGE_SIZE[0] - MARGIN * 2;
|
|
132
|
+
const TOP_Y = PAGE_SIZE[1] - MARGIN;
|
|
133
|
+
const BOTTOM_Y = MARGIN;
|
|
134
|
+
|
|
135
|
+
/** Greedy word-wrap against a font's measured width — pdf-lib exposes
|
|
136
|
+
* `font.widthOfTextAtSize` but no wrap primitive of its own. */
|
|
137
|
+
function wrapLine(text: string, font: PDFFont, size: number, maxWidth: number): string[] {
|
|
138
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
let current = "";
|
|
141
|
+
for (const word of words) {
|
|
142
|
+
const candidate = current.length > 0 ? `${current} ${word}` : word;
|
|
143
|
+
if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
|
|
144
|
+
current = candidate;
|
|
145
|
+
} else {
|
|
146
|
+
if (current.length > 0) lines.push(current);
|
|
147
|
+
current = word;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (current.length > 0) lines.push(current);
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface PdfWriter {
|
|
155
|
+
heading(text: string, size: number): void;
|
|
156
|
+
subheading(text: string, size: number): void;
|
|
157
|
+
paragraph(text: string, size: number): void;
|
|
158
|
+
spacer(height: number): void;
|
|
159
|
+
save(): Promise<Uint8Array>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A closure-scoped writer (`@working-theory/design-system/no-undisciplined-class`
|
|
163
|
+
* forbids `this`-bearing stateful classes) — the mutable page/y cursor lives
|
|
164
|
+
* in this function's own local variables, never on an instance. */
|
|
165
|
+
async function createPdfWriter(): Promise<PdfWriter> {
|
|
166
|
+
const doc = await PDFDocument.create();
|
|
167
|
+
const regular = await doc.embedFont(StandardFonts.Helvetica);
|
|
168
|
+
const bold = await doc.embedFont(StandardFonts.HelveticaBold);
|
|
169
|
+
|
|
170
|
+
let page: PDFPage = doc.addPage(PAGE_SIZE);
|
|
171
|
+
let y = TOP_Y;
|
|
172
|
+
|
|
173
|
+
function ensureSpace(height: number): void {
|
|
174
|
+
if (y - height < BOTTOM_Y) {
|
|
175
|
+
page = doc.addPage(PAGE_SIZE);
|
|
176
|
+
y = TOP_Y;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawLines(lines: string[], font: PDFFont, size: number, lineHeight: number): void {
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
ensureSpace(lineHeight);
|
|
183
|
+
page.drawText(line, { x: MARGIN, y: y - size, size, font });
|
|
184
|
+
y -= lineHeight;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
heading(text, size) {
|
|
190
|
+
ensureSpace(size * 1.6 + 8);
|
|
191
|
+
drawLines(wrapLine(text, bold, size, CONTENT_WIDTH), bold, size, size * 1.4);
|
|
192
|
+
y -= 8;
|
|
193
|
+
},
|
|
194
|
+
subheading(text, size) {
|
|
195
|
+
ensureSpace(size * 1.6 + 6);
|
|
196
|
+
drawLines(wrapLine(text, bold, size, CONTENT_WIDTH), bold, size, size * 1.35);
|
|
197
|
+
y -= 6;
|
|
198
|
+
},
|
|
199
|
+
paragraph(text, size) {
|
|
200
|
+
drawLines(wrapLine(text, regular, size, CONTENT_WIDTH), regular, size, size * 1.4);
|
|
201
|
+
y -= 6;
|
|
202
|
+
},
|
|
203
|
+
spacer(height) {
|
|
204
|
+
y -= height;
|
|
205
|
+
},
|
|
206
|
+
async save() {
|
|
207
|
+
return doc.save();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── renderPlaybookPdf ───────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
export interface RenderPlaybookPdfInput {
|
|
215
|
+
source: PlaybookSource;
|
|
216
|
+
/** Accepted per the spec's literal signature; not parsed for content in
|
|
217
|
+
* this unit — see the file header. */
|
|
218
|
+
markdown: string;
|
|
219
|
+
locale: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Renders a stored `PlaybookInstance` to a PDF: the Level/Gate heading, the
|
|
224
|
+
* Level's bound plays (title + body from the LIVE vendored library), and
|
|
225
|
+
* the unlocks-next block (spec §2.1). Throws `PlaybookRenderError` when the
|
|
226
|
+
* current library no longer declares a play `source` names — a library
|
|
227
|
+
* defect, never a render branch.
|
|
228
|
+
*/
|
|
229
|
+
export async function renderPlaybookPdf(input: RenderPlaybookPdfInput): Promise<Uint8Array> {
|
|
230
|
+
const { source, locale } = input;
|
|
231
|
+
|
|
232
|
+
const library = loadVendoredPlayLibrary();
|
|
233
|
+
const index = buildPlayIndex(library);
|
|
234
|
+
|
|
235
|
+
const currentPlays = resolvePlays(index, source.plays);
|
|
236
|
+
const unlockPlays = source.unlocks_next ? resolvePlays(index, source.unlocks_next) : [];
|
|
237
|
+
|
|
238
|
+
const writer = await createPdfWriter();
|
|
239
|
+
|
|
240
|
+
writer.heading(label("documentTitle", locale), 20);
|
|
241
|
+
writer.subheading(levelHeading(source.level, locale), 15);
|
|
242
|
+
writer.spacer(4);
|
|
243
|
+
|
|
244
|
+
writer.subheading(label("plays", locale), 13);
|
|
245
|
+
renderPlays(writer, currentPlays);
|
|
246
|
+
|
|
247
|
+
if (unlockPlays.length > 0) {
|
|
248
|
+
writer.spacer(10);
|
|
249
|
+
const unlockLevel = unlockPlays[0]!.level;
|
|
250
|
+
writer.subheading(`${label("unlocksNext", locale)} — ${levelHeading(unlockLevel, locale)}`, 13);
|
|
251
|
+
renderPlays(writer, unlockPlays);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return writer.save();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderPlays(writer: PdfWriter, plays: IndexedPlay[]): void {
|
|
258
|
+
for (const { play } of plays) {
|
|
259
|
+
writer.paragraph(`• ${play.title}`, 11);
|
|
260
|
+
writer.paragraph(play.text, 10);
|
|
261
|
+
writer.spacer(4);
|
|
262
|
+
}
|
|
263
|
+
}
|