@open-mercato/cli 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7194.1.ab4fc81f82

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 (169) hide show
  1. package/.turbo/turbo-build.log +6 -2
  2. package/dist/agentic/github-copilot/copilot-instructions.md.template +37 -0
  3. package/dist/agentic/github-copilot/instructions/entity-guard.instructions.md +27 -0
  4. package/dist/agentic/github-copilot/instructions/generated-guard.instructions.md +10 -0
  5. package/dist/agentic/github-copilot/mcp.json.example +13 -0
  6. package/dist/agentic/guides/module-facts.json +11527 -283
  7. package/dist/agentic/guides/module-facts.v2.json +11527 -283
  8. package/dist/agentic/guides/modules/agent_orchestrator/acl-features.md +31 -0
  9. package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +12 -0
  10. package/dist/agentic/guides/modules/agent_orchestrator/ai-agents.md +12 -0
  11. package/dist/agentic/guides/modules/agent_orchestrator/ai-tools-mcp-capabilities.md +13 -0
  12. package/dist/agentic/guides/modules/agent_orchestrator/backend-pages.md +28 -0
  13. package/dist/agentic/guides/modules/agent_orchestrator/cli-commands.md +14 -0
  14. package/dist/agentic/guides/modules/agent_orchestrator/contribution-resolutions.md +49 -0
  15. package/dist/agentic/guides/modules/agent_orchestrator/di-registrations-rich.md +42 -0
  16. package/dist/agentic/guides/modules/agent_orchestrator/di-service-tokens.md +24 -0
  17. package/dist/agentic/guides/modules/agent_orchestrator/domain-commands.md +28 -0
  18. package/dist/agentic/guides/modules/agent_orchestrator/encryption.md +20 -0
  19. package/dist/agentic/guides/modules/agent_orchestrator/entities.md +31 -0
  20. package/dist/agentic/guides/modules/agent_orchestrator/events.md +38 -0
  21. package/dist/agentic/guides/modules/agent_orchestrator/exact-override-targets.md +122 -0
  22. package/dist/agentic/guides/modules/agent_orchestrator/host-extension-points.md +10 -0
  23. package/dist/agentic/guides/modules/agent_orchestrator/index.md +31 -0
  24. package/dist/agentic/guides/modules/agent_orchestrator/owned-contract-module-metadata.md +11 -0
  25. package/dist/agentic/guides/modules/agent_orchestrator/setup.md +11 -0
  26. package/dist/agentic/guides/modules/agent_orchestrator/umes-contributions.md +49 -0
  27. package/dist/agentic/guides/modules/agent_orchestrator/umes-hosts.md +61 -0
  28. package/dist/agentic/guides/modules/agent_orchestrator/workers.md +14 -0
  29. package/dist/agentic/guides/modules/ai_assistant/di-registrations-rich.md +3 -2
  30. package/dist/agentic/guides/modules/ai_assistant/di-service-tokens.md +2 -1
  31. package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -2
  32. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  33. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  34. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  35. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  36. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  37. package/dist/agentic/guides/modules/auth/index.md +1 -1
  38. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  39. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  40. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  41. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  42. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  43. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  44. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  45. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  46. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  47. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  48. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  49. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  50. package/dist/agentic/guides/modules/configs/index.md +1 -1
  51. package/dist/agentic/guides/modules/content/index.md +1 -1
  52. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  53. package/dist/agentic/guides/modules/customer_accounts/cli-commands.md +11 -0
  54. package/dist/agentic/guides/modules/customer_accounts/exact-override-targets.md +5 -4
  55. package/dist/agentic/guides/modules/customer_accounts/index.md +3 -2
  56. package/dist/agentic/guides/modules/customer_accounts/setup.md +1 -1
  57. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
  58. package/dist/agentic/guides/modules/customers/exact-override-targets.md +1 -0
  59. package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +3 -0
  60. package/dist/agentic/guides/modules/customers/index.md +2 -2
  61. package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
  62. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  63. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  64. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  65. package/dist/agentic/guides/modules/devices/index.md +1 -1
  66. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  67. package/dist/agentic/guides/modules/directory/index.md +1 -1
  68. package/dist/agentic/guides/modules/documents/index.md +1 -1
  69. package/dist/agentic/guides/modules/entities/index.md +1 -1
  70. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  71. package/dist/agentic/guides/modules/events/index.md +1 -1
  72. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  73. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  74. package/dist/agentic/guides/modules/generators/index.md +1 -1
  75. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  76. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  77. package/dist/agentic/guides/modules/messages/index.md +1 -1
  78. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  79. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  80. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  81. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  82. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  83. package/dist/agentic/guides/modules/planner/index.md +1 -1
  84. package/dist/agentic/guides/modules/portal/index.md +1 -1
  85. package/dist/agentic/guides/modules/progress/index.md +1 -1
  86. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  87. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  88. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  89. package/dist/agentic/guides/modules/resources/index.md +1 -1
  90. package/dist/agentic/guides/modules/sales/index.md +1 -1
  91. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  92. package/dist/agentic/guides/modules/search/index.md +1 -1
  93. package/dist/agentic/guides/modules/security/index.md +1 -1
  94. package/dist/agentic/guides/modules/seeds/cli-commands.md +14 -0
  95. package/dist/agentic/guides/modules/seeds/exact-override-targets.md +14 -0
  96. package/dist/agentic/guides/modules/seeds/index.md +14 -0
  97. package/dist/agentic/guides/modules/seeds/owned-contract-module-metadata.md +11 -0
  98. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  99. package/dist/agentic/guides/modules/sso/index.md +1 -1
  100. package/dist/agentic/guides/modules/staff/index.md +1 -1
  101. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  102. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  103. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  104. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  105. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  106. package/dist/agentic/guides/modules/translations/index.md +1 -1
  107. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  108. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  109. package/dist/agentic/guides/modules/wms/index.md +1 -1
  110. package/dist/agentic/guides/modules/workflows/acl-features.md +11 -1
  111. package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +4 -1
  112. package/dist/agentic/guides/modules/workflows/ai-tools-mcp-capabilities.md +17 -0
  113. package/dist/agentic/guides/modules/workflows/backend-pages.md +3 -0
  114. package/dist/agentic/guides/modules/workflows/contribution-resolutions.md +24 -0
  115. package/dist/agentic/guides/modules/workflows/di-registrations-rich.md +11 -7
  116. package/dist/agentic/guides/modules/workflows/di-service-tokens.md +11 -7
  117. package/dist/agentic/guides/modules/workflows/domain-commands.md +11 -0
  118. package/dist/agentic/guides/modules/workflows/entities.md +2 -0
  119. package/dist/agentic/guides/modules/workflows/events.md +14 -8
  120. package/dist/agentic/guides/modules/workflows/exact-override-targets.md +45 -10
  121. package/dist/agentic/guides/modules/workflows/frontend-pages.md +2 -0
  122. package/dist/agentic/guides/modules/workflows/incoming-installed-contributions.md +17 -0
  123. package/dist/agentic/guides/modules/workflows/index.md +15 -12
  124. package/dist/agentic/guides/modules/workflows/notifications.md +2 -0
  125. package/dist/agentic/guides/modules/workflows/setup.md +1 -1
  126. package/dist/agentic/guides/modules/workflows/umes-contributions.md +24 -0
  127. package/dist/agentic/guides/modules/workflows/umes-hosts.md +14 -6
  128. package/dist/agentic/guides/modules/workflows/workers.md +4 -1
  129. package/dist/agentic/guides/reference-module-facts.json +2 -2
  130. package/dist/agentic/guides/reference-modules/example/index.md +1 -1
  131. package/dist/agentic/guides/upstream/AGENTS.md +3 -1
  132. package/dist/agentic/guides/upstream/manifest.json +2 -2
  133. package/dist/agentic/shared/ai/harness/design-system-inventory.json +118 -106
  134. package/dist/agentic/shared/ai/harness/source-link-inventory.json +8 -8
  135. package/dist/lib/agentic-init.js +4 -0
  136. package/dist/lib/agentic-init.js.map +2 -2
  137. package/dist/lib/agentic-setup.js +19 -5
  138. package/dist/lib/agentic-setup.js.map +2 -2
  139. package/dist/lib/generators/extensions/agent-files.js +913 -0
  140. package/dist/lib/generators/extensions/agent-files.js.map +7 -0
  141. package/dist/lib/generators/extensions/index.js +2 -0
  142. package/dist/lib/generators/extensions/index.js.map +2 -2
  143. package/dist/lib/generators/index.js +3 -1
  144. package/dist/lib/generators/index.js.map +2 -2
  145. package/dist/lib/generators/module-registry.js +40 -11
  146. package/dist/lib/generators/module-registry.js.map +2 -2
  147. package/dist/lib/generators/route-collisions.js +35 -0
  148. package/dist/lib/generators/route-collisions.js.map +7 -0
  149. package/dist/lib/generators/web-research-adapters.js +95 -0
  150. package/dist/lib/generators/web-research-adapters.js.map +7 -0
  151. package/dist/mercato.js +5 -2
  152. package/dist/mercato.js.map +2 -2
  153. package/package.json +6 -6
  154. package/src/__tests__/agent-files.test.ts +199 -0
  155. package/src/__tests__/mercato.test.ts +14 -0
  156. package/src/lib/__tests__/agentic-init.test.ts +11 -0
  157. package/src/lib/__tests__/agentic-setup.ownership.test.ts +5 -0
  158. package/src/lib/agentic-init.ts +4 -0
  159. package/src/lib/agentic-setup.ts +30 -5
  160. package/src/lib/generators/__tests__/agent-files-extension.test.ts +477 -0
  161. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +25 -8
  162. package/src/lib/generators/__tests__/route-collisions.test.ts +155 -0
  163. package/src/lib/generators/extensions/agent-files.ts +1332 -0
  164. package/src/lib/generators/extensions/index.ts +2 -0
  165. package/src/lib/generators/index.ts +1 -0
  166. package/src/lib/generators/module-registry.ts +66 -17
  167. package/src/lib/generators/route-collisions.ts +57 -0
  168. package/src/lib/generators/web-research-adapters.ts +133 -0
  169. package/src/mercato.ts +4 -1
