@open-mercato/cli 0.8.1-develop.7296.1.2111d779db → 0.8.1-develop.7441.1.b42958b98c

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 (217) hide show
  1. package/.turbo/turbo-build.log +8 -2
  2. package/AGENTS.md +2 -0
  3. package/dist/agentic/guides/module-facts.json +7614 -850
  4. package/dist/agentic/guides/module-facts.v2.json +7614 -850
  5. package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +1 -1
  6. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  7. package/dist/agentic/guides/modules/ai_assistant/encryption.md +13 -0
  8. package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -0
  9. package/dist/agentic/guides/modules/ai_assistant/index.md +4 -3
  10. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  11. package/dist/agentic/guides/modules/api_keys/domain-commands.md +12 -0
  12. package/dist/agentic/guides/modules/api_keys/index.md +3 -2
  13. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  14. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  15. package/dist/agentic/guides/modules/auth/index.md +1 -1
  16. package/dist/agentic/guides/modules/availability/acl-features.md +13 -0
  17. package/dist/agentic/guides/modules/availability/backend-pages.md +14 -0
  18. package/dist/agentic/guides/modules/availability/di-registrations-rich.md +11 -0
  19. package/dist/agentic/guides/modules/availability/domain-commands.md +13 -0
  20. package/dist/agentic/guides/modules/availability/entities.md +11 -0
  21. package/dist/agentic/guides/modules/availability/events.md +13 -0
  22. package/dist/agentic/guides/modules/availability/exact-override-targets.md +19 -0
  23. package/dist/agentic/guides/modules/availability/host-extension-points.md +10 -0
  24. package/dist/agentic/guides/modules/availability/index.md +22 -0
  25. package/dist/agentic/guides/modules/availability/owned-contract-module-metadata.md +11 -0
  26. package/dist/agentic/guides/modules/availability/setup.md +11 -0
  27. package/dist/agentic/guides/modules/availability/umes-hosts.md +14 -0
  28. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  29. package/dist/agentic/guides/modules/catalog/active-extension-bindings.md +1 -1
  30. package/dist/agentic/guides/modules/catalog/di-registrations-rich.md +1 -0
  31. package/dist/agentic/guides/modules/catalog/exact-override-targets.md +3 -0
  32. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  33. package/dist/agentic/guides/modules/catalog/workers.md +2 -0
  34. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  35. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  36. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  37. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  38. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  39. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  40. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  41. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  42. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  43. package/dist/agentic/guides/modules/communication_channels/acl-features.md +2 -1
  44. package/dist/agentic/guides/modules/communication_channels/contribution-resolutions.md +1 -0
  45. package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +11 -10
  46. package/dist/agentic/guides/modules/communication_channels/domain-commands.md +1 -0
  47. package/dist/agentic/guides/modules/communication_channels/events.md +2 -1
  48. package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +12 -10
  49. package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +1 -0
  50. package/dist/agentic/guides/modules/communication_channels/index.md +2 -2
  51. package/dist/agentic/guides/modules/communication_channels/umes-contributions.md +1 -0
  52. package/dist/agentic/guides/modules/communication_channels/umes-hosts.md +1 -0
  53. package/dist/agentic/guides/modules/configs/index.md +1 -1
  54. package/dist/agentic/guides/modules/content/index.md +1 -1
  55. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  56. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  57. package/dist/agentic/guides/modules/customer_groups/acl-features.md +16 -0
  58. package/dist/agentic/guides/modules/customer_groups/active-extension-bindings.md +13 -0
  59. package/dist/agentic/guides/modules/customer_groups/backend-pages.md +14 -0
  60. package/dist/agentic/guides/modules/customer_groups/cli-commands.md +11 -0
  61. package/dist/agentic/guides/modules/customer_groups/contribution-resolutions.md +13 -0
  62. package/dist/agentic/guides/modules/customer_groups/di-registrations-rich.md +13 -0
  63. package/dist/agentic/guides/modules/customer_groups/di-service-tokens.md +11 -0
  64. package/dist/agentic/guides/modules/customer_groups/domain-commands.md +11 -0
  65. package/dist/agentic/guides/modules/customer_groups/entities.md +13 -0
  66. package/dist/agentic/guides/modules/customer_groups/events.md +17 -0
  67. package/dist/agentic/guides/modules/customer_groups/exact-override-targets.md +27 -0
  68. package/dist/agentic/guides/modules/customer_groups/index.md +26 -0
  69. package/dist/agentic/guides/modules/customer_groups/owned-contract-module-metadata.md +11 -0
  70. package/dist/agentic/guides/modules/customer_groups/setup.md +11 -0
  71. package/dist/agentic/guides/modules/customer_groups/umes-contributions.md +13 -0
  72. package/dist/agentic/guides/modules/customer_groups/umes-hosts.md +21 -0
  73. package/dist/agentic/guides/modules/customers/acl-features.md +2 -1
  74. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +8 -7
  75. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
  76. package/dist/agentic/guides/modules/customers/domain-commands.md +1 -0
  77. package/dist/agentic/guides/modules/customers/entities.md +1 -0
  78. package/dist/agentic/guides/modules/customers/events.md +2 -1
  79. package/dist/agentic/guides/modules/customers/exact-override-targets.md +2 -0
  80. package/dist/agentic/guides/modules/customers/index.md +3 -3
  81. package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
  82. package/dist/agentic/guides/modules/customers/umes-hosts.md +3 -0
  83. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  84. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  85. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  86. package/dist/agentic/guides/modules/devices/index.md +1 -1
  87. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  88. package/dist/agentic/guides/modules/directory/active-extension-bindings.md +1 -1
  89. package/dist/agentic/guides/modules/directory/index.md +1 -1
  90. package/dist/agentic/guides/modules/documents/index.md +1 -1
  91. package/dist/agentic/guides/modules/entities/active-extension-bindings.md +1 -1
  92. package/dist/agentic/guides/modules/entities/index.md +1 -1
  93. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  94. package/dist/agentic/guides/modules/events/index.md +1 -1
  95. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  96. package/dist/agentic/guides/modules/forms/acl-features.md +16 -0
  97. package/dist/agentic/guides/modules/forms/active-extension-bindings.md +14 -0
  98. package/dist/agentic/guides/modules/forms/backend-pages.md +17 -0
  99. package/dist/agentic/guides/modules/forms/contribution-resolutions.md +21 -0
  100. package/dist/agentic/guides/modules/forms/di-registrations-rich.md +27 -0
  101. package/dist/agentic/guides/modules/forms/di-service-tokens.md +19 -0
  102. package/dist/agentic/guides/modules/forms/domain-commands.md +30 -0
  103. package/dist/agentic/guides/modules/forms/encryption.md +11 -0
  104. package/dist/agentic/guides/modules/forms/entities.md +21 -0
  105. package/dist/agentic/guides/modules/forms/events.md +28 -0
  106. package/dist/agentic/guides/modules/forms/exact-override-targets.md +59 -0
  107. package/dist/agentic/guides/modules/forms/frontend-pages.md +16 -0
  108. package/dist/agentic/guides/modules/forms/index.md +28 -0
  109. package/dist/agentic/guides/modules/forms/owned-contract-module-metadata.md +11 -0
  110. package/dist/agentic/guides/modules/forms/setup.md +11 -0
  111. package/dist/agentic/guides/modules/forms/umes-contributions.md +21 -0
  112. package/dist/agentic/guides/modules/forms/umes-hosts.md +43 -0
  113. package/dist/agentic/guides/modules/forms/workers.md +12 -0
  114. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  115. package/dist/agentic/guides/modules/generators/index.md +1 -1
  116. package/dist/agentic/guides/modules/inbox_ops/active-extension-bindings.md +1 -1
  117. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  118. package/dist/agentic/guides/modules/integrations/di-registrations-rich.md +8 -8
  119. package/dist/agentic/guides/modules/integrations/di-service-tokens.md +4 -4
  120. package/dist/agentic/guides/modules/integrations/exact-override-targets.md +8 -8
  121. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  122. package/dist/agentic/guides/modules/messages/index.md +1 -1
  123. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  124. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  125. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  126. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  127. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  128. package/dist/agentic/guides/modules/planner/active-extension-bindings.md +1 -1
  129. package/dist/agentic/guides/modules/planner/index.md +1 -1
  130. package/dist/agentic/guides/modules/portal/index.md +1 -1
  131. package/dist/agentic/guides/modules/progress/index.md +1 -1
  132. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  133. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  134. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  135. package/dist/agentic/guides/modules/resources/active-extension-bindings.md +1 -1
  136. package/dist/agentic/guides/modules/resources/index.md +1 -1
  137. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +1 -1
  138. package/dist/agentic/guides/modules/sales/exact-override-targets.md +4 -4
  139. package/dist/agentic/guides/modules/sales/index.md +1 -1
  140. package/dist/agentic/guides/modules/sales/setup.md +1 -1
  141. package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +1 -1
  142. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  143. package/dist/agentic/guides/modules/scheduler/workers.md +1 -1
  144. package/dist/agentic/guides/modules/search/index.md +1 -1
  145. package/dist/agentic/guides/modules/security/index.md +1 -1
  146. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  147. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  148. package/dist/agentic/guides/modules/sso/index.md +1 -1
  149. package/dist/agentic/guides/modules/staff/active-extension-bindings.md +1 -1
  150. package/dist/agentic/guides/modules/staff/index.md +1 -1
  151. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  152. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  153. package/dist/agentic/guides/modules/sync_excel/active-extension-bindings.md +11 -0
  154. package/dist/agentic/guides/modules/sync_excel/index.md +3 -2
  155. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  156. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  157. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  158. package/dist/agentic/guides/modules/translations/index.md +1 -1
  159. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  160. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  161. package/dist/agentic/guides/modules/wms/active-extension-bindings.md +1 -1
  162. package/dist/agentic/guides/modules/wms/contribution-resolutions.md +3 -0
  163. package/dist/agentic/guides/modules/wms/di-registrations-rich.md +8 -8
  164. package/dist/agentic/guides/modules/wms/exact-override-targets.md +11 -8
  165. package/dist/agentic/guides/modules/wms/index.md +3 -3
  166. package/dist/agentic/guides/modules/wms/umes-contributions.md +3 -0
  167. package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +3 -3
  168. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  169. package/dist/agentic/guides/reference-module-facts.json +1 -1
  170. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +73 -1
  171. package/dist/agentic/guides/upstream/manifest.json +2 -2
  172. package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +1 -1
  173. package/dist/bin.js +2 -2
  174. package/dist/bin.js.map +2 -2
  175. package/dist/lib/db/commands.js +84 -5
  176. package/dist/lib/db/commands.js.map +2 -2
  177. package/dist/lib/db/encryption-backfill-check.js +112 -0
  178. package/dist/lib/db/encryption-backfill-check.js.map +7 -0
  179. package/dist/lib/db/migration-reindex.js +2 -1
  180. package/dist/lib/db/migration-reindex.js.map +2 -2
  181. package/dist/lib/events-single-delivery.js.map +2 -2
  182. package/dist/lib/flush-before-exit.js +19 -0
  183. package/dist/lib/flush-before-exit.js.map +7 -0
  184. package/dist/lib/generate-watch-structure.js +113 -19
  185. package/dist/lib/generate-watch-structure.js.map +3 -3
  186. package/dist/lib/generators/module-registry.js +4 -2
  187. package/dist/lib/generators/module-registry.js.map +2 -2
  188. package/dist/lib/generators/web-research-adapters.js +32 -11
  189. package/dist/lib/generators/web-research-adapters.js.map +2 -2
  190. package/dist/lib/telemetry-init.js +69 -11
  191. package/dist/lib/telemetry-init.js.map +3 -3
  192. package/dist/lib/testing/integration.js +29 -2
  193. package/dist/lib/testing/integration.js.map +2 -2
  194. package/package.json +7 -6
  195. package/src/bin.ts +4 -4
  196. package/src/lib/__tests__/flush-before-exit.test.ts +32 -0
  197. package/src/lib/__tests__/generate-watch-structure.test.ts +261 -3
  198. package/src/lib/__tests__/telemetry-init.test.ts +89 -0
  199. package/src/lib/db/__tests__/commands.test.ts +43 -0
  200. package/src/lib/db/__tests__/dynamic-import-specifier.test.ts +36 -0
  201. package/src/lib/db/__tests__/encryption-backfill-check.test.ts +146 -0
  202. package/src/lib/db/__tests__/migration-reindex.test.ts +48 -3
  203. package/src/lib/db/commands.ts +113 -10
  204. package/src/lib/db/encryption-backfill-check.ts +163 -0
  205. package/src/lib/db/migration-reindex.ts +2 -1
  206. package/src/lib/events-single-delivery.ts +5 -2
  207. package/src/lib/flush-before-exit.ts +39 -0
  208. package/src/lib/generate-watch-structure.ts +134 -19
  209. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +14 -3
  210. package/src/lib/generators/__tests__/module-package-sources.test.ts +2 -2
  211. package/src/lib/generators/__tests__/registry-variant-parity.test.ts +16 -0
  212. package/src/lib/generators/__tests__/web-research-adapters.test.ts +141 -0
  213. package/src/lib/generators/module-registry.ts +2 -0
  214. package/src/lib/generators/web-research-adapters.ts +41 -20
  215. package/src/lib/telemetry-init.ts +68 -11
  216. package/src/lib/testing/__tests__/integration.test.ts +26 -0
  217. package/src/lib/testing/integration.ts +32 -2
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import {
5
5
  MODULE_CODE_EXTENSIONS,
6
6
  SCAN_CONFIGS,
7
+ resolveStandaloneSourceMirrorBase,
7
8
  scanModuleDir,
8
9
  stripModuleCodeExtension,
9
10
  type ModuleRoots,
@@ -66,6 +67,13 @@ function checksum(value: string): string {
66
67
  return crypto.createHash('md5').update(value).digest('hex')
67
68
  }
68
69
 
70
+ function moduleRootIdentity(roots: ModuleRoots): string {
71
+ return JSON.stringify([
72
+ roots.pkgBase.replace(/\\/g, '/'),
73
+ roots.appBase.replace(/\\/g, '/'),
74
+ ])
75
+ }
76
+
69
77
  function fileRecord(filePath: string, base: string, mode: 'content' | 'shape'): string | null {
70
78
  if (!fs.existsSync(filePath)) return null
71
79
  let stat: fs.Stats
@@ -86,6 +94,17 @@ function fileRecord(filePath: string, base: string, mode: 'content' | 'shape'):
86
94
  }
87
95
  }
