create-ailk 0.2.0 → 0.3.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/component-catalog.md +50 -53
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +2 -2
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +33 -5
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +459 -135
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +229 -4
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +14 -13
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +0 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/database/CHANGELOG.md +33 -0
- package/templates/database/inbox/schema.prisma +67 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1296
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/listing-promotion.ts +0 -342
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comparison instrument for #5314 — diffs the routes the server ACTUALLY
|
|
3
|
+
* registers against each surface that claims to describe them.
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces describe this API's endpoints: the root index `endpoints`
|
|
6
|
+
* object (`server.ts`) and the OpenAPI `paths` object (`spec.ts`). Before
|
|
7
|
+
* #5314 both were hand-maintained literals, and both had drifted — 11 and 7
|
|
8
|
+
* entries against 68 registered routes, with `/v1/project-listings/public`
|
|
9
|
+
* missing from both and `/v1/billing/usage` missing from the root index.
|
|
10
|
+
*
|
|
11
|
+
* This module only COMPARES. The derivation that keeps the surfaces honest
|
|
12
|
+
* lives in `spec.ts` beside the shapes it produces; keeping the comparison
|
|
13
|
+
* separate is what lets the gate assert against a surface it did not build.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately free of any `fastify` or `spec.ts` import: it takes plain data,
|
|
16
|
+
* so the gate can feed it a SERVED document (fetched over `inject`) rather
|
|
17
|
+
* than the object the code under test just returned.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** One route as Fastify registered it. `url` is Fastify-shaped (`:param`). */
|
|
21
|
+
export interface RegisteredRoute {
|
|
22
|
+
/** Uppercase HTTP method. HEAD is excluded — Fastify derives it from GET. */
|
|
23
|
+
method: string;
|
|
24
|
+
/** Fastify path, e.g. `/v1/content/:type/:slug`. */
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One surface's disagreement with the registered route table. */
|
|
29
|
+
export interface SurfaceDiff {
|
|
30
|
+
/** Which surface this diff describes. */
|
|
31
|
+
surface: "openapi-paths" | "root-index-endpoints";
|
|
32
|
+
/** `METHOD /path` registered by the server but absent from the surface. */
|
|
33
|
+
missingFromSurface: string[];
|
|
34
|
+
/** `METHOD /path` named by the surface but not registered by the server. */
|
|
35
|
+
orphanedInSurface: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fastify path → OpenAPI path template: `/v1/content/:type` becomes
|
|
40
|
+
* `/v1/content/{type}`. OpenAPI 3.1 has no `:param` form, so a document that
|
|
41
|
+
* published the Fastify string would be describing a path no client can build.
|
|
42
|
+
*/
|
|
43
|
+
export function toOpenApiPath(fastifyPath: string): string {
|
|
44
|
+
return fastifyPath
|
|
45
|
+
.split("/")
|
|
46
|
+
.map((segment) =>
|
|
47
|
+
segment.startsWith(":") ? `{${segment.slice(1)}}` : segment,
|
|
48
|
+
)
|
|
49
|
+
.join("/");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The comparison key both sides normalise to: `GET /v1/content/types`. */
|
|
53
|
+
export function routeKey(method: string, path: string): string {
|
|
54
|
+
return `${method.toUpperCase()} ${path}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registered routes as OpenAPI-shaped comparison keys. */
|
|
58
|
+
function registeredOpenApiKeys(routes: RegisteredRoute[]): Set<string> {
|
|
59
|
+
return new Set(routes.map((r) => routeKey(r.method, toOpenApiPath(r.url))));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Registered routes as Fastify-shaped comparison keys. */
|
|
63
|
+
function registeredFastifyKeys(routes: RegisteredRoute[]): Set<string> {
|
|
64
|
+
return new Set(routes.map((r) => routeKey(r.method, r.url)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The OpenAPI methods a path item declares. Non-method keys an OpenAPI path
|
|
69
|
+
* item may legally carry (`parameters`, `summary`, `description`, `servers`,
|
|
70
|
+
* `$ref`) are not operations and must not be compared as if they were.
|
|
71
|
+
*/
|
|
72
|
+
const OPENAPI_METHODS = new Set([
|
|
73
|
+
"get",
|
|
74
|
+
"put",
|
|
75
|
+
"post",
|
|
76
|
+
"delete",
|
|
77
|
+
"options",
|
|
78
|
+
"head",
|
|
79
|
+
"patch",
|
|
80
|
+
"trace",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Comparison keys a served OpenAPI `paths` object claims.
|
|
85
|
+
*
|
|
86
|
+
* Takes the served document's `paths` as plain data so the gate can compare
|
|
87
|
+
* what an agent would actually fetch, not what the builder returned.
|
|
88
|
+
*/
|
|
89
|
+
export function openApiSurfaceKeys(
|
|
90
|
+
paths: Record<string, Record<string, unknown>>,
|
|
91
|
+
): Set<string> {
|
|
92
|
+
const keys = new Set<string>();
|
|
93
|
+
for (const [path, item] of Object.entries(paths ?? {})) {
|
|
94
|
+
for (const method of Object.keys(item ?? {})) {
|
|
95
|
+
if (!OPENAPI_METHODS.has(method.toLowerCase())) continue;
|
|
96
|
+
if (method.toLowerCase() === "head") continue;
|
|
97
|
+
keys.add(routeKey(method, path));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return keys;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Comparison keys a root-index `endpoints` object claims.
|
|
105
|
+
*
|
|
106
|
+
* Each value is `"METHOD[/METHOD...] /path"` — the shape the hand-authored
|
|
107
|
+
* object already used (`"GET/PATCH /v1/user/profile"`), preserved so no
|
|
108
|
+
* published entry changes shape. A value that does not parse is reported as
|
|
109
|
+
* an orphan rather than skipped: an unparseable entry is precisely the kind
|
|
110
|
+
* of hand-edit this instrument exists to catch.
|
|
111
|
+
*/
|
|
112
|
+
export function rootIndexSurfaceKeys(
|
|
113
|
+
endpoints: Record<string, string>,
|
|
114
|
+
): Set<string> {
|
|
115
|
+
const keys = new Set<string>();
|
|
116
|
+
for (const value of Object.values(endpoints ?? {})) {
|
|
117
|
+
const match = /^([A-Za-z/]+)\s+(\S+)$/.exec(value ?? "");
|
|
118
|
+
if (!match?.[1] || !match[2]) {
|
|
119
|
+
keys.add(`UNPARSEABLE ${value}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const method of match[1].split("/")) {
|
|
123
|
+
if (!method) continue;
|
|
124
|
+
if (method.toUpperCase() === "HEAD") continue;
|
|
125
|
+
keys.add(routeKey(method, match[2]));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return keys;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function diff(registered: Set<string>, surface: Set<string>) {
|
|
132
|
+
return {
|
|
133
|
+
missingFromSurface: [...registered].filter((k) => !surface.has(k)).sort(),
|
|
134
|
+
orphanedInSurface: [...surface].filter((k) => !registered.has(k)).sort(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Diff the registered route table against both surfaces.
|
|
140
|
+
*
|
|
141
|
+
* `paths` is the served OpenAPI `paths` object (OpenAPI-templated); `endpoints`
|
|
142
|
+
* is the served root-index `endpoints` object (Fastify-shaped, matching the
|
|
143
|
+
* strings that surface has always published). Each is compared in its own
|
|
144
|
+
* vocabulary so neither surface is judged against a shape it never used.
|
|
145
|
+
*/
|
|
146
|
+
export function compareSurfaces(
|
|
147
|
+
registered: RegisteredRoute[],
|
|
148
|
+
paths: Record<string, Record<string, unknown>>,
|
|
149
|
+
endpoints: Record<string, string>,
|
|
150
|
+
): SurfaceDiff[] {
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
surface: "openapi-paths",
|
|
154
|
+
...diff(registeredOpenApiKeys(registered), openApiSurfaceKeys(paths)),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
surface: "root-index-endpoints",
|
|
158
|
+
...diff(
|
|
159
|
+
registeredFastifyKeys(registered),
|
|
160
|
+
rootIndexSurfaceKeys(endpoints),
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether any surface disagrees with the registered route table. */
|
|
167
|
+
export function hasDrift(diffs: SurfaceDiff[]): boolean {
|
|
168
|
+
return diffs.some(
|
|
169
|
+
(d) => d.missingFromSurface.length > 0 || d.orphanedInSurface.length > 0,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Render the drift for a failing assertion, or `""` when there is none.
|
|
175
|
+
*
|
|
176
|
+
* A surface that agrees contributes nothing, so the empty string means "no
|
|
177
|
+
* drift" and can be asserted against directly. Counts come first so a reader
|
|
178
|
+
* sees the scale before the list, then EVERY entry — a truncated drift report
|
|
179
|
+
* is how a gate teaches people to ignore it.
|
|
180
|
+
*/
|
|
181
|
+
export function formatSurfaceDrift(diffs: SurfaceDiff[]): string {
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
for (const d of diffs) {
|
|
184
|
+
if (d.missingFromSurface.length === 0 && d.orphanedInSurface.length === 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
lines.push(
|
|
188
|
+
`${d.surface}: ${d.missingFromSurface.length} registered route(s) missing from the surface, ` +
|
|
189
|
+
`${d.orphanedInSurface.length} surface entr(ies) with no registered route`,
|
|
190
|
+
);
|
|
191
|
+
for (const k of d.missingFromSurface) lines.push(` missing: ${k}`);
|
|
192
|
+
for (const k of d.orphanedInSurface) lines.push(` orphaned: ${k}`);
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
@@ -61,14 +61,28 @@ const validBody = {
|
|
|
61
61
|
|
|
62
62
|
describe("POST /api/content/:type", () => {
|
|
63
63
|
let app: FastifyInstance;
|
|
64
|
+
// The write handlers only register when content-write is enabled
|
|
65
|
+
// (routes/content/index.ts's isModuleEnabled('content-write') gate); an
|
|
66
|
+
// unset AILK_ENABLED_MODULES no longer defaults to every module on
|
|
67
|
+
// (D-44, #5368), so this suite enables it explicitly.
|
|
68
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
64
69
|
|
|
65
70
|
beforeEach(async () => {
|
|
71
|
+
process.env.AILK_ENABLED_MODULES = "content-write";
|
|
66
72
|
app = require("fastify")();
|
|
67
73
|
await app.register(contentRoutes);
|
|
68
74
|
jest.clearAllMocks();
|
|
69
75
|
mockCheckLicense.mockReset();
|
|
70
76
|
});
|
|
71
77
|
|
|
78
|
+
afterAll(() => {
|
|
79
|
+
if (priorEnabledModules === undefined) {
|
|
80
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
81
|
+
} else {
|
|
82
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
72
86
|
afterEach(async () => {
|
|
73
87
|
await app.close();
|
|
74
88
|
});
|
|
@@ -49,14 +49,28 @@ function freeLicense() {
|
|
|
49
49
|
|
|
50
50
|
describe("DELETE /api/content/:type/:slug", () => {
|
|
51
51
|
let app: FastifyInstance;
|
|
52
|
+
// The write handlers only register when content-write is enabled
|
|
53
|
+
// (routes/content/index.ts's isModuleEnabled('content-write') gate); an
|
|
54
|
+
// unset AILK_ENABLED_MODULES no longer defaults to every module on
|
|
55
|
+
// (D-44, #5368), so this suite enables it explicitly.
|
|
56
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
52
57
|
|
|
53
58
|
beforeEach(async () => {
|
|
59
|
+
process.env.AILK_ENABLED_MODULES = "content-write";
|
|
54
60
|
app = require("fastify")();
|
|
55
61
|
await app.register(contentRoutes);
|
|
56
62
|
jest.clearAllMocks();
|
|
57
63
|
mockCheckLicense.mockReset();
|
|
58
64
|
});
|
|
59
65
|
|
|
66
|
+
afterAll(() => {
|
|
67
|
+
if (priorEnabledModules === undefined) {
|
|
68
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
69
|
+
} else {
|
|
70
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
60
74
|
afterEach(async () => {
|
|
61
75
|
await app.close();
|
|
62
76
|
});
|
|
@@ -61,14 +61,28 @@ const validBody = {
|
|
|
61
61
|
|
|
62
62
|
describe("PATCH /api/content/:type/:slug", () => {
|
|
63
63
|
let app: FastifyInstance;
|
|
64
|
+
// The write handlers only register when content-write is enabled
|
|
65
|
+
// (routes/content/index.ts's isModuleEnabled('content-write') gate); an
|
|
66
|
+
// unset AILK_ENABLED_MODULES no longer defaults to every module on
|
|
67
|
+
// (D-44, #5368), so this suite enables it explicitly.
|
|
68
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
64
69
|
|
|
65
70
|
beforeEach(async () => {
|
|
71
|
+
process.env.AILK_ENABLED_MODULES = "content-write";
|
|
66
72
|
app = require("fastify")();
|
|
67
73
|
await app.register(contentRoutes);
|
|
68
74
|
jest.clearAllMocks();
|
|
69
75
|
mockCheckLicense.mockReset();
|
|
70
76
|
});
|
|
71
77
|
|
|
78
|
+
afterAll(() => {
|
|
79
|
+
if (priorEnabledModules === undefined) {
|
|
80
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
81
|
+
} else {
|
|
82
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
72
86
|
afterEach(async () => {
|
|
73
87
|
await app.close();
|
|
74
88
|
});
|
|
@@ -24,25 +24,52 @@
|
|
|
24
24
|
import type { FastifyPluginAsync } from "fastify";
|
|
25
25
|
|
|
26
26
|
import { isModuleEnabled } from "../../config/modules.js";
|
|
27
|
+
import { loadSlice } from "../../lib/slice-load.js";
|
|
27
28
|
|
|
28
29
|
import { getPageRoute } from "./get-page.js";
|
|
29
30
|
import { listPagesRoute } from "./list-pages.js";
|
|
30
31
|
import { listTypesRoute } from "./list-types.js";
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
* Content-write slice entry points, as { specifier, exportName }.
|
|
34
|
+
* Content-write slice entry points, as { specifier, exportName, load }.
|
|
34
35
|
*
|
|
35
36
|
* What defeats `tsc`'s resolution is the INDIRECTION, not the type: TypeScript
|
|
36
37
|
* resolves `import("...")` only when the argument is a string-literal
|
|
37
|
-
* EXPRESSION written at the call site.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* EXPRESSION written at the call site.
|
|
39
|
+
*
|
|
40
|
+
* WHICH indirection matters, and #5304 is the correction. This table used to
|
|
41
|
+
* hold the specifier as a string and the loop read it out per iteration — a
|
|
42
|
+
* shape `@vercel/nft` cannot evaluate either, so the deploy-time tracer did not
|
|
43
|
+
* emit these files and a deployed site shipped without them. A module-level
|
|
44
|
+
* constant dereferenced into `import()` is unresolvable to `tsc` and traceable
|
|
45
|
+
* to `nft`, so the strip and the deployment are both satisfied. `specifier` is
|
|
46
|
+
* carried for the drift message below and reads the same constant as `load`, so
|
|
47
|
+
* the two cannot disagree.
|
|
41
48
|
*/
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const CREATE_ROUTE = "./create.js";
|
|
50
|
+
const UPDATE_ROUTE = "./update.js";
|
|
51
|
+
const DELETE_ROUTE = "./delete.js";
|
|
52
|
+
|
|
53
|
+
const WRITE_ROUTES: ReadonlyArray<{
|
|
54
|
+
specifier: string;
|
|
55
|
+
exportName: string;
|
|
56
|
+
load: () => Promise<unknown>;
|
|
57
|
+
}> = [
|
|
58
|
+
{
|
|
59
|
+
specifier: CREATE_ROUTE,
|
|
60
|
+
exportName: "createPageRoute",
|
|
61
|
+
load: () => import(CREATE_ROUTE),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
specifier: UPDATE_ROUTE,
|
|
65
|
+
exportName: "updatePageRoute",
|
|
66
|
+
load: () => import(UPDATE_ROUTE),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
specifier: DELETE_ROUTE,
|
|
70
|
+
exportName: "deletePageRoute",
|
|
71
|
+
load: () => import(DELETE_ROUTE),
|
|
72
|
+
},
|
|
46
73
|
];
|
|
47
74
|
|
|
48
75
|
export const contentRoutes: FastifyPluginAsync = async (fastify) => {
|
|
@@ -51,8 +78,8 @@ export const contentRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
51
78
|
await fastify.register(getPageRoute);
|
|
52
79
|
|
|
53
80
|
if (isModuleEnabled("content-write")) {
|
|
54
|
-
for (const { specifier, exportName } of WRITE_ROUTES) {
|
|
55
|
-
const mod = (await
|
|
81
|
+
for (const { specifier, exportName, load } of WRITE_ROUTES) {
|
|
82
|
+
const mod = (await loadSlice(specifier, load)) as Record<
|
|
56
83
|
string,
|
|
57
84
|
FastifyPluginAsync | undefined
|
|
58
85
|
>;
|
package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Not marked oss: false (D-48/#5366 escalation) — tests the
|
|
2
|
+
// always-shipping project-listings anonymous surface; see
|
|
3
|
+
// routes/project-listings/me.ts's header comment.
|
|
1
4
|
/**
|
|
2
5
|
* @file configured-application.test.ts
|
|
3
6
|
*
|
|
@@ -43,6 +46,13 @@ jest.mock("../../../lib/prisma.js");
|
|
|
43
46
|
|
|
44
47
|
jest.mock("@working-theory/email", () => ({
|
|
45
48
|
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
49
|
+
// #5406 — `start` now reaches the resume email. Declared here for the reason
|
|
50
|
+
// docs/lessons/project-listings/04 records: a mock factory that omits an
|
|
51
|
+
// export the code under test calls leaves it `undefined` at the callsite.
|
|
52
|
+
// These suites configure no `resumeEmail`, so the send short-circuits before
|
|
53
|
+
// this is reached; it is declared so a future fixture that DOES configure
|
|
54
|
+
// one fails on its assertion rather than on a missing mock.
|
|
55
|
+
sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
|
|
46
56
|
}));
|
|
47
57
|
|
|
48
58
|
jest.mock("@working-theory/license", () => ({
|
|
@@ -217,6 +227,13 @@ function installStore(): void {
|
|
|
217
227
|
anythingElse: null,
|
|
218
228
|
address: null,
|
|
219
229
|
questions: null,
|
|
230
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
231
|
+
// started draft on a site that asks for one name line: parts null,
|
|
232
|
+
// no phone, no verification stamp.
|
|
233
|
+
founderFirstName: null,
|
|
234
|
+
founderLastName: null,
|
|
235
|
+
founderPhone: null,
|
|
236
|
+
emailVerifiedAt: null,
|
|
220
237
|
founderCompany: null,
|
|
221
238
|
leadId: null,
|
|
222
239
|
headline: null,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Not marked oss: false (D-48/#5366 escalation) — tests the
|
|
2
|
+
// always-shipping project-listings anonymous surface; see
|
|
3
|
+
// routes/project-listings/me.ts's header comment.
|
|
1
4
|
/**
|
|
2
5
|
* @file drafts.test.ts
|
|
3
6
|
*
|
|
@@ -40,6 +43,13 @@ jest.mock("../../../lib/prisma.js");
|
|
|
40
43
|
|
|
41
44
|
jest.mock("@working-theory/email", () => ({
|
|
42
45
|
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-test" }),
|
|
46
|
+
// #5406 — `start` now reaches the resume email. Declared here for the reason
|
|
47
|
+
// docs/lessons/project-listings/04 records: a mock factory that omits an
|
|
48
|
+
// export the code under test calls leaves it `undefined` at the callsite.
|
|
49
|
+
// These suites configure no `resumeEmail`, so the send short-circuits before
|
|
50
|
+
// this is reached; it is declared so a future fixture that DOES configure
|
|
51
|
+
// one fails on its assertion rather than on a missing mock.
|
|
52
|
+
sendListingResumeEmail: jest.fn().mockResolvedValue({ ok: true, messageId: "msg-resume" }),
|
|
43
53
|
}));
|
|
44
54
|
|
|
45
55
|
jest.mock("@working-theory/license", () => ({
|
|
@@ -193,6 +203,13 @@ function installStore(): void {
|
|
|
193
203
|
anythingElse: null,
|
|
194
204
|
address: null,
|
|
195
205
|
questions: null,
|
|
206
|
+
// #5407 — a row written before the columns existed, and a freshly
|
|
207
|
+
// started draft on a site that asks for one name line: parts null,
|
|
208
|
+
// no phone, no verification stamp.
|
|
209
|
+
founderFirstName: null,
|
|
210
|
+
founderLastName: null,
|
|
211
|
+
founderPhone: null,
|
|
212
|
+
emailVerifiedAt: null,
|
|
196
213
|
founderCompany: null,
|
|
197
214
|
leadId: null,
|
|
198
215
|
headline: null,
|