@@ -1,4 +1,5 @@
1
1
  import type { GeneratorExtension } from '../extension'
2
+ import { createAgentFilesExtension } from './agent-files'
2
3
  import { createAiAgentsExtension } from './ai-agents'
3
4
  import { createAiToolsExtension } from './ai-tools'
4
5
  import { createAnalyticsExtension } from './analytics'
@@ -25,6 +26,7 @@ export function loadGeneratorExtensions(): GeneratorExtension[] {
25
26
  createMessagesExtension(),
26
27
  createAiToolsExtension(),
27
28
  createAiAgentsExtension(),
29
+ createAgentFilesExtension(),
28
30
  createEventsExtension(),
29
31
  createAnalyticsExtension(),
30
32
  createTranslatableFieldsExtension(),
@@ -10,3 +10,4 @@ export { generateModuleEntities, type ModuleEntitiesOptions } from './module-ent
10
10
  export { generateModuleDi, type ModuleDiOptions } from './module-di'
11
11
  export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
12
12
  export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
13
+ export { generateWebResearchAdapters, type WebResearchAdaptersOptions } from './web-research-adapters'
@@ -59,6 +59,7 @@ import {
59
59
  namespaceFallback,
60
60
  namespaceImportSpec,
61
61
  } from './extensions/shared'
62
+ import { detectBackendRouteCollisions, type BackendRouteRegistration } from './route-collisions'
62
63
  import {
63
64
  scanModuleDir,
64
65
  resolveModuleFile,
@@ -118,6 +119,7 @@ type ModuleRegistryDiscoveryEntry = {
118
119
  roots: ModuleRoots
119
120
  imps: ModuleImports
120
121
  appImportBase: string
122
+ isAppModule: boolean
121
123
  resolve: (relativePath: string) => ResolvedFile | null
122
124
  frontendFiles: ScannedFile[]
123
125
  backendFiles: ScannedFile[]
@@ -171,22 +173,39 @@ type PageRouteGenerationResult = {
171
173
  // patterns collide only when a page is left un-namespaced (e.g. `backend/[id]`
172
174
  // instead of `backend/<mod>/[id]`), silently shadowing another module at runtime.
173
175
  // Mirrors the existing duplicate-command-id guard.
176
+ type BackendRoutePatternOwner = { moduleId: string; isAppModule: boolean }
177
+
174
178
  function assertUniqueBackendRoutePattern(
175
- seen: Map<string, string>,
179
+ seen: Map<string, BackendRoutePatternOwner>,
176
180
  moduleId: string,
177
181
  pattern: string,
182
+ isAppModule: boolean,
178
183
  ): void {
179
184
  if (!pattern.startsWith('/backend/')) return
180
185
  const previous = seen.get(pattern)
181
- if (previous && previous !== moduleId) {
182
- throw new Error(
183
- `[generate] Duplicate backend route pattern "${pattern}" generated by modules ` +
184
- `"${previous}" and "${moduleId}". Each backend page must resolve to a unique URL — ` +
185
- `nest the page under a module-specific folder (e.g. backend/${moduleId}/...) so it ` +
186
- `does not silently shadow another module.`,
187
- )
188
- }
189
- seen.set(pattern, moduleId)
186
+ if (previous && previous.moduleId !== moduleId) {
187
+ // An app-local module shadowing a package module's page is a supported
188
+ // override — the app entry wins at runtime, which is the whole point of
189
+ // putting a module in `apps/<app>/src/modules/`. Only a collision where
190
+ // neither side can predictably win is fatal: package-vs-package (no
191
+ // precedence between them) or app-vs-app (the same app twice).
192
+ const isIntentionalAppShadow = previous.isAppModule !== isAppModule
193
+ if (!isIntentionalAppShadow) {
194
+ const message =
195
+ `[generate] Duplicate backend route pattern "${pattern}" generated by modules ` +
196
+ `"${previous.moduleId}" and "${moduleId}". Each backend page must resolve to a unique URL — ` +
197
+ `nest the page under a module-specific folder (e.g. backend/${moduleId}/...) so it ` +
198
+ `does not silently shadow another module.`
199
+ // Print before throwing: this guard short-circuits the aggregate collision
200
+ // sweep that would otherwise report the offending path and modules, and an
201
+ // operator needs the detail whichever guard fires first.
202
+ console.error(message)
203
+ throw new Error(message)
204
+ }
205
+ }
206
+ // The app module owns the pattern once it shadows, so a package module seen
207
+ // afterwards is measured against the app entry rather than replacing it.
208
+ if (!previous || isAppModule) seen.set(pattern, { moduleId, isAppModule })
190
209
  }
191
210
 
192
211
  type ApiRouteGenerationResult = {
@@ -2056,6 +2075,7 @@ async function createModuleRegistryDiscovery(
2056
2075
  roots,
2057
2076
  imps,
2058
2077
  appImportBase,
2078
+ isAppModule: entry.from === '@app',
2059
2079
  resolve,
2060
2080
  frontendFiles: scanModuleDir(roots, SCAN_CONFIGS.frontendPages),
2061
2081
  backendFiles: scanModuleDir(roots, SCAN_CONFIGS.backendPages),
@@ -2121,9 +2141,10 @@ async function processPageFiles(options: {
2121
2141
  runtimeImports: string[]
2122
2142
  manifestImports?: string[]
2123
2143
  importIdRef: { value: number }
2144
+ collectRoute?: (registration: { routePath: string; fromApp: boolean }) => void
2124
2145
  quiet?: boolean
2125
2146
  }): Promise<PageRouteGenerationResult> {
2126
- const { files, type, modId, appDir, pkgDir, appImportBase, pkgImportBase, eagerImports, runtimeImports, manifestImports, importIdRef, quiet } = options
2147
+ const { files, type, modId, appDir, pkgDir, appImportBase, pkgImportBase, eagerImports, runtimeImports, manifestImports, importIdRef, collectRoute, quiet } = options
2127
2148
  const metaPrefix = type === 'frontend' ? 'M' : 'BM'
2128
2149
  const eagerRoutes: string[] = []
2129
2150
  const runtimeRoutes: string[] = []
@@ -2148,6 +2169,7 @@ async function processPageFiles(options: {
2148
2169
  const moduleBaseDir = path.join(fromApp ? appDir : pkgDir, ...segs)
2149
2170
  const sourceFile = findExistingModuleFile(moduleBaseDir, pageFile)
2150
2171
  if (!sourceFile || !hasDefaultExport(sourceFile)) continue
2172
+ collectRoute?.({ routePath, fromApp })
2151
2173
  const metaPath = findExistingModuleFileByBaseNames(moduleBaseDir, ['page.meta', 'meta'])
2152
2174
  warnIfPageMetaMissingMetadataExport(metaPath, quiet)
2153
2175
  let metaExpr = 'undefined'
@@ -2245,6 +2267,7 @@ async function processPageFiles(options: {
2245
2267
  const moduleBaseDir = path.join(fromApp ? appDir : pkgDir, ...segs)
2246
2268
  const sourceFile = findExistingModuleFile(moduleBaseDir, file)
2247
2269
  if (!sourceFile || !hasDefaultExport(sourceFile)) continue
2270
+ collectRoute?.({ routePath, fromApp })
2248
2271
  const metaPath = findExistingModuleFileByBaseNames(moduleBaseDir, [`${name}.meta`, 'meta'])
2249
2272
  let metaExpr = 'undefined'
2250
2273
  let runtimeMetaExpr = 'undefined'
@@ -3609,6 +3632,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3609
3632
  const importIdRef = { value: 0 }
3610
3633
  const requiresByModule = new Map<string, string[]>()
3611
3634
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3635
+ const backendRouteRegistrations: BackendRouteRegistration[] = []
3612
3636
  let hasRouteComponents = false
3613
3637
  let hasAbandonHookWorkers = false
3614
3638
 
@@ -3620,10 +3644,10 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3620
3644
  aclPath?: string
3621
3645
  }> = []
3622
3646
 
3623
- const seenBackendRoutePatterns = new Map<string, string>()
3647
+ const seenBackendRoutePatterns = new Map<string, BackendRoutePatternOwner>()
3624
3648
 
3625
3649
  for (const discovered of discovery.modules) {
3626
- const { modId, roots, imps, appImportBase } = discovered
3650
+ const { modId, roots, imps, appImportBase, isAppModule } = discovered
3627
3651
  commandLoaderEntries.push(...discovered.commandLoaderEntries)
3628
3652
 
3629
3653
  const frontendRoutes: string[] = []
@@ -3814,6 +3838,13 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3814
3838
  runtimeImports,
3815
3839
  manifestImports: backendRouteManifestImports,
3816
3840
  importIdRef,
3841
+ collectRoute: ({ routePath, fromApp }) => {
3842
+ backendRouteRegistrations.push({
3843
+ routePath,
3844
+ moduleId: modId,
3845
+ packageSourced: !fromApp && !isAppModule,
3846
+ })
3847
+ },
3817
3848
  quiet,
3818
3849
  })
3819
3850
  backendRoutes.push(...generatedBackendRoutes.eagerRoutes)
@@ -3822,7 +3853,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3822
3853
  backendRouteShardEntries.push(...generatedBackendRoutes.manifestShardRoutes)
3823
3854
  backendRouteMetadataEntries.push(...generatedBackendRoutes.metadataRoutes)
3824
3855
  for (const pattern of generatedBackendRoutes.routePatterns) {
3825
- assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
3856
+ assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern, isAppModule)
3826
3857
  }
3827
3858
  hasRouteComponents = true
3828
3859
  }
@@ -4111,6 +4142,24 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
4111
4142
  }
4112
4143
  }
4113
4144
 
4145
+ // Backend route collision detection: duplicate paths between package modules are
4146
+ // always a bug (one silently shadows the other at runtime); app-module shadowing
4147
+ // of a package page is the supported override mechanism and only logs a note.
4148
+ {
4149
+ const collisionResult = detectBackendRouteCollisions(backendRouteRegistrations)
4150
+ for (const note of collisionResult.notes) {
4151
+ if (!quiet) console.log(`[Route Shadow] ${note}`)
4152
+ }
4153
+ if (collisionResult.errors.length > 0) {
4154
+ for (const error of collisionResult.errors) {
4155
+ console.error(`\x1b[31m[Route Collision]\x1b[0m ${error}`)
4156
+ }
4157
+ throw new Error(
4158
+ `Backend route collision detection found ${collisionResult.errors.length} duplicate path(s) registered by multiple package modules. Rename the colliding page directories before proceeding.`
4159
+ )
4160
+ }
4161
+ }
4162
+
4114
4163
  const output = renderAstModuleRegistryFile({
4115
4164
  fileName: 'modules.generated.ts',
4116
4165
  generator: 'registry',
@@ -4326,10 +4375,10 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4326
4375
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
4327
4376
  let hasRouteComponents = false
4328
4377
  let hasAbandonHookWorkers = false
4329
- const seenBackendRoutePatterns = new Map<string, string>()
4378
+ const seenBackendRoutePatterns = new Map<string, BackendRoutePatternOwner>()
4330
4379
 
4331
4380
  for (const discovered of discovery.modules) {
4332
- const { modId, roots, imps, appImportBase } = discovered
4381
+ const { modId, roots, imps, appImportBase, isAppModule } = discovered
4333
4382
  commandLoaderEntries.push(...discovered.commandLoaderEntries)
4334
4383
 
4335
4384
  const frontendRoutes: WriterFunction[] = []
@@ -4460,7 +4509,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4460
4509
  })
4461
4510
  backendRoutes.push(...beAst.routes)
4462
4511
  for (const pattern of beAst.routePatterns) {
4463
- assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
4512
+ assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern, isAppModule)
4464
4513
  }
4465
4514
  hasRouteComponents = true
4466
4515
  }
@@ -0,0 +1,57 @@
1
+ export type BackendRouteRegistration = {
2
+ routePath: string
3
+ moduleId: string
4
+ packageSourced: boolean
5
+ }
6
+
7
+ export type BackendRouteCollisionResult = {
8
+ errors: string[]
9
+ notes: string[]
10
+ }
11
+
12
+ export function detectBackendRouteCollisions(
13
+ registrations: BackendRouteRegistration[],
14
+ ): BackendRouteCollisionResult {
15
+ const byPath = new Map<string, BackendRouteRegistration[]>()
16
+ for (const registration of registrations) {
17
+ const existing = byPath.get(registration.routePath)
18
+ if (existing) existing.push(registration)
19
+ else byPath.set(registration.routePath, [registration])
20
+ }
21
+
22
+ const errors: string[] = []
23
+ const notes: string[] = []
24
+ for (const [routePath, entries] of byPath) {
25
+ const moduleIds = new Set(entries.map((entry) => entry.moduleId))
26
+ if (moduleIds.size < 2) continue
27
+
28
+ const packageModuleIds = Array.from(
29
+ new Set(entries.filter((entry) => entry.packageSourced).map((entry) => entry.moduleId)),
30
+ ).sort((a, b) => a.localeCompare(b))
31
+
32
+ if (packageModuleIds.length >= 2) {
33
+ errors.push(
34
+ `Backend route "${routePath}" is registered by ${packageModuleIds.length} package modules: ${packageModuleIds
35
+ .map((id) => `"${id}"`)
36
+ .join(', ')}. Duplicate backend paths between package modules are always a bug — rename one module's page directory.`,
37
+ )
38
+ continue
39
+ }
40
+
41
+ const appModuleIds = Array.from(
42
+ new Set(entries.filter((entry) => !entry.packageSourced).map((entry) => entry.moduleId)),
43
+ ).sort((a, b) => a.localeCompare(b))
44
+ const shadowedId = packageModuleIds[0]
45
+ notes.push(
46
+ shadowedId
47
+ ? `Backend route "${routePath}" from package module "${shadowedId}" is shadowed by app module(s) ${appModuleIds
48
+ .map((id) => `"${id}"`)
49
+ .join(', ')} (intentional override mechanism).`
50
+ : `Backend route "${routePath}" is registered by multiple app modules: ${appModuleIds
51
+ .map((id) => `"${id}"`)
52
+ .join(', ')}.`,
53
+ )
54
+ }
55
+
56
+ return { errors, notes }
57
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { PackageResolver } from '../resolver'
4
+ import { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'
5
+
6
+ export interface WebResearchAdaptersOptions {
7
+ resolver: PackageResolver
8
+ quiet?: boolean
9
+ }
10
+
11
+ /**
12
+ * Mirrors `openMercato.webResearchAdapter` in `@open-mercato/web-research`. It is
13
+ * duplicated rather than imported because the CLI must not depend on the engine
14
+ * package to run its generators.
15
+ */
16
+ const MANIFEST_NAMESPACE = 'openMercato'
17
+ const ADAPTER_MANIFEST_KEY = 'webResearchAdapter'
18
+
19
+ type DiscoveredAdapter = {
20
+ packageName: string
21
+ adapterId: string
22
+ sourceRoot: string
23
+ }
24
+
25
+ function readJson(file: string): unknown {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ function readManifest(packageJsonPath: string): DiscoveredAdapter | null {
34
+ const parsed = readJson(packageJsonPath)
35
+ if (typeof parsed !== 'object' || parsed === null) return null
36
+ const pkg = parsed as Record<string, unknown>
37
+ const namespace = pkg[MANIFEST_NAMESPACE]
38
+ if (typeof namespace !== 'object' || namespace === null) return null
39
+ const manifest = (namespace as Record<string, unknown>)[ADAPTER_MANIFEST_KEY]
40
+ if (typeof manifest !== 'object' || manifest === null) return null
41
+ const adapterId = (manifest as Record<string, unknown>).id
42
+ const packageName = pkg.name
43
+ if (typeof packageName !== 'string' || packageName.length === 0) return null
44
+ if (typeof adapterId !== 'string' || adapterId.length === 0) return null
45
+ return { packageName, adapterId, sourceRoot: path.dirname(packageJsonPath) }
46
+ }
47
+
48
+ function scanDirectory(root: string, results: Map<string, DiscoveredAdapter>): void {
49
+ if (!fs.existsSync(root)) return
50
+ let entries: fs.Dirent[]
51
+ try {
52
+ entries = fs.readdirSync(root, { withFileTypes: true })
53
+ } catch {
54
+ return
55
+ }
56
+ for (const entry of entries) {
57
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
58
+ if (entry.name === '.bin' || entry.name === '.cache') continue
59
+ const child = path.join(root, entry.name)
60
+ // Scoped packages nest one level deeper (`@scope/name`).
61
+ if (entry.name.startsWith('@')) {
62
+ scanDirectory(child, results)
63
+ continue
64
+ }
65
+ const discovered = readManifest(path.join(child, 'package.json'))
66
+ if (discovered && !results.has(discovered.packageName)) results.set(discovered.packageName, discovered)
67
+ }
68
+ }
69
+
70
+ function identifierFor(index: number): string {
71
+ return `adapter${index}`
72
+ }
73
+
74
+ /**
75
+ * Emits the statically imported adapter registry. Discovery happens here, at
76
+ * build time, because a bundler cannot follow `import()` with a runtime-computed
77
+ * specifier — a dynamic registry would resolve to nothing under Turbopack.
78
+ */
79
+ export async function generateWebResearchAdapters(
80
+ options: WebResearchAdaptersOptions,
81
+ ): Promise<GeneratorResult> {
82
+ const { resolver, quiet } = options
83
+ const result = createGeneratorResult()
84
+ const outputDir = resolver.getOutputDir()
85
+ const outFile = path.join(outputDir, 'web-research-adapters.generated.ts')
86
+ const checksumFile = path.join(outputDir, 'web-research-adapters.checksum')
87
+
88
+ const appDir = resolver.getAppDir()
89
+ const repoRoot = path.resolve(appDir, '..', '..')
90
+ const discovered = new Map<string, DiscoveredAdapter>()
91
+ const scanRoots = [
92
+ path.join(repoRoot, 'packages'),
93
+ path.join(repoRoot, 'node_modules'),
94
+ path.join(appDir, 'node_modules'),
95
+ ]
96
+ for (const root of scanRoots) scanDirectory(root, discovered)
97
+
98
+ const adapters = [...discovered.values()].sort((left, right) =>
99
+ left.packageName.localeCompare(right.packageName),
100
+ )
101
+
102
+ const imports = adapters
103
+ .map((adapter, index) => `import * as ${identifierFor(index)} from '${adapter.packageName}'`)
104
+ .join('\n')
105
+ const entries = adapters
106
+ .map(
107
+ (adapter, index) =>
108
+ ` { packageName: '${adapter.packageName}', module: ${identifierFor(index)} },`,
109
+ )
110
+ .join('\n')
111
+
112
+ const content = `// AUTO-GENERATED — do not edit by hand.
113
+ // Source: packages declaring \`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\` in package.json.
114
+ // Regenerate with: yarn generate
115
+ import type { AdapterRegistryEntry } from '@open-mercato/web-research'
116
+ ${imports}
117
+
118
+ export const webResearchAdapterEntries: AdapterRegistryEntry[] = [
119
+ ${entries}
120
+ ]
121
+ `
122
+
123
+ writeGeneratedFile({
124
+ outFile,
125
+ checksumFile,
126
+ content,
127
+ structureChecksum: calculateStructureChecksum(adapters.map((adapter) => adapter.sourceRoot)),
128
+ result,
129
+ quiet,
130
+ })
131
+
132
+ return result
133
+ }
package/src/mercato.ts CHANGED
@@ -790,6 +790,7 @@ async function runGeneratorSuite(quiet: boolean): Promise<boolean> {
790
790
  generateModuleDi,
791
791
  generateModulePackageSources,
792
792
  generateOpenApi,
793
+ generateWebResearchAdapters,
793
794
  } = await import('./lib/generators')
794
795
  const resolver = createResolver()
795
796
  const results = [
@@ -798,6 +799,7 @@ async function runGeneratorSuite(quiet: boolean): Promise<boolean> {
798
799
  await generateModuleEntities({ resolver, quiet }),
799
800
  await generateModuleDi({ resolver, quiet }),
800
801
  await generateModulePackageSources({ resolver, quiet }),
802
+ await generateWebResearchAdapters({ resolver, quiet }),
801
803
  await generateOpenApi({ resolver, quiet }),
802
804
  ]
803
805
  return results.some((result) => (result?.filesWritten.length ?? 0) > 0)
@@ -1089,13 +1091,14 @@ export async function run(argv = process.argv) {
1089
1091
  // Step 1: Run generators directly (no process spawn)
1090
1092
  console.log('🔧 Preparing modules (registry, entities, DI)...')
1091
1093
  const { createResolver } = await import('./lib/resolver')
1092
- const { generateEntityIds, generateModuleRegistries, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi } = await import('./lib/generators')
1094
+ const { generateEntityIds, generateModuleRegistries, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi, generateWebResearchAdapters } = await import('./lib/generators')
1093
1095
  const resolver = createResolver()
1094
1096
  await generateEntityIds({ resolver, quiet: true })
1095
1097
  await generateModuleRegistries({ resolver, quiet: true })
1096
1098
  await generateModuleEntities({ resolver, quiet: true })
1097
1099
  await generateModuleDi({ resolver, quiet: true })
1098
1100
  await generateModulePackageSources({ resolver, quiet: true })
1101
+ await generateWebResearchAdapters({ resolver, quiet: true })
1099
1102
  await generateOpenApi({ resolver, quiet: true })
1100
1103
  console.log('✅ Modules prepared\n')
1101
1104