88
96
 
97
+ function addFileRecord(
98
+ records: string[],
99
+ prefix: string,
100
+ filePath: string,
101
+ base: string,
102
+ mode: 'content' | 'shape',
103
+ ): void {
104
+ const record = fileRecord(filePath, base, mode)
105
+ if (record) records.push(`${prefix}:${record}`)
106
+ }
107
+
89
108
  function resolveCodeFile(base: string, relativePath: string): string | null {
90
109
  const stripped = stripModuleCodeExtension(relativePath)
91
110
  const candidates = MODULE_CODE_EXTENSIONS.map((extension) => `${stripped}${extension}`)
@@ -96,6 +115,16 @@ function resolveCodeFile(base: string, relativePath: string): string | null {
96
115
  return null
97
116
  }
98
117
 
118
+ function resolveRuntimeCounterpart(
119
+ pkgBase: string,
120
+ sourceBase: string,
121
+ sourceFile: string,
122
+ ): string | null {
123
+ if (sourceBase === pkgBase) return null
124
+ const relativePath = path.relative(sourceBase, sourceFile).replace(/\\/g, '/')
125
+ return resolveCodeFile(pkgBase, relativePath)
126
+ }
127
+
99
128
  function hasInlinePageMetadata(filePath: string): boolean {
100
129
  try {
101
130
  const source = fs.readFileSync(filePath, 'utf8')
@@ -107,48 +136,131 @@ function hasInlinePageMetadata(filePath: string): boolean {
107
136
  }
108
137
 
109
138
  function addConventionRecords(records: string[], roots: ModuleRoots): void {
139
+ const packageSourceBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
110
140
  for (const base of [roots.pkgBase, roots.appBase]) {
111
141
  records.push(`module-root:${base}:${fs.existsSync(base) ? 'present' : 'missing'}`)
112
- for (const relativePath of STRUCTURAL_CONVENTION_FILES) {
113
- const filePath = resolveCodeFile(base, relativePath)
114
- if (!filePath) {
115
- records.push(`missing:${base}:${relativePath}`)
116
- continue
142
+ }
143
+ if (packageSourceBase !== roots.pkgBase) {
144
+ records.push(
145
+ `module-root:${packageSourceBase}:${fs.existsSync(packageSourceBase) ? 'present' : 'missing'}`,
146
+ )
147
+ }
148
+
149
+ for (const relativePath of STRUCTURAL_CONVENTION_FILES) {
150
+ const appFile = resolveCodeFile(roots.appBase, relativePath)
151
+ if (appFile) {
152
+ addFileRecord(records, 'convention:app', appFile, roots.appBase, 'content')
153
+ continue
154
+ }
155
+
156
+ const sourceFile = resolveCodeFile(packageSourceBase, relativePath)
157
+ if (!sourceFile) {
158
+ const runtimeFile = packageSourceBase === roots.pkgBase
159
+ ? null
160
+ : resolveCodeFile(roots.pkgBase, relativePath)
161
+ if (runtimeFile) {
162
+ addFileRecord(records, 'convention:package-runtime', runtimeFile, roots.pkgBase, 'content')
163
+ } else {
164
+ records.push(`convention:missing:${relativePath}`)
117
165
  }
118
- const record = fileRecord(filePath, base, 'content')
119
- if (record) records.push(record)
166
+ continue
167
+ }
168
+
169
+ const packagePrefix = packageSourceBase === roots.pkgBase ? 'package' : 'package-source'
170
+ addFileRecord(records, `convention:${packagePrefix}`, sourceFile, packageSourceBase, 'content')
171
+ const runtimeFile = resolveRuntimeCounterpart(roots.pkgBase, packageSourceBase, sourceFile)
172
+ if (runtimeFile) {
173
+ addFileRecord(records, 'convention:package-runtime', runtimeFile, roots.pkgBase, 'content')
174
+ } else if (packageSourceBase !== roots.pkgBase) {
175
+ records.push(`convention:package-runtime:missing:${relativePath}`)
120
176
  }
121
177
  }
122
178
  }
123
179
 
124
180
  function addScannedRecords(records: string[], roots: ModuleRoots): void {
181
+ const packageSourceBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
125
182
  for (const config of CONTENT_SENSITIVE_SCAN_CONFIGS) {
126
183
  for (const scanned of scanModuleDir(roots, config)) {
127
- const base = scanned.fromApp ? roots.appBase : roots.pkgBase
184
+ const base = scanned.fromApp ? roots.appBase : packageSourceBase
128
185
  const filePath = path.join(base, ...config.folder.split('/'), ...scanned.relPath.split('/'))
129
- const record = fileRecord(filePath, base, 'content')
130
- if (record) records.push(`${config.folder}:${record}`)
186
+ let sourceOrigin = 'app'
187
+ if (!scanned.fromApp) {
188
+ sourceOrigin = packageSourceBase === roots.pkgBase ? 'package' : 'package-source'
189
+ }
190
+ addFileRecord(records, `${config.folder}:${sourceOrigin}`, filePath, base, 'content')
191
+
192
+ if (!scanned.fromApp && packageSourceBase !== roots.pkgBase) {
193
+ const runtimeFile = resolveRuntimeCounterpart(roots.pkgBase, packageSourceBase, filePath)
194
+ if (runtimeFile) {
195
+ addFileRecord(
196
+ records,
197
+ `${config.folder}:package-runtime`,
198
+ runtimeFile,
199
+ roots.pkgBase,
200
+ 'content',
201
+ )
202
+ } else {
203
+ records.push(`${config.folder}:package-runtime:missing:${scanned.relPath}`)
204
+ }
205
+ }
131
206
  }
132
207
  }
133
208
 
134
209
  for (const config of ROUTE_SHAPE_SCAN_CONFIGS) {
135
210
  for (const scanned of scanModuleDir(roots, config)) {
136
- const base = scanned.fromApp ? roots.appBase : roots.pkgBase
211
+ const base = scanned.fromApp ? roots.appBase : packageSourceBase
137
212
  const folderPath = path.join(base, ...config.folder.split('/'))
138
213
  const filePath = path.join(folderPath, ...scanned.relPath.split('/'))
139
- const pageRecord = fileRecord(filePath, base, hasInlinePageMetadata(filePath) ? 'content' : 'shape')
140
- if (pageRecord) records.push(`${config.folder}:${pageRecord}`)
214
+ let sourceOrigin = 'app'
215
+ if (!scanned.fromApp) {
216
+ sourceOrigin = packageSourceBase === roots.pkgBase ? 'package' : 'package-source'
217
+ }
218
+ addFileRecord(
219
+ records,
220
+ `${config.folder}:${sourceOrigin}`,
221
+ filePath,
222
+ base,
223
+ hasInlinePageMetadata(filePath) ? 'content' : 'shape',
224
+ )
225
+
226
+ let runtimeFile: string | null = null
227
+ if (!scanned.fromApp && packageSourceBase !== roots.pkgBase) {
228
+ runtimeFile = resolveRuntimeCounterpart(roots.pkgBase, packageSourceBase, filePath)
229
+ if (runtimeFile) {
230
+ addFileRecord(
231
+ records,
232
+ `${config.folder}:package-runtime`,
233
+ runtimeFile,
234
+ roots.pkgBase,
235
+ hasInlinePageMetadata(runtimeFile) ? 'content' : 'shape',
236
+ )
237
+ } else {
238
+ records.push(`${config.folder}:package-runtime:missing:${scanned.relPath}`)
239
+ }
240
+ }
141
241
 
142
- const dir = path.dirname(filePath)
143
242
  const stem = stripModuleCodeExtension(path.basename(filePath))
144
243
  const metaCandidates = stem === 'page'
145
244
  ? ['page.meta', 'meta']
146
245
  : [`${stem}.meta`, 'meta']
147
246
  for (const candidate of metaCandidates) {
148
- const metaPath = resolveCodeFile(dir, candidate)
247
+ const metaPath = resolveCodeFile(path.dirname(filePath), candidate)
149
248
  if (!metaPath) continue
150
- const record = fileRecord(metaPath, base, 'content')
151
- if (record) records.push(`${config.folder}:meta:${record}`)
249
+ addFileRecord(records, `${config.folder}:meta:${sourceOrigin}`, metaPath, base, 'content')
250
+ }
251
+
252
+ if (runtimeFile) {
253
+ for (const candidate of metaCandidates) {
254
+ const runtimeMetaPath = resolveCodeFile(path.dirname(runtimeFile), candidate)
255
+ if (!runtimeMetaPath) continue
256
+ addFileRecord(
257
+ records,
258
+ `${config.folder}:meta:package-runtime`,
259
+ runtimeMetaPath,
260
+ roots.pkgBase,
261
+ 'content',
262
+ )
263
+ }
152
264
  }
153
265
  }
154
266
  }
@@ -163,8 +275,11 @@ export function calculateGenerateWatchStructureChecksum(options: {
163
275
  records.push(modulesRecord ?? `missing:${options.modulesFile}`)
164
276
 
165
277
  for (const roots of options.moduleRoots) {
166
- addConventionRecords(records, roots)
167
- addScannedRecords(records, roots)
278
+ const moduleRecords: string[] = []
279
+ addConventionRecords(moduleRecords, roots)
280
+ addScannedRecords(moduleRecords, roots)
281
+ const rootIdentity = moduleRootIdentity(roots)
282
+ records.push(...moduleRecords.map((record) => `${rootIdentity}:${record}`))
168
283
  }
169
284
 
170
285
  return checksum(records.sort((a, b) => a.localeCompare(b)).join('\n'))
@@ -18,7 +18,11 @@ describe('module-facts customers fixture (T1 anti-drift guard)', () => {
18
18
 
19
19
  it('locks the source-derived customers entity surface in colon-form ids', () => {
20
20
  expect(facts.module).toBe('customers')
21
- expect(facts.entities).toHaveLength(25)
21
+ // 26 since the email conversation-share feature added
22
+ // `CustomerEmailConversationShare` (the read grant backing owner-controlled
23
+ // conversation sharing). Bump deliberately: this guard exists to make an
24
+ // entity-surface change visible, not to be silenced.
25
+ expect(facts.entities).toHaveLength(26)
22
26
  expect(facts.entities.every((entity) => entity.id.startsWith('customers:'))).toBe(true)
23
27
  expect(facts.entities[0]).toMatchObject({ id: 'customers:customer_entity' })
24
28
  for (const entity of facts.entities) {
@@ -31,8 +35,15 @@ describe('module-facts customers fixture (T1 anti-drift guard)', () => {
31
35
  // 50 since #5114 added `customers.person.company_assignment.detached` — the profile-only
32
36
  // sibling of the person_company_link CRUD events. Bump deliberately: this guard exists to
33
37
  // make an event-surface change visible, not to be silenced.
34
- expect(facts.events).toHaveLength(50)
35
- expect(facts.aclFeatures).toHaveLength(21)
38
+ //
39
+ // 51 since the email conversation-share feature added
40
+ // `customers.email.conversation_visibility_changed` (audit trail for the
41
+ // owner-controlled share/un-share act), and 22 ACL features since it added
42
+ // `customers.email.share_conversation` (owner-scoped: it only ever lets you
43
+ // share your OWN conversation, so the existing `customers.*` admin wildcard
44
+ // grants no escalation).
45
+ expect(facts.events).toHaveLength(51)
46
+ expect(facts.aclFeatures).toHaveLength(22)
36
47
  expect(facts.searchEntities).toEqual([
37
48
  'customers:customer_person_profile',
38
49
  'customers:customer_company_profile',
@@ -86,7 +86,7 @@ describe('generateModulePackageSources', () => {
86
86
  await generateModulePackageSources({ resolver, quiet: true })
87
87
  const outputBefore = fs.readFileSync(outFile, 'utf8')
88
88
  const checksumBefore = readChecksumRecord(checksumFile)
89
- const pinnedTime = new Date(Date.now() - 60_000)
89
+ const pinnedTime = new Date('2020-01-01T00:00:00.000Z')
90
90
  fs.utimesSync(outFile, pinnedTime, pinnedTime)
91
91
  fs.utimesSync(checksumFile, pinnedTime, pinnedTime)
92
92
 
@@ -121,7 +121,7 @@ describe('generateModulePackageSources', () => {
121
121
  await generateModulePackageSources({ resolver: initialResolver, quiet: true })
122
122
  const outputBefore = fs.readFileSync(outFile, 'utf8')
123
123
  const initialStructure = readChecksumRecord(checksumFile)?.structure
124
- const pinnedTime = new Date(Date.now() - 60_000)
124
+ const pinnedTime = new Date('2020-01-01T00:00:00.000Z')
125
125
  fs.utimesSync(outFile, pinnedTime, pinnedTime)
126
126
 
127
127
  await generateModulePackageSources({ resolver: alternateResolver, quiet: true })
@@ -263,6 +263,22 @@ describe('module registry variant parity', () => {
263
263
  expect(carriesRuntime).toEqual(registryFiles.map((fileName) => [fileName, true, false]))
264
264
  })
265
265
 
266
+ // The IIFE `namespaceFallback` emits narrows `unknown` to `{}`. That satisfies `setup`, whose
267
+ // every member is optional, but `ModuleRuntime.start` is required — so without a cast the three
268
+ // namespace-lookup registries fail `tsc` the moment any module ships a runtime.ts. `generate`
269
+ // stays green either way, which is what let this ship.
270
+ it('casts the emitted runtime, so its required `start` does not break typecheck', () => {
271
+ const namespaceLookupRegistries = [
272
+ 'modules.app.generated.ts',
273
+ 'modules.bootstrap.generated.ts',
274
+ 'modules.cli.generated.ts',
275
+ ]
276
+
277
+ for (const fileName of namespaceLookupRegistries) {
278
+ expect(readGenerated(fileName)).toContain("as Module['runtime']")
279
+ }
280
+ })
281
+
266
282
  describe('known, intentional divergences', () => {
267
283
  it('only the main variant carries eager api handlers and the cli entry point', () => {
268
284
  expect(mainVariant.get('orders')!.has('apis')).toBe(true)
@@ -0,0 +1,141 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import ts from 'typescript-js'
6
+ import type { PackageResolver } from '../../resolver'
7
+ import { generateWebResearchAdapters } from '../web-research-adapters'
8
+
9
+ let tmpRoot: string
10
+ let appDir: string
11
+ let outputDir: string
12
+
13
+ function createMockResolver(): PackageResolver {
14
+ return {
15
+ getAppDir: () => appDir,
16
+ getOutputDir: () => outputDir,
17
+ } as unknown as PackageResolver
18
+ }
19
+
20
+ function writeAdapterPackage(
21
+ packageDir: string,
22
+ packageName: string,
23
+ adapterId: string,
24
+ source = `export const id = '${adapterId}'\n`,
25
+ ): void {
26
+ fs.mkdirSync(packageDir, { recursive: true })
27
+ fs.writeFileSync(
28
+ path.join(packageDir, 'package.json'),
29
+ JSON.stringify({
30
+ name: packageName,
31
+ type: 'module',
32
+ exports: './index.js',
33
+ openMercato: { webResearchAdapter: { id: adapterId } },
34
+ }),
35
+ )
36
+ fs.writeFileSync(path.join(packageDir, 'index.js'), source)
37
+ }
38
+
39
+ function writeWorkspaceOnlyAdapter(directoryName: string, packageName: string, adapterId: string): void {
40
+ writeAdapterPackage(path.join(tmpRoot, 'packages', directoryName), packageName, adapterId)
41
+ }
42
+
43
+ function writeInstalledAdapter(packageName: string, adapterId: string, source?: string): void {
44
+ writeAdapterPackage(path.join(tmpRoot, 'node_modules', ...packageName.split('/')), packageName, adapterId, source)
45
+ }
46
+
47
+ async function generate(): Promise<string> {
48
+ await generateWebResearchAdapters({ resolver: createMockResolver(), quiet: true })
49
+ return fs.readFileSync(path.join(outputDir, 'web-research-adapters.generated.ts'), 'utf8')
50
+ }
51
+
52
+ function runGeneratedRegistry(content: string): { status: number | null; stdout: string; stderr: string } {
53
+ const compiled = ts.transpileModule(content, {
54
+ compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
55
+ }).outputText
56
+ const registryFile = path.join(outputDir, 'web-research-adapters.generated.mjs')
57
+ fs.writeFileSync(registryFile, compiled)
58
+ const script = [
59
+ `const { webResearchAdapterEntries } = await import(${JSON.stringify(registryFile)})`,
60
+ 'console.log(JSON.stringify(webResearchAdapterEntries.map((entry) => [entry.packageName, entry.module.id])))',
61
+ ].join('\n')
62
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
63
+ cwd: appDir,
64
+ encoding: 'utf8',
65
+ })
66
+ return { status: result.status, stdout: result.stdout.trim(), stderr: result.stderr }
67
+ }
68
+
69
+ beforeEach(() => {
70
+ tmpRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'web-research-adapters-test-')))
71
+ appDir = path.join(tmpRoot, 'apps', 'mercato')
72
+ outputDir = path.join(appDir, '.mercato', 'generated')
73
+ fs.mkdirSync(outputDir, { recursive: true })
74
+ })
75
+
76
+ afterEach(() => {
77
+ fs.rmSync(tmpRoot, { recursive: true, force: true })
78
+ })
79
+
80
+ describe('generateWebResearchAdapters', () => {
81
+ it('emits an empty registry when no adapter package is discovered', async () => {
82
+ const content = await generate()
83
+
84
+ expect(content).toContain('export const webResearchAdapterEntries: AdapterRegistryEntry[] = []')
85
+ expect(content).not.toMatch(/import\(/)
86
+ })
87
+
88
+ it('imports every discovered adapter with a literal specifier and no static import', async () => {
89
+ writeInstalledAdapter('@acme/web-research-installed', 'installed')
90
+ writeWorkspaceOnlyAdapter('web-research-workspace-only', '@acme/web-research-workspace-only', 'workspace-only')
91
+
92
+ const content = await generate()
93
+
94
+ expect(content).toContain("import('@acme/web-research-installed')")
95
+ expect(content).toContain("import('@acme/web-research-workspace-only')")
96
+ expect(content).not.toMatch(/^import \* as /m)
97
+ })
98
+
99
+ it('loads installed adapters and skips workspace packages the app cannot resolve', async () => {
100
+ writeInstalledAdapter('@acme/web-research-installed', 'installed')
101
+ writeWorkspaceOnlyAdapter('web-research-workspace-only', '@acme/web-research-workspace-only', 'workspace-only')
102
+
103
+ const result = runGeneratedRegistry(await generate())
104
+
105
+ expect(result.stderr).toBe('')
106
+ expect(result.status).toBe(0)
107
+ expect(JSON.parse(result.stdout)).toEqual([['@acme/web-research-installed', 'installed']])
108
+ })
109
+
110
+ it('still surfaces an installed adapter that fails while loading', async () => {
111
+ writeInstalledAdapter('@acme/web-research-broken', 'broken', "throw new Error('adapter exploded')\n")
112
+
113
+ const result = runGeneratedRegistry(await generate())
114
+
115
+ expect(result.status).not.toBe(0)
116
+ expect(result.stderr).toContain('adapter exploded')
117
+ })
118
+
119
+ it('still surfaces an installed adapter whose own dependency is missing', async () => {
120
+ writeInstalledAdapter(
121
+ '@acme/web-research-needs-dep',
122
+ 'needs-dep',
123
+ "import '@acme/missing-adapter-dependency'\nexport const id = 'needs-dep'\n",
124
+ )
125
+
126
+ const result = runGeneratedRegistry(await generate())
127
+
128
+ expect(result.status).not.toBe(0)
129
+ expect(result.stderr).toContain('@acme/missing-adapter-dependency')
130
+ })
131
+
132
+ it('still surfaces an installed adapter whose entry file is missing', async () => {
133
+ writeInstalledAdapter('@acme/web-research-unbuilt', 'unbuilt')
134
+ fs.rmSync(path.join(tmpRoot, 'node_modules', '@acme', 'web-research-unbuilt', 'index.js'))
135
+
136
+ const result = runGeneratedRegistry(await generate())
137
+
138
+ expect(result.status).not.toBe(0)
139
+ expect(result.stderr).toMatch(/Cannot find module/)
140
+ })
141
+ })
@@ -4624,6 +4624,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4624
4624
  importName: moduleRuntimeImportName,
4625
4625
  members: ['default', 'runtime'],
4626
4626
  fallback: identifier('undefined'),
4627
+ castType: "Module['runtime']",
4627
4628
  }),
4628
4629
  })
4629
4630
  }
@@ -5035,6 +5036,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
5035
5036
  importName: moduleRuntimeImportName,
5036
5037
  members: ['default', 'runtime'],
5037
5038
  fallback: identifier('undefined'),
5039
+ castType: "Module['runtime']",
5038
5040
  }),
5039
5041
  })
5040
5042
  }
@@ -67,14 +67,48 @@ function scanDirectory(root: string, results: Map<string, DiscoveredAdapter>): v
67
67
  }
68
68
  }
69
69
 
70
- function identifierFor(index: number): string {
71
- return `adapter${index}`
70
+ function renderEntries(adapters: readonly DiscoveredAdapter[]): string {
71
+ if (adapters.length === 0) return 'export const webResearchAdapterEntries: AdapterRegistryEntry[] = []\n'
72
+ const loaders = adapters
73
+ .map(
74
+ (adapter) =>
75
+ ` loadAdapterEntry('${adapter.packageName}', () => import('${adapter.packageName}')),`,
76
+ )
77
+ .join('\n')
78
+ return `const MISSING_MODULE_CODES = new Set(['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'])
79
+
80
+ async function loadAdapterEntry(
81
+ packageName: string,
82
+ load: () => Promise<unknown>,
83
+ ): Promise<AdapterRegistryEntry | null> {
84
+ try {
85
+ return { packageName, module: await load() }
86
+ } catch (error) {
87
+ const { code, message } = (error ?? {}) as { code?: unknown; message?: unknown }
88
+ const isMissingCode = typeof code === 'string' && MISSING_MODULE_CODES.has(code)
89
+ const namesThisPackage = typeof message === 'string' && message.includes(\`'\${packageName}'\`)
90
+ if (isMissingCode && namesThisPackage) return null
91
+ throw error
92
+ }
93
+ }
94
+
95
+ const loadedEntries = await Promise.all([
96
+ ${loaders}
97
+ ])
98
+
99
+ export const webResearchAdapterEntries: AdapterRegistryEntry[] = loadedEntries.filter(
100
+ (entry): entry is AdapterRegistryEntry => entry !== null,
101
+ )
102
+ `
72
103
  }
73
104
 
74
105
  /**
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.
106
+ * Emits the adapter registry. Discovery happens here, at build time, because a
107
+ * bundler cannot follow `import()` with a runtime-computed specifier — a dynamic
108
+ * registry would resolve to nothing under Turbopack. Each adapter is imported
109
+ * with a literal specifier and a package that is not installed where the
110
+ * registry runs is skipped: discovery scans the whole workspace, but a process
111
+ * such as a worker in a production image only has the app's own dependencies.
78
112
  */
79
113
  export async function generateWebResearchAdapters(
80
114
  options: WebResearchAdaptersOptions,
@@ -99,26 +133,13 @@ export async function generateWebResearchAdapters(
99
133
  left.packageName.localeCompare(right.packageName),
100
134
  )
101
135
 
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
136
  const content = `// AUTO-GENERATED — do not edit by hand.
113
137
  // Source: packages declaring \`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\` in package.json.
114
138
  // Regenerate with: yarn generate
139
+ // Adapter packages that are not installed where this registry runs are skipped.
115
140
  import type { AdapterRegistryEntry } from '@open-mercato/web-research'
116
- ${imports}
117
141
 
118
- export const webResearchAdapterEntries: AdapterRegistryEntry[] = [
119
- ${entries}
120
- ]
121
- `
142
+ ${renderEntries(adapters)}`
122
143
 
123
144
  writeGeneratedFile({
124
145
  outFile,
@@ -102,17 +102,40 @@ export async function register(): Promise<void> {
102
102
  // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set). OTEL's NodeSDK
103
103
  // is Node-only and incompatible with the edge runtime, so the telemetry
104
104
  // bootstrap — which can pull in the SDK — is imported only on the Node.js
105
- // runtime. The helper owns init + graceful degrade + shutdown flush.
105
+ // runtime. The helper owns init + graceful degrade + shutdown flush; a
106
+ // deliberate configuration failure bubbles here so the host exits loudly.
106
107
  if (
107
108
  process.env.NEXT_RUNTIME === 'nodejs'
108
109
  && isTelemetryBackendEnabled()
109
110
  ) {
110
111
  const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')
111
- await registerTelemetryForNextjs()
112
+ try {
113
+ await registerTelemetryForNextjs()
114
+ } catch (err) {
115
+ const nodeProcess = process
116
+ nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)
117
+ nodeProcess.exit(1)
118
+ }
112
119
  }
113
120
  }
114
121
  `
115
122
 
123
+ const MULTILINE_TELEMETRY_GUARD = ` if (
124
+ process.env.NEXT_RUNTIME === 'nodejs'
125
+ && isTelemetryBackendEnabled()
126
+ ) {`
127
+ const INLINE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`
128
+ const LEGACY_NODE_TELEMETRY_GUARD = ` if (process.env.NEXT_RUNTIME === 'nodejs') {`
129
+ const TELEMETRY_DYNAMIC_IMPORT = ` const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')`
130
+ const BARE_TELEMETRY_REGISTRATION = ` await registerTelemetryForNextjs()`
131
+ const GUARDED_TELEMETRY_REGISTRATION = ` try {
132
+ await registerTelemetryForNextjs()
133
+ } catch (err) {
134
+ const nodeProcess = process
135
+ nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)
136
+ nodeProcess.exit(1)
137
+ }`
138
+
116
139
  const DISPATCHER_SNIPPET = ` // add near the other imports:
117
140
  import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
118
141
 
@@ -331,17 +354,45 @@ function patchInstrumentation(appDir: string, options: TelemetryInitOptions): St
331
354
  }
332
355
  const content = readFileSync(path, 'utf8')
333
356
  if (content.includes('registerTelemetryForNextjs')) {
334
- if (content.includes('isTelemetryBackendEnabled')) {
357
+ const guardedBootstraps = [MULTILINE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD]
358
+ const safeBootstrap = guardedBootstraps
359
+ .map((guard) => `${guard}\n${TELEMETRY_DYNAMIC_IMPORT}\n${GUARDED_TELEMETRY_REGISTRATION}`)
360
+ .find((bootstrap) => content.includes(bootstrap))
361
+ if (safeBootstrap) {
335
362
  return { file, status: 'skipped', detail: 'telemetry already wired' }
336
363
  }
337
- const migrated =
338
- `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n` +
339
- content.replace(
340
- `if (process.env.NEXT_RUNTIME === 'nodejs') {`,
341
- `if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {`,
364
+ const guardedLegacyBootstrap = guardedBootstraps
365
+ .map((guard) => `${guard}\n${TELEMETRY_DYNAMIC_IMPORT}\n${BARE_TELEMETRY_REGISTRATION}`)
366
+ .find((bootstrap) => content.includes(bootstrap))
367
+ if (guardedLegacyBootstrap) {
368
+ const migrated = content.replace(
369
+ guardedLegacyBootstrap,
370
+ guardedLegacyBootstrap.replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION),
342
371
  )
343
- if (!options.dryRun) writeFileSync(path, migrated)
344
- return { file, status: 'patched', detail: 'added the explicit backend import guard' }
372
+ if (!options.dryRun) writeFileSync(path, migrated)
373
+ return { file, status: 'patched', detail: 'wrapped OTLP dependency failures with host termination' }
374
+ }
375
+ const nodeOnlyLegacyBootstrap = `${LEGACY_NODE_TELEMETRY_GUARD}\n${TELEMETRY_DYNAMIC_IMPORT}\n${BARE_TELEMETRY_REGISTRATION}`
376
+ if (content.includes(nodeOnlyLegacyBootstrap)) {
377
+ const guardedBootstrap = nodeOnlyLegacyBootstrap
378
+ .replace(LEGACY_NODE_TELEMETRY_GUARD, INLINE_TELEMETRY_GUARD)
379
+ .replace(BARE_TELEMETRY_REGISTRATION, GUARDED_TELEMETRY_REGISTRATION)
380
+ const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n`
381
+ const withBootstrap = content.replace(nodeOnlyLegacyBootstrap, guardedBootstrap)
382
+ const migrated = withBootstrap.includes(importLine) ? withBootstrap : importLine + withBootstrap
383
+ if (!options.dryRun) writeFileSync(path, migrated)
384
+ return {
385
+ file,
386
+ status: 'patched',
387
+ detail: 'added the explicit backend guard and wrapped OTLP dependency failures with host termination',
388
+ }
389
+ }
390
+ return {
391
+ file,
392
+ status: 'manual',
393
+ detail: 'existing telemetry bootstrap needs the canonical Node guard and fail-closed host wrapper',
394
+ manualSnippet: INSTRUMENTATION_TS,
395
+ }
345
396
  }
346
397
  const anchor = content.match(/export\s+async\s+function\s+register\s*\([^)]*\)\s*:\s*Promise<void>\s*\{/)
347
398
  if (!anchor || anchor.index === undefined) {
@@ -357,7 +408,13 @@ function patchInstrumentation(appDir: string, options: TelemetryInitOptions): St
357
408
  `\n // Initialize telemetry (no-op unless TELEMETRY_BACKEND is set); Node-only.` +
358
409
  `\n if (process.env.NEXT_RUNTIME === 'nodejs' && isTelemetryBackendEnabled()) {` +
359
410
  `\n const { registerTelemetryForNextjs } = await import('@open-mercato/telemetry/nextjs')` +
360
- `\n await registerTelemetryForNextjs()` +
411
+ `\n try {` +
412
+ `\n await registerTelemetryForNextjs()` +
413
+ `\n } catch (err) {` +
414
+ `\n const nodeProcess = process` +
415
+ `\n nodeProcess.stderr.write(\`\${err instanceof Error ? err.message : String(err)}\\n\`)` +
416
+ `\n nodeProcess.exit(1)` +
417
+ `\n }` +
361
418
  `\n }`
362
419
  const importLine = `import { isTelemetryBackendEnabled } from '@open-mercato/shared/lib/telemetry/runtime'\n`
363
420
  const withImport = content.includes('isTelemetryBackendEnabled')