create-ailk 0.23.0 → 0.24.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.
Files changed (96) hide show
  1. package/README.md +27 -14
  2. package/dist/add-module.js +6 -1
  3. package/dist/add-route.js +7 -1
  4. package/dist/derive-architecture.js +17 -0
  5. package/dist/lib/scaffold-render-sweep.d.ts +33 -0
  6. package/dist/lib/scaffold-render-sweep.js +45 -0
  7. package/dist/module-architecture.js +17 -0
  8. package/dist/sync-routes.d.ts +30 -4
  9. package/dist/sync-routes.js +271 -18
  10. package/package.json +3 -3
  11. package/templates/.env.example +4 -0
  12. package/templates/CLAUDE.md +4 -2
  13. package/templates/apps/api/.env.example +7 -0
  14. package/templates/apps/api/CLAUDE.md +25 -11
  15. package/templates/apps/api/src/lib/__mocks__/prisma.ts +4 -1
  16. package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
  17. package/templates/apps/api/src/lib/site.ts +4 -1
  18. package/templates/apps/api/src/lib/stripe.ts +3 -0
  19. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +127 -0
  20. package/templates/apps/api/src/openapi/spec.ts +333 -0
  21. package/templates/apps/api/src/routes/billing/plan-checkout.ts +18 -11
  22. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +126 -0
  23. package/templates/apps/api/src/routes/content/get-page.ts +34 -119
  24. package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
  25. package/templates/apps/api/src/routes/content/search.ts +16 -45
  26. package/templates/apps/api/src/routes/leads/index.ts +364 -298
  27. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
  28. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
  29. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
  30. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -0
  31. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +1 -0
  32. package/templates/apps/api/src/routes/webhooks/stripe.ts +2 -0
  33. package/templates/apps/api/src/server.ts +25 -0
  34. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
  35. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +10 -0
  36. package/templates/apps/api/src/services/__tests__/person.test.ts +28 -0
  37. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +6 -0
  38. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
  39. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
  40. package/templates/apps/api/src/services/abandon-email.ts +28 -5
  41. package/templates/apps/api/src/services/deliverable-fulfillment.ts +2 -7
  42. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +53 -0
  43. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +107 -0
  44. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +4 -0
  45. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +4 -0
  46. package/templates/apps/api/src/services/offerings/catalog.ts +10 -0
  47. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +23 -5
  48. package/templates/apps/api/src/services/offerings/connection.ts +83 -3
  49. package/templates/apps/api/src/services/offerings/index.ts +6 -6
  50. package/templates/apps/api/src/services/person.ts +23 -0
  51. package/templates/apps/api/src/services/project-listings.ts +43 -31
  52. package/templates/apps/mcp/CLAUDE.md +11 -5
  53. package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
  54. package/templates/apps/mcp/__tests__/http-transport.test.ts +91 -10
  55. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
  56. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
  57. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
  58. package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
  59. package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
  60. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
  61. package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
  62. package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
  63. package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
  64. package/templates/apps/mcp/__tests__/submit_lead.test.ts +82 -167
  65. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
  66. package/templates/apps/mcp/package.json +1 -0
  67. package/templates/apps/mcp/src/abuse/index.ts +18 -11
  68. package/templates/apps/mcp/src/http-transport.ts +47 -9
  69. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
  70. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
  71. package/templates/apps/mcp/src/lib/site-config.ts +53 -0
  72. package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
  73. package/templates/apps/mcp/src/tools/index.ts +22 -21
  74. package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
  75. package/templates/apps/mcp/src/tools/read_content.ts +46 -40
  76. package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
  77. package/templates/apps/mcp/src/tools/search_content.ts +20 -24
  78. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
  79. package/templates/apps/web/CLAUDE.md +12 -3
  80. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  81. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  82. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  83. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +21 -4
  84. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  85. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  86. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  87. package/templates/database/CHANGELOG.md +20 -0
  88. package/templates/database/__tests__/seed.test.ts +33 -1
  89. package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
  90. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +28 -0
  91. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
  92. package/templates/database/package.json +1 -1
  93. package/templates/database/seed.ts +27 -0
  94. package/templates/tools/lint-architecture.ts +205 -1
  95. package/versions.json +34 -34
  96. package/templates/apps/api/src/routes/content/slug.ts +0 -9
