create-ailk 0.4.0 → 0.6.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 +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `scanSitePages` (apps/mcp/src/lib/audit-single-page.ts), exercised
|
|
3
|
+
* through `buildSinglePageAuditInput` with a mocked `ContentAdapter` (#6012,
|
|
4
|
+
* a follow-up from the #6010 review).
|
|
5
|
+
*
|
|
6
|
+
* `scanSitePages` treats two conditions differently, and this file is the
|
|
7
|
+
* test that proves it:
|
|
8
|
+
*
|
|
9
|
+
* - a type the adapter has no pages of resolves NORMALLY to `[]` — never an
|
|
10
|
+
* error, never logged. This is the ordinary "site has nothing of this
|
|
11
|
+
* type" case every real adapter (MDX, database) takes today.
|
|
12
|
+
* - a type whose adapter call THROWS is a genuine failure. It must not take
|
|
13
|
+
* down the audit (the other four reads still complete and their pages
|
|
14
|
+
* still count), but it must be visible — logged via `console.error`
|
|
15
|
+
* (stderr, NOT stdout — the default stdio MCP transport's JSON-RPC
|
|
16
|
+
* framing channel, #6012 round 2) — rather than silently collapsing into
|
|
17
|
+
* the same vacuous "nothing found" result.
|
|
18
|
+
*
|
|
19
|
+
* The call-count assertions below prove that all five reads still ran despite
|
|
20
|
+
* the throw — the read AFTER the failing one is not skipped, which a
|
|
21
|
+
* sequential loop that ABORTED on the first throw would fail to show. They
|
|
22
|
+
* are NOT, on their own, proof of CONCURRENCY: the prior sequential `for`
|
|
23
|
+
* loop this replaced used `continue` (not a re-throw), so it too issued all
|
|
24
|
+
* five calls. The `Promise.all` in the source is what delivers the
|
|
25
|
+
* concurrency AC; these assertions cover the narrower, still load-bearing
|
|
26
|
+
* property that a throw part-way through never truncates the scan.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const mockResolveContentAdapter = jest.fn();
|
|
30
|
+
|
|
31
|
+
jest.mock("@working-theory/content-adapters", () => ({
|
|
32
|
+
...jest.requireActual("@working-theory/content-adapters"),
|
|
33
|
+
resolveContentAdapter: (...args: unknown[]) =>
|
|
34
|
+
mockResolveContentAdapter(...args),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import type { ContentPage } from "@working-theory/content-adapters";
|
|
38
|
+
|
|
39
|
+
import { buildSinglePageAuditInput } from "../../src/lib/audit-single-page.js";
|
|
40
|
+
|
|
41
|
+
const HOME_PAGE: ContentPage = {
|
|
42
|
+
type: "HomePage",
|
|
43
|
+
slug: "home",
|
|
44
|
+
title: "Home",
|
|
45
|
+
description: "The home page.",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PRODUCT_PAGE: ContentPage = {
|
|
49
|
+
type: "Product",
|
|
50
|
+
slug: "widget",
|
|
51
|
+
title: "Widget",
|
|
52
|
+
description: "A widget for sale.",
|
|
53
|
+
price: 49,
|
|
54
|
+
priceCurrency: "USD",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () => {
|
|
58
|
+
const previous = process.env.CONTENT_ADAPTER;
|
|
59
|
+
let errorSpy: jest.SpyInstance;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mockResolveContentAdapter.mockReset();
|
|
63
|
+
process.env.CONTENT_ADAPTER = "mdx";
|
|
64
|
+
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (previous === undefined) delete process.env.CONTENT_ADAPTER;
|
|
69
|
+
else process.env.CONTENT_ADAPTER = previous;
|
|
70
|
+
errorSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a type the adapter legitimately has no pages of resolves silently — nothing logged", async () => {
|
|
74
|
+
const listPages = jest.fn().mockResolvedValue([]);
|
|
75
|
+
const listDetail = jest.fn().mockResolvedValue([]);
|
|
76
|
+
mockResolveContentAdapter.mockReturnValue({
|
|
77
|
+
getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
|
|
78
|
+
listPages,
|
|
79
|
+
listDetail,
|
|
80
|
+
getDetailBySlug: jest.fn(),
|
|
81
|
+
searchPages: jest.fn(),
|
|
82
|
+
extractStructuredData: jest.fn(),
|
|
83
|
+
createPage: jest.fn(),
|
|
84
|
+
updatePage: jest.fn(),
|
|
85
|
+
deletePage: jest.fn(),
|
|
86
|
+
getPageVersion: jest.fn(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await buildSinglePageAuditInput({
|
|
90
|
+
slug: "home",
|
|
91
|
+
type: "HomePage",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.readiness!.sitePages).toEqual([]);
|
|
95
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
96
|
+
// All five reads still ran — two listDetail (Product, Service), three
|
|
97
|
+
// listPages (the three policy types).
|
|
98
|
+
expect(listDetail).toHaveBeenCalledTimes(2);
|
|
99
|
+
expect(listPages).toHaveBeenCalledTimes(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("a type whose adapter call throws is logged to stderr and does not fail the audit", async () => {
|
|
103
|
+
const listPages = jest.fn().mockResolvedValue([]);
|
|
104
|
+
const listDetail = jest.fn().mockImplementation((type: string) => {
|
|
105
|
+
if (type === "Service") {
|
|
106
|
+
throw new Error("simulated adapter failure");
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve([PRODUCT_PAGE]);
|
|
109
|
+
});
|
|
110
|
+
mockResolveContentAdapter.mockReturnValue({
|
|
111
|
+
getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
|
|
112
|
+
listPages,
|
|
113
|
+
listDetail,
|
|
114
|
+
getDetailBySlug: jest.fn(),
|
|
115
|
+
searchPages: jest.fn(),
|
|
116
|
+
extractStructuredData: jest.fn(),
|
|
117
|
+
createPage: jest.fn(),
|
|
118
|
+
updatePage: jest.fn(),
|
|
119
|
+
deletePage: jest.fn(),
|
|
120
|
+
getPageVersion: jest.fn(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = await buildSinglePageAuditInput({
|
|
124
|
+
slug: "home",
|
|
125
|
+
type: "HomePage",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// The audit as a whole never fails, and the type that threw contributes
|
|
129
|
+
// nothing — but the type that succeeded (Product) still counts.
|
|
130
|
+
expect(result.readiness!.sitePages).toEqual([
|
|
131
|
+
{ slug: "widget", type: "Product", price: 49 },
|
|
132
|
+
]);
|
|
133
|
+
// The throwing type is visible, not silently swallowed — and via
|
|
134
|
+
// console.error (stderr), never console.debug/console.log (stdout,
|
|
135
|
+
// the stdio transport's JSON-RPC channel).
|
|
136
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(errorSpy.mock.calls[0]![0]).toMatch(/Service/);
|
|
138
|
+
// All five reads still ran despite the throw — the read AFTER the
|
|
139
|
+
// throwing one (ReturnsPolicyPage, et al.) was not skipped.
|
|
140
|
+
expect(listDetail).toHaveBeenCalledTimes(2);
|
|
141
|
+
expect(listPages).toHaveBeenCalledTimes(3);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the boot-memoised commerce claim (#5971 spec D2).
|
|
3
|
+
*
|
|
4
|
+
* Idiom: `__tests__/lib/require-paid-tier.test.ts` — `checkLicense()` is
|
|
5
|
+
* injected, never called for real, so no suite reaches a licence server
|
|
6
|
+
* (NFR-6). Every arm that cannot POSITIVELY establish the claim must resolve
|
|
7
|
+
* to `"unclaimed"` (NFR-3, fail closed).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
initMcpCommerceClaim,
|
|
12
|
+
mcpCommerceClaim,
|
|
13
|
+
resetMcpCommerceClaim,
|
|
14
|
+
__setMcpCommerceClaimForTest,
|
|
15
|
+
} from "../../src/lib/commerce-claim.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The injected licence reader. Typed off `initMcpCommerceClaim`'s own option
|
|
19
|
+
* rather than off `@working-theory/license`, so the stubs below satisfy
|
|
20
|
+
* exactly what the function under test calls — no double assertion needed.
|
|
21
|
+
*/
|
|
22
|
+
type CheckLicenseSeam = NonNullable<
|
|
23
|
+
NonNullable<Parameters<typeof initMcpCommerceClaim>[0]>["checkLicense"]
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
type LicenseShape = Awaited<ReturnType<CheckLicenseSeam>>;
|
|
27
|
+
|
|
28
|
+
/** A licence reader that resolves to the given result. */
|
|
29
|
+
function resolving(result: Partial<LicenseShape>): CheckLicenseSeam {
|
|
30
|
+
return () => Promise.resolve(result as LicenseShape);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A licence reader that rejects — the "an exception is a bug" arm. */
|
|
34
|
+
function rejecting(): CheckLicenseSeam {
|
|
35
|
+
return () => Promise.reject(new Error("boom"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
resetMcpCommerceClaim();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("initMcpCommerceClaim", () => {
|
|
43
|
+
it("resolves claimed when features[] carries 'commerce'", async () => {
|
|
44
|
+
const claim = await initMcpCommerceClaim({
|
|
45
|
+
checkLicense: resolving({ tier: "pro", features: ["commerce"] }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(claim).toBe("claimed");
|
|
49
|
+
expect(mcpCommerceClaim()).toBe("claimed");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("resolves unclaimed when features[] omits 'commerce'", async () => {
|
|
53
|
+
await initMcpCommerceClaim({
|
|
54
|
+
checkLicense: resolving({ tier: "pro", features: ["scheduling"] }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("resolves unclaimed on a free tier with no features", async () => {
|
|
61
|
+
await initMcpCommerceClaim({
|
|
62
|
+
checkLicense: resolving({ tier: "free", features: [] }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("resolves unclaimed when the licence is revoked, even carrying the feature", async () => {
|
|
69
|
+
await initMcpCommerceClaim({
|
|
70
|
+
checkLicense: resolving({
|
|
71
|
+
tier: "pro",
|
|
72
|
+
features: ["commerce"],
|
|
73
|
+
revoked: true,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("resolves unclaimed — never throws — when checkLicense() rejects", async () => {
|
|
81
|
+
const claim = await initMcpCommerceClaim({
|
|
82
|
+
checkLicense: rejecting(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(claim).toBe("unclaimed");
|
|
86
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("re-resolves and re-memoises on a second call", async () => {
|
|
90
|
+
await initMcpCommerceClaim({
|
|
91
|
+
checkLicense: resolving({ tier: "pro", features: ["commerce"] }),
|
|
92
|
+
});
|
|
93
|
+
expect(mcpCommerceClaim()).toBe("claimed");
|
|
94
|
+
|
|
95
|
+
await initMcpCommerceClaim({
|
|
96
|
+
checkLicense: resolving({ tier: "free", features: [] }),
|
|
97
|
+
});
|
|
98
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("mcpCommerceClaim — fail-closed initial state", () => {
|
|
103
|
+
it("is unclaimed before any init", () => {
|
|
104
|
+
expect(mcpCommerceClaim()).toBe("unclaimed");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("the test injector sets it directly", () => {
|
|
108
|
+
__setMcpCommerceClaimForTest("claimed");
|
|
109
|
+
expect(mcpCommerceClaim()).toBe("claimed");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP ↔ API content parity through the one resolver (#5805, Epic #5322 C1).
|
|
3
|
+
*
|
|
4
|
+
* `apps/api` resolves its adapter with `resolveContentAdapter()`
|
|
5
|
+
* (`apps/api/src/lib/content-adapter.ts`); after this change `apps/mcp` does
|
|
6
|
+
* too, via `getMcpContentAdapter()`. This test proves the two consumers read
|
|
7
|
+
* the same page for the same slug when pointed at the same content root — the
|
|
8
|
+
* whole point of one-function resolution.
|
|
9
|
+
*
|
|
10
|
+
* It resolves the "API side" by calling `resolveContentAdapter()` directly
|
|
11
|
+
* with the shape `apps/api` passes, rather than importing `apps/api` — an
|
|
12
|
+
* app-to-app import is banned by the architecture boundary. The assertion is
|
|
13
|
+
* deliberately small: this is a proof of one-function resolution, not a second
|
|
14
|
+
* content-contract suite.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
resolveContentAdapter,
|
|
21
|
+
resetResolvedContentAdapter,
|
|
22
|
+
} from "@working-theory/content-adapters";
|
|
23
|
+
|
|
24
|
+
import { resolveLocale } from "../../src/lib/resolve-locale.js";
|
|
25
|
+
import { ailkReadContent } from "../../src/tools/read_content.js";
|
|
26
|
+
|
|
27
|
+
const REPO_ROOT = resolve(__dirname, "../../../..");
|
|
28
|
+
const CONTENT_ROOT = join(REPO_ROOT, "content");
|
|
29
|
+
|
|
30
|
+
describe("read_content ↔ apps/api adapter parity (CONTENT_ADAPTER=mdx)", () => {
|
|
31
|
+
const previous = process.env.CONTENT_ADAPTER;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
process.env.CONTENT_ADAPTER = "mdx";
|
|
35
|
+
resetResolvedContentAdapter();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (previous === undefined) delete process.env.CONTENT_ADAPTER;
|
|
40
|
+
else process.env.CONTENT_ADAPTER = previous;
|
|
41
|
+
resetResolvedContentAdapter();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns the same title and slug the API's adapter returns", async () => {
|
|
45
|
+
const viaMcp = await ailkReadContent(
|
|
46
|
+
{ type: "HomePage", slug: "home" },
|
|
47
|
+
CONTENT_ROOT,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// The shape apps/api hands the resolver: a content root and the project's
|
|
51
|
+
// default locale, no workspaceId. Both sides read `project.yaml` for that
|
|
52
|
+
// locale, so resolving it once here keeps the two calls comparable.
|
|
53
|
+
const locale = resolveLocale(undefined);
|
|
54
|
+
const apiAdapter = resolveContentAdapter({
|
|
55
|
+
contentRoot: CONTENT_ROOT,
|
|
56
|
+
defaultLocale: locale,
|
|
57
|
+
});
|
|
58
|
+
const viaApi = await apiAdapter.getPageBySlug("HomePage", "home", locale);
|
|
59
|
+
|
|
60
|
+
expect(viaMcp).not.toBeNull();
|
|
61
|
+
expect(viaApi).not.toBeNull();
|
|
62
|
+
expect(viaMcp!.slug).toBe(viaApi!.slug);
|
|
63
|
+
expect(viaMcp!.frontmatter.title).toBe(viaApi!.title);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shared MCP content-adapter helper (#5805, extended #5876).
|
|
3
|
+
*
|
|
4
|
+
* Epic #5322 exit criterion 2: every content consumer resolves its adapter
|
|
5
|
+
* through one function. These tests pin the delegation contract of
|
|
6
|
+
* `getMcpContentAdapter()` — the single seam the twelve MCP tools and
|
|
7
|
+
* `audit-single-page` now go through:
|
|
8
|
+
*
|
|
9
|
+
* - it delegates to `resolveContentAdapter()` rather than constructing an
|
|
10
|
+
* adapter itself;
|
|
11
|
+
* - it forwards the caller's `contentRoot` test-override verbatim;
|
|
12
|
+
* - it forwards the caller's resolved `locale` as `defaultLocale`;
|
|
13
|
+
* - it defaults `contentRoot` to `<cwd>/content` when the caller passes none;
|
|
14
|
+
* - its MDX-narrowed twin `getMcpMdxContentAdapter()` (which `read_content`
|
|
15
|
+
* needs for `getPageSource()`) resolves the same way and raises a named
|
|
16
|
+
* error rather than a TypeError when the selected adapter is not MDX;
|
|
17
|
+
* - on `CONTENT_ADAPTER=mdx` (the default, unset), it passes NO `workspaceId`;
|
|
18
|
+
* - on `CONTENT_ADAPTER=database`, it reads `mcpPublicWorkspace()` (#5876
|
|
19
|
+
* spec D2/D4/D5/D6): a resolved workspace forwards `workspaceId` + `prisma`
|
|
20
|
+
* to `resolveContentAdapter()`; an unresolved one calls the resolver NOT AT
|
|
21
|
+
* ALL and returns the fail-closed empty adapter instead.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
const mockResolveContentAdapter = jest.fn();
|
|
27
|
+
|
|
28
|
+
jest.mock("@working-theory/content-adapters", () => ({
|
|
29
|
+
...jest.requireActual("@working-theory/content-adapters"),
|
|
30
|
+
resolveContentAdapter: (...args: unknown[]) =>
|
|
31
|
+
mockResolveContentAdapter(...args),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import type { ArtifactPrismaClient } from "@working-theory/content-adapters";
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
getMcpContentAdapter,
|
|
38
|
+
getMcpMdxContentAdapter,
|
|
39
|
+
} from "../../src/lib/content-adapter.js";
|
|
40
|
+
import {
|
|
41
|
+
__setMcpPublicWorkspaceForTest,
|
|
42
|
+
resetMcpPublicWorkspace,
|
|
43
|
+
} from "../../src/lib/public-workspace.js";
|
|
44
|
+
|
|
45
|
+
describe("getMcpContentAdapter", () => {
|
|
46
|
+
const previous = process.env.CONTENT_ADAPTER;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockResolveContentAdapter.mockReset();
|
|
50
|
+
mockResolveContentAdapter.mockReturnValue({ marker: "resolved" });
|
|
51
|
+
process.env.CONTENT_ADAPTER = "mdx";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
if (previous === undefined) delete process.env.CONTENT_ADAPTER;
|
|
56
|
+
else process.env.CONTENT_ADAPTER = previous;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("delegates to resolveContentAdapter and returns what it builds", () => {
|
|
60
|
+
const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
|
|
61
|
+
|
|
62
|
+
expect(mockResolveContentAdapter).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(adapter).toEqual({ marker: "resolved" });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("forwards contentRoot and locale to the resolver", () => {
|
|
67
|
+
getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "fr-CA" });
|
|
68
|
+
|
|
69
|
+
expect(mockResolveContentAdapter).toHaveBeenCalledWith({
|
|
70
|
+
contentRoot: "/fixtures/content",
|
|
71
|
+
defaultLocale: "fr-CA",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("defaults contentRoot to <cwd>/content when none is given", () => {
|
|
76
|
+
getMcpContentAdapter();
|
|
77
|
+
|
|
78
|
+
expect(mockResolveContentAdapter).toHaveBeenCalledWith({
|
|
79
|
+
contentRoot: join(process.cwd(), "content"),
|
|
80
|
+
defaultLocale: undefined,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("passes no workspaceId on CONTENT_ADAPTER=mdx — the MDX path stays byte-identical", () => {
|
|
85
|
+
getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "en-US" });
|
|
86
|
+
|
|
87
|
+
const options = mockResolveContentAdapter.mock.calls[0]![0] as Record<
|
|
88
|
+
string,
|
|
89
|
+
unknown
|
|
90
|
+
>;
|
|
91
|
+
expect(options).not.toHaveProperty("workspaceId");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("getMcpContentAdapter — CONTENT_ADAPTER=database", () => {
|
|
96
|
+
const previous = process.env.CONTENT_ADAPTER;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
mockResolveContentAdapter.mockReset();
|
|
100
|
+
// A full MdxContentAdapter-shaped stub — getMcpContentAdapter()'s database
|
|
101
|
+
// branch always passes the resolver's return through readOnlyContentAdapter
|
|
102
|
+
// (content-adapter.ts), which binds every read method.
|
|
103
|
+
mockResolveContentAdapter.mockReturnValue({
|
|
104
|
+
getPageBySlug: jest.fn(),
|
|
105
|
+
listPages: jest.fn(),
|
|
106
|
+
getDetailBySlug: jest.fn(),
|
|
107
|
+
listDetail: jest.fn(),
|
|
108
|
+
searchPages: jest.fn(),
|
|
109
|
+
extractStructuredData: jest.fn(),
|
|
110
|
+
getPageSource: jest.fn(),
|
|
111
|
+
createPage: jest.fn(),
|
|
112
|
+
updatePage: jest.fn(),
|
|
113
|
+
deletePage: jest.fn(),
|
|
114
|
+
getPageVersion: jest.fn(),
|
|
115
|
+
});
|
|
116
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
if (previous === undefined) delete process.env.CONTENT_ADAPTER;
|
|
121
|
+
else process.env.CONTENT_ADAPTER = previous;
|
|
122
|
+
resetMcpPublicWorkspace();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("forwards workspaceId and prisma to the resolver when the workspace resolved", () => {
|
|
126
|
+
// This test only asserts the prisma VALUE is forwarded by reference — it
|
|
127
|
+
// never calls a method on it, so a minimal marker object stands in.
|
|
128
|
+
// eslint-disable-next-line no-restricted-syntax -- structural test stub, mirrors audit-single-page.test.ts's identical pattern
|
|
129
|
+
const prismaStub = {
|
|
130
|
+
marker: "prisma-stub",
|
|
131
|
+
} as unknown as ArtifactPrismaClient;
|
|
132
|
+
__setMcpPublicWorkspaceForTest({
|
|
133
|
+
kind: "resolved",
|
|
134
|
+
workspaceId: "wksp_00000000-0000-7000-8000-000000000001",
|
|
135
|
+
prisma: prismaStub,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
getMcpContentAdapter({ contentRoot: "/fixtures/content", locale: "en-US" });
|
|
139
|
+
|
|
140
|
+
expect(mockResolveContentAdapter).toHaveBeenCalledWith({
|
|
141
|
+
workspaceId: "wksp_00000000-0000-7000-8000-000000000001",
|
|
142
|
+
prisma: prismaStub,
|
|
143
|
+
contentRoot: "/fixtures/content",
|
|
144
|
+
defaultLocale: "en-US",
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("never calls the resolver and returns the fail-closed empty adapter when unresolved", async () => {
|
|
149
|
+
__setMcpPublicWorkspaceForTest({ kind: "unresolved", reason: "ambiguous" });
|
|
150
|
+
|
|
151
|
+
const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
|
|
152
|
+
|
|
153
|
+
expect(mockResolveContentAdapter).not.toHaveBeenCalled();
|
|
154
|
+
await expect(adapter.getPageBySlug("HomePage", "home")).resolves.toBeNull();
|
|
155
|
+
await expect(adapter.listPages("HomePage")).resolves.toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("never calls the resolver and returns the fail-closed empty adapter before bootstrap has resolved", async () => {
|
|
159
|
+
// No __setMcpPublicWorkspaceForTest call — the module's initial
|
|
160
|
+
// { kind: "not-required" } state, exactly as an MCP tool invoked before
|
|
161
|
+
// apps/mcp/src/index.ts awaits initMcpPublicWorkspace() would see it (D4).
|
|
162
|
+
const adapter = getMcpContentAdapter({ contentRoot: "/fixtures/content" });
|
|
163
|
+
|
|
164
|
+
expect(mockResolveContentAdapter).not.toHaveBeenCalled();
|
|
165
|
+
await expect(adapter.getPageBySlug("HomePage", "home")).resolves.toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("getMcpMdxContentAdapter", () => {
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
mockResolveContentAdapter.mockReset();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns the resolved adapter when it exposes getPageSource", () => {
|
|
175
|
+
const mdxish = { getPageSource: jest.fn() };
|
|
176
|
+
mockResolveContentAdapter.mockReturnValue(mdxish);
|
|
177
|
+
|
|
178
|
+
expect(getMcpMdxContentAdapter({ contentRoot: "/fixtures/content" })).toBe(
|
|
179
|
+
mdxish,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("throws a named error when the resolved adapter is not MDX-capable", () => {
|
|
184
|
+
mockResolveContentAdapter.mockReturnValue({ getPageBySlug: jest.fn() });
|
|
185
|
+
|
|
186
|
+
expect(() => getMcpMdxContentAdapter()).toThrow(/CONTENT_ADAPTER=mdx/);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AC-2's "every content tool" sweep, demonstrated rather than argued (#5876
|
|
3
|
+
* spec D5, §4.1). `read_content.test.ts` and `list_pages.test.ts` (`##
|
|
4
|
+
* Scope`) already cover two of the nine read tools' database-path behaviour;
|
|
5
|
+
* this table drives all nine plus `buildSinglePageAuditInput` (the `audit`
|
|
6
|
+
* tool's page reader) through the SAME unresolved arm and asserts each
|
|
7
|
+
* returns its own not-found/empty shape — never a client-supplied workspace,
|
|
8
|
+
* never an enumeration signal, never a query.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
resetResolvedContentAdapter,
|
|
15
|
+
type ArtifactPrismaClient,
|
|
16
|
+
} from "@working-theory/content-adapters";
|
|
17
|
+
|
|
18
|
+
import { buildSinglePageAuditInput } from "../../src/lib/audit-single-page.js";
|
|
19
|
+
import {
|
|
20
|
+
__setMcpPublicWorkspaceForTest,
|
|
21
|
+
resetMcpPublicWorkspace,
|
|
22
|
+
} from "../../src/lib/public-workspace.js";
|
|
23
|
+
import { ailkGetBusinessHours } from "../../src/tools/get_business_hours.js";
|
|
24
|
+
import { ailkGetFaq } from "../../src/tools/get_faq.js";
|
|
25
|
+
import { ailkGetLocations } from "../../src/tools/get_locations.js";
|
|
26
|
+
import { ailkGetOfferings } from "../../src/tools/get_offerings.js";
|
|
27
|
+
import { ailkGetPricing } from "../../src/tools/get_pricing.js";
|
|
28
|
+
import { ailkGetSiteInfo } from "../../src/tools/get_site_info.js";
|
|
29
|
+
import { ailkListPages } from "../../src/tools/list_pages.js";
|
|
30
|
+
import { ailkReadContent } from "../../src/tools/read_content.js";
|
|
31
|
+
import { ailkSearchContent } from "../../src/tools/search_content.js";
|
|
32
|
+
|
|
33
|
+
/** A structural stub — never resolved into the seam under this arm, so every
|
|
34
|
+
* one of its methods must remain untouched by every case below. */
|
|
35
|
+
function makeArtifactPrismaClientStub(): ArtifactPrismaClient {
|
|
36
|
+
return {
|
|
37
|
+
artifact: {
|
|
38
|
+
findFirst: jest.fn(),
|
|
39
|
+
findMany: jest.fn(),
|
|
40
|
+
create: jest.fn(),
|
|
41
|
+
updateMany: jest.fn(),
|
|
42
|
+
deleteMany: jest.fn(),
|
|
43
|
+
},
|
|
44
|
+
artifactVersion: {
|
|
45
|
+
findFirst: jest.fn(),
|
|
46
|
+
findMany: jest.fn(),
|
|
47
|
+
create: jest.fn(),
|
|
48
|
+
updateMany: jest.fn(),
|
|
49
|
+
},
|
|
50
|
+
$transaction: jest.fn(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("every content tool — CONTENT_ADAPTER=database, unresolved workspace", () => {
|
|
55
|
+
const previous = process.env.CONTENT_ADAPTER;
|
|
56
|
+
let stub: ArtifactPrismaClient;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
60
|
+
resetResolvedContentAdapter();
|
|
61
|
+
stub = makeArtifactPrismaClientStub();
|
|
62
|
+
__setMcpPublicWorkspaceForTest({ kind: "unresolved", reason: "ambiguous" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
if (previous === undefined) delete process.env.CONTENT_ADAPTER;
|
|
67
|
+
else process.env.CONTENT_ADAPTER = previous;
|
|
68
|
+
resetMcpPublicWorkspace();
|
|
69
|
+
resetResolvedContentAdapter();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Every stub method is asserted untouched — nothing here reaches a query,
|
|
73
|
+
* not even the stub the deployment never got wired to (NFR-5). */
|
|
74
|
+
function expectNoQuery(): void {
|
|
75
|
+
expect(stub.artifact.findFirst).not.toHaveBeenCalled();
|
|
76
|
+
expect(stub.artifact.findMany).not.toHaveBeenCalled();
|
|
77
|
+
expect(stub.artifactVersion.findFirst).not.toHaveBeenCalled();
|
|
78
|
+
expect(stub.artifactVersion.findMany).not.toHaveBeenCalled();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
it("ailkReadContent returns null", async () => {
|
|
82
|
+
const result = await ailkReadContent({ type: "HomePage", slug: "home" });
|
|
83
|
+
expect(result).toBeNull();
|
|
84
|
+
expectNoQuery();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("ailkListPages returns { pages: [] }", async () => {
|
|
88
|
+
const result = await ailkListPages({ type: "HomePage" });
|
|
89
|
+
expect(result).toEqual({ pages: [] });
|
|
90
|
+
expectNoQuery();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("ailkSearchContent returns { results: [] }", async () => {
|
|
94
|
+
const result = await ailkSearchContent({ query: "anything" });
|
|
95
|
+
expect(result).toEqual({ results: [] });
|
|
96
|
+
expectNoQuery();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("ailkGetFaq returns null", async () => {
|
|
100
|
+
const result = await ailkGetFaq({});
|
|
101
|
+
expect(result).toBeNull();
|
|
102
|
+
expectNoQuery();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("ailkGetOfferings returns { offerings: [] }", async () => {
|
|
106
|
+
const result = await ailkGetOfferings({});
|
|
107
|
+
expect(result).toEqual({ offerings: [] });
|
|
108
|
+
expectNoQuery();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("ailkGetSiteInfo returns null", async () => {
|
|
112
|
+
const result = await ailkGetSiteInfo({});
|
|
113
|
+
expect(result).toBeNull();
|
|
114
|
+
expectNoQuery();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("ailkGetBusinessHours returns null", async () => {
|
|
118
|
+
const result = await ailkGetBusinessHours({});
|
|
119
|
+
expect(result).toBeNull();
|
|
120
|
+
expectNoQuery();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("ailkGetLocations returns { locations: [] }", async () => {
|
|
124
|
+
const result = await ailkGetLocations({});
|
|
125
|
+
expect(result).toEqual({ locations: [] });
|
|
126
|
+
expectNoQuery();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("ailkGetPricing returns null", async () => {
|
|
130
|
+
const result = await ailkGetPricing({});
|
|
131
|
+
expect(result).toBeNull();
|
|
132
|
+
expectNoQuery();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("buildSinglePageAuditInput throws its own not-found error (never a workspace-state error)", async () => {
|
|
136
|
+
await expect(buildSinglePageAuditInput({ slug: "home" })).rejects.toThrow(
|
|
137
|
+
/Page not found/,
|
|
138
|
+
);
|
|
139
|
+
expectNoQuery();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("no content tool source names workspaceId (structural guard — D8)", () => {
|
|
143
|
+
// The tools imported above — a caller-supplied workspaceId is never a
|
|
144
|
+
// valid MCP input, on any tool, at any point (mirrors read_content.test.ts's
|
|
145
|
+
// existing "no console.*" structural guard idiom).
|
|
146
|
+
const toolFiles = [
|
|
147
|
+
"read_content.ts",
|
|
148
|
+
"list_pages.ts",
|
|
149
|
+
"search_content.ts",
|
|
150
|
+
"get_faq.ts",
|
|
151
|
+
"get_offerings.ts",
|
|
152
|
+
"get_site_info.ts",
|
|
153
|
+
"get_business_hours.ts",
|
|
154
|
+
"get_locations.ts",
|
|
155
|
+
"get_pricing.ts",
|
|
156
|
+
];
|
|
157
|
+
for (const file of toolFiles) {
|
|
158
|
+
const src = readFileSync(
|
|
159
|
+
resolve(__dirname, "../../src/tools", file),
|
|
160
|
+
"utf-8",
|
|
161
|
+
);
|
|
162
|
+
expect(src).not.toMatch(/\bworkspaceId\b/);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|