package/README.md CHANGED
@@ -214,9 +214,9 @@ as-is, they never fail the check, and `--apply` never overwrites them.
214
214
 
215
215
  `--apply` only creates route directories that are absent, through the same
216
216
  copier a fresh scaffold uses (so the copied files are byte-identical to a fresh
217
- scaffold's). It never rewrites or deletes an existing file, never writes
218
- outside `apps/web/app/[locale]/`, runs no install, and a second run is a
219
- no-op. `--apply` exits 0 on success.
217
+ scaffold's). It never rewrites or deletes an existing file, writes nothing
218
+ outside `apps/web/app/[locale]/` except the local modules those routes import
219
+ (below), runs no install, and a second run is a no-op. `--apply` exits 0 on success.
220
220
 
221
221
  **Scope.** Every core route (`about`, `contact`, `faq`, `thanks/[slug]`,
222
222
  `sign-in`) is always checked, and so is the `(authed)` route GROUP —
@@ -232,17 +232,30 @@ gated off at scaffold time is listed as skipped, not missing — its absence is
232
232
  deliberate, not drift. Add a surface with `--sync-architecture` and the
233
233
  `add-page` skill, not with `sync-routes`.
234
234
 
235
- **The lib-and-messages dependency contract (#5014).** Some routes import
236
- files OUTSIDE their own route directory — `sign-in/page.tsx` imports
237
- `apps/web/lib/auth-env.ts` and `lib/site-brand.tsx`; the `(authed)` group's
238
- `layout.tsx` imports `apps/web/auth.ts`; `(authed)/account/page.tsx` imports
239
- `lib/account-config.ts`, `lib/server-api.ts`, and two files from its
240
- `(authed)/waitlist` sibling (`components/ForbiddenState.tsx`, `format.ts`).
241
- Copying a route's directory alone would leave those imports dangling in an
242
- older consumer that never had them. `ROUTE_DEPENDENCIES` in `src/surfaces.ts`
243
- declares them per route; `sync-routes` copies each one `--apply` finds
244
- missing (never overwriting — the same rule routes themselves follow) and
245
- reports it in check mode under an `Also required` heading. `ROUTE_MESSAGE_NAMESPACES`
235
+ **The local-module dependency contract (#5014, #9406).** Some routes import
236
+ files OUTSIDE their own route directory — `purchase/success/page.tsx` imports
237
+ `apps/web/src/lib/commerce-claim.ts`; the `(authed)` group's `layout.tsx`
238
+ imports `apps/web/auth.ts`. Copying a route's directory alone would leave those
239
+ imports dangling in an older consumer that never had them. So `sync-routes`
240
+ resolves every relative import (`./`, `../`) of every file the route would
241
+ copy, and of every helper that brings in turn, against the site first and then
242
+ the template. `ROUTE_DEPENDENCIES` in `src/surfaces.ts` is a declared
243
+ supplement, unioned with what the resolver finds. Each imported module has one
244
+ of three outcomes:
245
+
246
+ - **Copied** — the site lacks it and the template has it: `--apply` copies it
247
+ (never overwriting) and check mode lists it under `copies:` and `Also
248
+ required`.
249
+ - **Left as-is** — the site has it and its copy differs from the template's:
250
+ it is named under `left as-is (differs from the template)` and never
251
+ written. The route is still copied; run your type-check afterwards.
252
+ - **Refused** — the import resolves to no file in the site or the template,
253
+ only to a spoke-excluded file, or to a surface the site does not carry: the
254
+ route is listed under `cannot resolve:` and `--apply` does not write it, nor
255
+ its sub-routes, nor a route that imports from it. Its ancestor route is still
256
+ copied without the refused sub-tree.
257
+
258
+ `ROUTE_MESSAGE_NAMESPACES`
246
259
  does the same for the i18n namespaces a route's copy needs
247
260
  (`signIn`, `account`, `waitlistDashboard.gate`) — but CHECK-ONLY: a
248
261
  consumer's `apps/web/messages/<locale>.json` already exists and may carry
@@ -351,7 +351,9 @@ export function formatAddModuleReport(result, moduleName) {
351
351
  const lines = [
352
352
  `[create-ailk] add-module ${moduleName} → ${result.targetDir}`,
353
353
  ];
354
- if (result.report.length === 0 && result.routes.applied.length === 0) {
354
+ if (result.report.length === 0 &&
355
+ result.routes.applied.length === 0 &&
356
+ result.routes.refused.length === 0) {
355
357
  lines.push(`Nothing to add — the site already carries everything ${moduleName} brings.`);
356
358
  }
357
359
  if (written.length > 0) {
@@ -387,6 +389,9 @@ export function formatAddModuleReport(result, moduleName) {
387
389
  lines.push(`Also copied ${routes.appliedDependencies.length} file(s) those routes import:`);
388
390
  lines.push(...routes.appliedDependencies.map((dep) => ` ${dep}`));
389
391
  }
392
+ if (routes.refused.length > 0) {
393
+ lines.push(`Routes not copied (${routes.refused.length}) — an import does not resolve (run create-ailk sync-routes for the paths): ${routes.refused.join(", ")}`);
394
+ }
390
395
  if (routes.missingMessageNamespaces.length > 0) {
391
396
  lines.push("Also required — message namespace(s) not found in apps/web/messages/en.json (add manually, not auto-merged):");
392
397
  lines.push(...routes.missingMessageNamespaces.map((ns) => ` ${ns}`));
package/dist/add-route.js CHANGED
@@ -226,13 +226,16 @@ export function formatAddRouteReport(result) {
226
226
  const skipped = result.report.filter((row) => row.action === "skipped");
227
227
  const routes = result.routes ?? {
228
228
  applied: [],
229
+ refused: [],
229
230
  appliedDependencies: [],
230
231
  missingMessageNamespaces: [],
231
232
  };
232
233
  const lines = [
233
234
  `[create-ailk] add-route ${result.route} → ${result.targetDir}`,
234
235
  ];
235
- if (result.report.length === 0 && routes.applied.length === 0) {
236
+ if (result.report.length === 0 &&
237
+ routes.applied.length === 0 &&
238
+ routes.refused.length === 0) {
236
239
  lines.push(`Nothing to add — the site already carries the ${result.route} route.`);
237
240
  }
238
241
  if (routes.applied.length > 0) {
@@ -243,6 +246,9 @@ export function formatAddRouteReport(result) {
243
246
  lines.push(`Also copied ${routes.appliedDependencies.length} file(s) those routes import:`);
244
247
  lines.push(...routes.appliedDependencies.map((dep) => ` ${dep}`));
245
248
  }
249
+ if (routes.refused.length > 0) {
250
+ lines.push(`Routes not copied (${routes.refused.length}) — an import does not resolve (run create-ailk sync-routes for the paths): ${routes.refused.join(", ")}`);
251
+ }
246
252
  if (written.length > 0) {
247
253
  lines.push(`Written (${written.length}):`);
248
254
  lines.push(...written.map((row) => ` ${row.path}`));
@@ -15,6 +15,8 @@
15
15
  */
16
16
  import { ARCHITECTURE_BASE, MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
17
17
  import { CORE_SURFACE_PATHS, RECOGNIZED_SURFACES, resolveSurfaces, } from "./surfaces.js";
18
+ /** The npm scope a site consumes and never forks (#9191). */
19
+ const PACKAGE_SCOPE = "@working-theory/";
18
20
  /** A spoke's composition tiers (#7426 D8): its data and API are the hub's. */
19
21
  const SPOKE_TIERS = {
20
22
  data: "none — the hub's database",
@@ -314,6 +316,20 @@ export function deriveArchitecture(modules = ["marketing"], surfaces, options =
314
316
  for (const s of slices)
315
317
  extraForbidden.push(...(s.forbiddenPatterns ?? []));
316
318
  const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden).map(scoped);
319
+ // The upgradeable-consumer rules (#9191): the workspaces this scaffold
320
+ // creates and the in-scope package names it gives them. The site lint
321
+ // reads this block to refuse a forked, patched or pinned-off package.
322
+ // Every app the scaffold ships is named `@working-theory/<dir>`.
323
+ const upgradeableConsumer = {
324
+ package_scope: PACKAGE_SCOPE,
325
+ workspaces: spoke ? ["apps/*"] : ["apps/*", "database"],
326
+ scaffold_packages: [
327
+ ...(spoke ? [] : ["database"]),
328
+ ...Object.keys(apps),
329
+ ]
330
+ .map((dir) => `${PACKAGE_SCOPE}${dir}`)
331
+ .sort(),
332
+ };
317
333
  const doc = {
318
334
  version: ARCHITECTURE_BASE.version,
319
335
  ...(spoke ? { topology: "spoke" } : {}),
@@ -330,6 +346,7 @@ export function deriveArchitecture(modules = ["marketing"], surfaces, options =
330
346
  apps,
331
347
  ...(nsSlice ? { namespaces: namespacesToYaml(nsSlice) } : {}),
332
348
  forbidden_patterns: forbidden.map(forbiddenToYaml),
349
+ upgradeable_consumer: upgradeableConsumer,
333
350
  };
334
351
  const header = [
335
352
  "# Architecture Boundaries — AI Launch Kit site",
@@ -21,6 +21,39 @@ export declare const MAX_REDIRECTS = 5;
21
21
  * so each entry must answer exactly 404. A 200 means the gate stopped gating.
22
22
  */
23
23
  export declare const CLAIM_GATED_ROUTES: Readonly<Record<string, string>>;
24
+ /**
25
+ * Routes whose answer depends on whether the site has a hub to ask (#9423),
26
+ * and the flow each one sells. With no API origin configured the flow read
27
+ * is `not-configured` and the route must answer exactly 404 — the no-hub
28
+ * 404 where the module that owns it is installed, Next's own 404 where it
29
+ * is not. With one configured it must render its own content.
30
+ */
31
+ export declare const HUB_GATED_ROUTES: Readonly<Record<string, string>>;
32
+ /**
33
+ * The API origin names `apps/web` reads, in step with `apiOriginConfigured()`
34
+ * in `apps/web/app/[locale]/flows/[slug]/magnet-page.tsx`.
35
+ */
36
+ export declare const HUB_ORIGIN_ENV_NAMES: readonly ["AILK_API_URL", "NEXT_PUBLIC_AILK_API_URL", "NEXT_PUBLIC_API_URL"];
37
+ /**
38
+ * Whether a served `apps/web` has an API origin: a non-empty value for one
39
+ * of `HUB_ORIGIN_ENV_NAMES` in the serve environment or assigned in one of
40
+ * the env files Next loads (`envFileTexts`).
41
+ */
42
+ export declare function hubOriginConfigured(env: Readonly<Record<string, string | undefined>>, envFileTexts: string[]): boolean;
43
+ /**
44
+ * A `HUB_GATED_ROUTES` entry. Hub configured: the page must render its own
45
+ * content (`assessPageRender`). No hub: exactly 404 with no redirect — a 200
46
+ * means the no-hub branch stopped running.
47
+ */
48
+ export declare function assessHubGatedRoute(input: {
49
+ route: string;
50
+ hubConfigured: boolean;
51
+ hops: RedirectHop[];
52
+ status: number | null;
53
+ contentType: string | null;
54
+ html: string;
55
+ locale: string;
56
+ }): SweepVerdict;
24
57
  /**
25
58
  * A route whose live page needs an env var the driver never sets (#6146).
26
59
  * While it is unset the route serves its Free placeholder seed instead
@@ -37,6 +37,51 @@ export const CLAIM_GATED_ROUTES = {
37
37
  "/purchase/success": "commerce",
38
38
  "/purchase/cancel": "commerce",
39
39
  };
40
+ /**
41
+ * Routes whose answer depends on whether the site has a hub to ask (#9423),
42
+ * and the flow each one sells. With no API origin configured the flow read
43
+ * is `not-configured` and the route must answer exactly 404 — the no-hub
44
+ * 404 where the module that owns it is installed, Next's own 404 where it
45
+ * is not. With one configured it must render its own content.
46
+ */
47
+ export const HUB_GATED_ROUTES = {
48
+ "/trial": "trial",
49
+ };
50
+ /**
51
+ * The API origin names `apps/web` reads, in step with `apiOriginConfigured()`
52
+ * in `apps/web/app/[locale]/flows/[slug]/magnet-page.tsx`.
53
+ */
54
+ export const HUB_ORIGIN_ENV_NAMES = ["AILK_API_URL", "NEXT_PUBLIC_AILK_API_URL", "NEXT_PUBLIC_API_URL"];
55
+ /**
56
+ * Whether a served `apps/web` has an API origin: a non-empty value for one
57
+ * of `HUB_ORIGIN_ENV_NAMES` in the serve environment or assigned in one of
58
+ * the env files Next loads (`envFileTexts`).
59
+ */
60
+ export function hubOriginConfigured(env, envFileTexts) {
61
+ if (HUB_ORIGIN_ENV_NAMES.some((name) => Boolean(env[name])))
62
+ return true;
63
+ return envFileTexts.some((text) => HUB_ORIGIN_ENV_NAMES.some((name) => new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=\\s*["']?[^\\s"'#]`, "m").test(text)));
64
+ }
65
+ /**
66
+ * A `HUB_GATED_ROUTES` entry. Hub configured: the page must render its own
67
+ * content (`assessPageRender`). No hub: exactly 404 with no redirect — a 200
68
+ * means the no-hub branch stopped running.
69
+ */
70
+ export function assessHubGatedRoute(input) {
71
+ const { route, hubConfigured, hops, status, contentType, html, locale } = input;
72
+ const flow = HUB_GATED_ROUTES[route];
73
+ if (hubConfigured) {
74
+ const rendered = assessPageRender({ route, hops, status, contentType, html, locale });
75
+ return { ok: rendered.ok, reason: `${rendered.reason} (hub-gated: ${flow}, hub configured)` };
76
+ }
77
+ if (hops.length > 0 || status !== 404) {
78
+ const first = hops[0];
79
+ const seen = first ? `${first.status} redirect` : `status ${status}`;
80
+ const why = status === 200 ? "rendered with no hub to ask" : `expected 404, got ${seen}`;
81
+ return { ok: false, reason: `${route} -> ${why} (hub-gated: ${flow}, no hub)` };
82
+ }
83
+ return { ok: true, reason: `404 (hub-gated: ${flow}, no hub)` };
84
+ }
40
85
  /**
41
86
  * A route whose live page needs an env var the driver never sets (#6146).
42
87
  * While it is unset the route serves its Free placeholder seed instead
@@ -863,6 +863,13 @@ export const MODULE_SLICES = {
863
863
  // tenant-db.ts import, co-owned with `workspace` for the same reason.
864
864
  "apps/api/src/routes/workspaces/orders.ts",
865
865
  "apps/api/src/services/inbox-orders.ts",
866
+ // #9254 DD5 — the Inbox callbacks read: the same gate and the same
867
+ // tenant-db.ts import, co-owned with `workspace` for the same reason.
868
+ "apps/api/src/routes/workspaces/callbacks.ts",
869
+ // #9287 DD2 — Start, Get and Extend trial: the same gate and the same
870
+ // tenant-db.ts import, co-owned with `workspace` for the same reason.
871
+ "apps/api/src/routes/workspaces/trial.ts",
872
+ "apps/api/src/services/trial.ts",
866
873
  // #852 D8 — the People reads: the same workspace||multi-tenant gate and
867
874
  // the same tenant-db.ts import, co-owned with `workspace` for the same
868
875
  // reason.
@@ -1035,6 +1042,11 @@ export const MODULE_SLICES = {
1035
1042
  // Co-owned with `multi-tenant` (#9170), as the reads above.
1036
1043
  "apps/api/src/routes/workspaces/orders.ts",
1037
1044
  "apps/api/src/services/inbox-orders.ts",
1045
+ // Co-owned with `multi-tenant` (#9254 DD5), as the reads above.
1046
+ "apps/api/src/routes/workspaces/callbacks.ts",
1047
+ // Co-owned with `multi-tenant` (#9287 DD2), as the reads above.
1048
+ "apps/api/src/routes/workspaces/trial.ts",
1049
+ "apps/api/src/services/trial.ts",
1038
1050
  // Co-owned with `multi-tenant` (#852 D8), as the reads above.
1039
1051
  "apps/api/src/routes/workspaces/people.ts",
1040
1052
  // Co-owned with `multi-tenant` (#7877 D9, D11) — see that slice's entry.
@@ -1719,6 +1731,11 @@ export const MODULE_SLICES = {
1719
1731
  // its siblings (the note above).
1720
1732
  "apps/web/app/[locale]/careers/apply/page.tsx",
1721
1733
  "apps/web/app/[locale]/careers/[slug]/apply/page.tsx",
1734
+ // #9287 DD10 — the trial magnet pair. Its own `trial` route segment,
1735
+ // which no surface gates, so `ownedFiles` keeps both pages out of a
1736
+ // keyless scaffold, beside the careers walks.
1737
+ "apps/web/app/[locale]/trial/page.tsx",
1738
+ "apps/web/app/[locale]/trial/request/page.tsx",
1722
1739
  // #5581 U4 — the eleven signal-collectors components + the slice's own
1723
1740
  // re-export entry point moved to packages-paid/ui-signal-collectors;
1724
1741
  // the package is delivered to a keyed scaffold via `patches` below,
@@ -49,12 +49,23 @@ export interface SyncRoutesResult {
49
49
  missing: RouteReport[];
50
50
  /** Route dirs present but with a direct file absent or changed, sorted. */
51
51
  modified: RouteReport[];
52
- /** Route dirs copied this run (the missing list when applying, else empty). */
52
+ /** Route dirs copied this run (the missing list minus `refused` when applying, else empty). */
53
53
  applied: string[];
54
54
  /**
55
- * `ROUTE_DEPENDENCIES` files, of any route in `missing`, absent from the
56
- * target — copied when `apply` is set (never overwriting), else reported
57
- * only. Sorted, deduplicated.
55
+ * Missing routes `--apply` does not write because an import does not
56
+ * resolve (#9406 DD4) — reported in check mode too. Sorted.
57
+ */
58
+ refused: string[];
59
+ /**
60
+ * Per missing route, the local modules its files import (#9406 DD5): one
61
+ * entry per route with any non-empty list, sorted by route.
62
+ */
63
+ routeImports: RouteImportReport[];
64
+ /**
65
+ * Files, of any route in `missing` not `refused`, absent from the target:
66
+ * its `ROUTE_DEPENDENCIES` entries unioned with the `copies` its imports
67
+ * resolve to (#9406). Copied when `apply` is set (never overwriting), else
68
+ * reported only. Sorted, deduplicated.
58
69
  */
59
70
  missingDependencies: string[];
60
71
  /** Dependency files copied this run. Sorted. */
@@ -96,6 +107,21 @@ export interface RetiredCheckoutReport {
96
107
  /** Generated files rewritten (without `apply`: to rewrite) — the billing action. */
97
108
  rewritten: string[];
98
109
  }
110
+ /** One missing route's site-local imports (#9406 DD5) — see `walkRouteImports`. */
111
+ export interface RouteImportReport {
112
+ /** ROUTE_ROOT-relative, `/`-separated. */
113
+ route: string;
114
+ /** Template files the route reaches that the site lacks — copied with `--apply`. Sorted. */
115
+ copies: string[];
116
+ /** Files the route reaches that the site has, whose bytes differ from the template's — never written. Sorted. */
117
+ differs: string[];
118
+ /** Imports that resolve nowhere this sync can write; any entry refuses the route. Sorted by `from`, then `specifier`. */
119
+ unresolved: Array<{
120
+ specifier: string;
121
+ from: string;
122
+ reason: string;
123
+ }>;
124
+ }
99
125
  /** One `NO_STRUCTURED_DATA_ROUTES` element: its source text, dedented, and the routes it exempts. */
100
126
  interface StructuredDataExemption {
101
127
  route: string;