@open-mercato/cli 0.6.8-develop.6930.1.1e5976efc3 → 0.6.8-develop.6940.1.177ea30c6e

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 (168) hide show
  1. package/.turbo/turbo-build.log +65 -1
  2. package/build.mjs +57 -7
  3. package/dist/agentic/guides/backend-ui.md +7 -1
  4. package/dist/agentic/guides/module-facts.json +565 -224
  5. package/dist/agentic/guides/module-facts.v2.json +122933 -0
  6. package/dist/agentic/guides/modules/ai_assistant.md +2 -2
  7. package/dist/agentic/guides/modules/api_docs.md +2 -2
  8. package/dist/agentic/guides/modules/api_keys.md +2 -2
  9. package/dist/agentic/guides/modules/attachments.md +2 -2
  10. package/dist/agentic/guides/modules/audit_logs.md +2 -2
  11. package/dist/agentic/guides/modules/auth.md +2 -2
  12. package/dist/agentic/guides/modules/business_rules.md +2 -2
  13. package/dist/agentic/guides/modules/catalog.md +11 -4
  14. package/dist/agentic/guides/modules/channel_gmail.md +2 -2
  15. package/dist/agentic/guides/modules/channel_imap.md +2 -2
  16. package/dist/agentic/guides/modules/checkout.md +8 -2
  17. package/dist/agentic/guides/modules/communication_channels.md +6 -6
  18. package/dist/agentic/guides/modules/configs.md +2 -2
  19. package/dist/agentic/guides/modules/content.md +2 -2
  20. package/dist/agentic/guides/modules/currencies.md +2 -2
  21. package/dist/agentic/guides/modules/customer_accounts.md +2 -2
  22. package/dist/agentic/guides/modules/customers.md +8 -8
  23. package/dist/agentic/guides/modules/dashboards.md +12 -12
  24. package/dist/agentic/guides/modules/data_sync.md +4 -4
  25. package/dist/agentic/guides/modules/design_system.md +2 -2
  26. package/dist/agentic/guides/modules/dictionaries.md +2 -2
  27. package/dist/agentic/guides/modules/directory.md +2 -2
  28. package/dist/agentic/guides/modules/entities.md +3 -3
  29. package/dist/agentic/guides/modules/eudr.md +3 -3
  30. package/dist/agentic/guides/modules/events.md +2 -2
  31. package/dist/agentic/guides/modules/feature_toggles.md +2 -2
  32. package/dist/agentic/guides/modules/gateway_stripe.md +2 -2
  33. package/dist/agentic/guides/modules/generators.md +2 -2
  34. package/dist/agentic/guides/modules/inbox_ops.md +2 -2
  35. package/dist/agentic/guides/modules/integrations.md +5 -2
  36. package/dist/agentic/guides/modules/messages.md +2 -2
  37. package/dist/agentic/guides/modules/notifications.md +2 -2
  38. package/dist/agentic/guides/modules/onboarding.md +2 -2
  39. package/dist/agentic/guides/modules/payment_gateways.md +7 -3
  40. package/dist/agentic/guides/modules/perspectives.md +2 -2
  41. package/dist/agentic/guides/modules/planner.md +2 -2
  42. package/dist/agentic/guides/modules/portal.md +2 -2
  43. package/dist/agentic/guides/modules/progress.md +2 -2
  44. package/dist/agentic/guides/modules/query_index.md +2 -2
  45. package/dist/agentic/guides/modules/record_locks.md +3 -3
  46. package/dist/agentic/guides/modules/resources.md +2 -2
  47. package/dist/agentic/guides/modules/sales.md +13 -4
  48. package/dist/agentic/guides/modules/scheduler.md +2 -2
  49. package/dist/agentic/guides/modules/search.md +4 -4
  50. package/dist/agentic/guides/modules/security.md +3 -3
  51. package/dist/agentic/guides/modules/shipping_carriers.md +2 -2
  52. package/dist/agentic/guides/modules/sso.md +2 -2
  53. package/dist/agentic/guides/modules/staff.md +7 -4
  54. package/dist/agentic/guides/modules/storage_s3.md +2 -2
  55. package/dist/agentic/guides/modules/sync_akeneo.md +4 -4
  56. package/dist/agentic/guides/modules/sync_excel.md +2 -2
  57. package/dist/agentic/guides/modules/system_status_overlays.md +2 -2
  58. package/dist/agentic/guides/modules/translations.md +2 -2
  59. package/dist/agentic/guides/modules/webhooks.md +2 -2
  60. package/dist/agentic/guides/modules/wms.md +31 -4
  61. package/dist/agentic/guides/modules/workflows.md +2 -2
  62. package/dist/agentic/guides/reference-module-facts.json +8335 -0
  63. package/dist/agentic/guides/reference-modules/example.md +489 -0
  64. package/dist/agentic/guides/spec-delivery.md +34 -0
  65. package/dist/agentic/guides/testing-debugging.md +2 -1
  66. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +3 -2
  67. package/dist/agentic/guides/upstream/manifest.json +2 -2
  68. package/dist/agentic/shared/AGENTS.md.template +10 -14
  69. package/dist/agentic/shared/ai/harness/README.md +4 -4
  70. package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
  71. package/dist/agentic/shared/ai/harness/cases.json +11350 -5088
  72. package/dist/agentic/shared/ai/harness/cases.schema.json +70 -7
  73. package/dist/agentic/shared/ai/harness/design-system-inventory.json +6697 -0
  74. package/dist/agentic/shared/ai/harness/fixtures/index.json +3 -1
  75. package/dist/agentic/shared/ai/harness/fixtures/seeds.json +4 -0
  76. package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
  77. package/dist/agentic/shared/ai/harness/release-matrix.json +9 -4
  78. package/dist/agentic/shared/ai/harness/result.schema.json +60 -0
  79. package/dist/agentic/shared/ai/harness/routing-response.schema.json +10 -0
  80. package/dist/agentic/shared/ai/harness/source-link-inventory.json +3231 -0
  81. package/dist/agentic/shared/ai/harness/validators.json +8 -4
  82. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +5 -2
  83. package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +3 -0
  84. package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +879 -0
  85. package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.7-to-0.7.0/SKILL.md +102 -0
  86. package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +1 -0
  87. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +15 -0
  88. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +6 -0
  89. package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +4 -0
  90. package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +1 -0
  91. package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +2 -0
  92. package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +2 -0
  93. package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +3 -1
  94. package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +1 -0
  95. package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +2 -0
  96. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +2 -0
  97. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +3 -3
  98. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
  99. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +133 -0
  100. package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +1 -0
  101. package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +11 -0
  102. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +1 -1
  103. package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +2 -2
  104. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +14 -1
  105. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +2 -0
  106. package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +19 -0
  107. package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +1 -4
  108. package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +1 -0
  109. package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +24 -0
  110. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +3 -1
  111. package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +1 -0
  112. package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +13 -0
  113. package/dist/agentic/shared/ai/skills/tiers.json +2 -1
  114. package/dist/agentic/shared/ai/specs/2026-08-06-reference-module-activation.md +248 -0
  115. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +39 -2
  116. package/dist/agentic/shared/scripts/design-source-contract.mjs +108 -0
  117. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +935 -28
  118. package/dist/agentic/shared/scripts/framework-context.mjs +4 -1
  119. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1 -1
  120. package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +1511 -0
  121. package/dist/lib/__integration__/TC-INT-008.spec.js +1 -0
  122. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  123. package/dist/lib/agentic-setup.js +9 -1
  124. package/dist/lib/agentic-setup.js.map +2 -2
  125. package/dist/lib/generators/module-extension-facts.js +93 -26
  126. package/dist/lib/generators/module-extension-facts.js.map +3 -3
  127. package/dist/lib/generators/module-facts-discovery.js +23 -0
  128. package/dist/lib/generators/module-facts-discovery.js.map +2 -2
  129. package/dist/lib/generators/module-facts.js +969 -41
  130. package/dist/lib/generators/module-facts.js.map +2 -2
  131. package/dist/lib/generators/module-override-targets.js +35 -11
  132. package/dist/lib/generators/module-override-targets.js.map +2 -2
  133. package/dist/lib/generators/module-registry.js +105 -2
  134. package/dist/lib/generators/module-registry.js.map +2 -2
  135. package/dist/lib/resolver.js +5 -0
  136. package/dist/lib/resolver.js.map +2 -2
  137. package/dist/lib/testing/integration-discovery.js +34 -0
  138. package/dist/lib/testing/integration-discovery.js.map +2 -2
  139. package/dist/lib/testing/integration.js +10 -2
  140. package/dist/lib/testing/integration.js.map +2 -2
  141. package/dist/mercato.js +4 -4
  142. package/dist/mercato.js.map +2 -2
  143. package/package.json +6 -6
  144. package/src/lib/__integration__/TC-INT-008.spec.ts +4 -2
  145. package/src/lib/__tests__/resolve-environment.test.ts +18 -0
  146. package/src/lib/agentic-setup.ts +10 -2
  147. package/src/lib/generators/__tests__/helpers/override-target-diagnostic-fixtures.ts +150 -0
  148. package/src/lib/generators/__tests__/module-extension-facts.test.ts +290 -0
  149. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +72 -7
  150. package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
  151. package/src/lib/generators/__tests__/module-facts.example-fact-coverage.test.ts +502 -0
  152. package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +414 -0
  153. package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
  154. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +42 -1
  155. package/src/lib/generators/__tests__/module-override-targets.diagnostic-wiring.test.ts +92 -0
  156. package/src/lib/generators/__tests__/module-override-targets.test.ts +24 -0
  157. package/src/lib/generators/__tests__/module-override-targets.unknown-mode.test.ts +117 -0
  158. package/src/lib/generators/__tests__/module-subset.test.ts +60 -0
  159. package/src/lib/generators/module-extension-facts.ts +181 -25
  160. package/src/lib/generators/module-facts-discovery.ts +46 -0
  161. package/src/lib/generators/module-facts.ts +1330 -86
  162. package/src/lib/generators/module-override-targets.ts +70 -26
  163. package/src/lib/generators/module-registry.ts +135 -2
  164. package/src/lib/resolver.ts +6 -1
  165. package/src/lib/testing/__tests__/integration-discovery.test.ts +35 -0
  166. package/src/lib/testing/integration-discovery.ts +50 -0
  167. package/src/lib/testing/integration.ts +10 -2
  168. package/src/mercato.ts +3 -3
@@ -9,7 +9,7 @@ import type {
9
9
  ModuleOwnedContractFact,
10
10
  } from './module-facts'
11
11
  import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
12
- import { getFrameworkOverrideModes } from './module-extension-facts'
12
+ import { getFrameworkOverrideHostOperations } from './module-extension-facts'
13
13
 
14
14
  /**
15
15
  * Exact module-specific unified override targets.
@@ -26,11 +26,26 @@ import { getFrameworkOverrideModes } from './module-extension-facts'
26
26
  * `nav.groupOrder` are never module targets.
27
27
  */
28
28
 
29
- export type ModuleOverrideMode = 'disable-replace' | 'replace' | 'additive'
29
+ /**
30
+ * Closed mode/note/diagnostic sets, published as `as const` ledgers whose types
31
+ * derive from them. The `factCoverage` ledger enumerates these at runtime, so a
32
+ * value added here without a ledger row fails the coverage contract test instead
33
+ * of silently widening the public surface.
34
+ */
35
+ export const MODULE_OVERRIDE_MODES = ['disable-replace', 'replace', 'additive'] as const
36
+
37
+ export type ModuleOverrideMode = (typeof MODULE_OVERRIDE_MODES)[number]
38
+
39
+ export const MODULE_OVERRIDE_TARGET_NOTES = [
40
+ 'safe-metadata-only',
41
+ 'page-middleware-not-mutation-guard',
42
+ ] as const
30
43
 
31
- export type ModuleOverrideTargetNote =
32
- | 'safe-metadata-only'
33
- | 'page-middleware-not-mutation-guard'
44
+ export type ModuleOverrideTargetNote = (typeof MODULE_OVERRIDE_TARGET_NOTES)[number]
45
+
46
+ function isModuleOverrideMode(value: string): value is ModuleOverrideMode {
47
+ return (MODULE_OVERRIDE_MODES as readonly string[]).includes(value)
48
+ }
34
49
 
35
50
  export type ModuleOverrideTarget = {
36
51
  id: string
@@ -43,11 +58,15 @@ export type ModuleOverrideTarget = {
43
58
  notes?: ModuleOverrideTargetNote[]
44
59
  }
45
60
 
46
- export type ModuleOverrideTargetDiagnosticCode =
47
- | 'missing-owned-fact'
48
- | 'missing-source'
49
- | 'unsupported-dynamic-key'
50
- | 'unknown-framework-domain'
61
+ export const MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES = [
62
+ 'missing-owned-fact',
63
+ 'missing-source',
64
+ 'unsupported-dynamic-key',
65
+ 'unknown-framework-domain',
66
+ 'unknown-framework-mode',
67
+ ] as const
68
+
69
+ export type ModuleOverrideTargetDiagnosticCode = (typeof MODULE_OVERRIDE_TARGET_DIAGNOSTIC_CODES)[number]
51
70
 
52
71
  export type ModuleOverrideTargetDiagnostic = {
53
72
  code: ModuleOverrideTargetDiagnosticCode
@@ -82,17 +101,35 @@ type InternalAdapter = {
82
101
  run(moduleFacts: ModuleFactsJsonEntry, resolveFactSource: FactSourceLookup): AdapterResult
83
102
  }
84
103
 
85
- const OVERRIDE_MODES = getFrameworkOverrideModes()
104
+ const OVERRIDE_HOST_OPERATIONS = getFrameworkOverrideHostOperations()
105
+
106
+ /**
107
+ * Why a dotted host produced no modes. The two failure causes are distinct and a
108
+ * downstream app needs to tell them apart: an absent host means the catalog never
109
+ * described this override surface, while an unrecognized mode means the catalog
110
+ * *does* describe it but names an operation this generator cannot map to a public
111
+ * {@link ModuleOverrideMode}. Collapsing the second into the first would report a
112
+ * catalog the generator has fallen behind as a surface that does not exist.
113
+ */
114
+ export type FrameworkOverrideModeResolution =
115
+ | { outcome: 'resolved'; modes: ModuleOverrideMode[] }
116
+ | { outcome: 'unknown-framework-domain' }
117
+ | { outcome: 'unknown-framework-mode'; operation: string }
86
118
 
87
119
  /**
88
120
  * Supported modes come from the framework catalog only. A host the catalog does not
89
- * describe is never guessed: the caller emits an `unknown-framework-domain`
90
- * diagnostic and no target, so a downstream app is not told a key is overridable in
91
- * a mode the runtime may not support.
121
+ * describe is never guessed, and neither is a host whose declared operation is not a
122
+ * known mode: both produce no target, so a downstream app is never told a key is
123
+ * overridable in a mode the runtime may not support.
92
124
  */
93
- function modesFor(dottedHost: string): ModuleOverrideMode[] | null {
94
- const mode = OVERRIDE_MODES[dottedHost]
95
- return mode ? [mode] : null
125
+ export function resolveFrameworkOverrideModes(operation: string | undefined): FrameworkOverrideModeResolution {
126
+ if (operation === undefined) return { outcome: 'unknown-framework-domain' }
127
+ if (!isModuleOverrideMode(operation)) return { outcome: 'unknown-framework-mode', operation }
128
+ return { outcome: 'resolved', modes: [operation] }
129
+ }
130
+
131
+ function modesFor(dottedHost: string): FrameworkOverrideModeResolution {
132
+ return resolveFrameworkOverrideModes(OVERRIDE_HOST_OPERATIONS[dottedHost])
96
133
  }
97
134
 
98
135
  /**
@@ -146,9 +183,13 @@ type MakeTargetInput = {
146
183
  notes?: ModuleOverrideTargetNote[]
147
184
  }
148
185
 
149
- function makeTarget(input: MakeTargetInput): ModuleOverrideTarget | null {
150
- const modes = modesFor(input.dottedHost)
151
- if (!modes) return null
186
+ /** The resolution outcomes that yield no target; each names its own diagnostic code. */
187
+ type UnresolvedOverrideMode = Exclude<FrameworkOverrideModeResolution, { outcome: 'resolved' }>
188
+
189
+ function makeTarget(input: MakeTargetInput): ModuleOverrideTarget | UnresolvedOverrideMode {
190
+ const resolution = modesFor(input.dottedHost)
191
+ if (resolution.outcome !== 'resolved') return resolution
192
+ const { modes } = resolution
152
193
  const target: ModuleOverrideTarget = {
153
194
  id: computeOverrideTargetId(input.moduleId, input.domain, input.path),
154
195
  domain: input.domain,
@@ -163,21 +204,24 @@ function makeTarget(input: MakeTargetInput): ModuleOverrideTarget | null {
163
204
  }
164
205
 
165
206
  /**
166
- * Emits the target, or an `unknown-framework-domain` diagnostic when the framework
167
- * catalog does not describe the target's dotted host.
207
+ * Emits the target, or the diagnostic matching why the framework catalog yielded no
208
+ * modes: `unknown-framework-domain` when the catalog does not describe the target's
209
+ * dotted host at all, `unknown-framework-mode` when it describes the host but names
210
+ * an operation that is not a public {@link ModuleOverrideMode}. Neither case guesses
211
+ * a target.
168
212
  */
169
213
  function pushTarget(
170
214
  targets: ModuleOverrideTarget[],
171
215
  diagnostics: ModuleOverrideTargetDiagnostic[],
172
216
  input: MakeTargetInput,
173
217
  ): void {
174
- const target = makeTarget(input)
175
- if (target) {
176
- targets.push(target)
218
+ const result = makeTarget(input)
219
+ if (!('outcome' in result)) {
220
+ targets.push(result)
177
221
  return
178
222
  }
179
223
  diagnostics.push({
180
- code: 'unknown-framework-domain',
224
+ code: result.outcome,
181
225
  moduleId: input.moduleId,
182
226
  domain: input.domain,
183
227
  candidatePath: input.path,
@@ -301,6 +301,131 @@ function buildCacheBustedSourceImportUrl(sourceFile: string): string {
301
301
  return url.href
302
302
  }
303
303
 
304
+ function assertGeneratorPluginUsesTypeOnlyImports(sourceFile: string): void {
305
+ const source = fs.readFileSync(sourceFile, 'utf8')
306
+ const scriptKind = sourceFile.endsWith('.js') || sourceFile.endsWith('.mjs') || sourceFile.endsWith('.cjs')
307
+ ? ts.ScriptKind.JS
308
+ : ts.ScriptKind.TS
309
+ const parsed = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.Latest, true, scriptKind)
310
+ const violations: string[] = []
311
+
312
+ for (const statement of parsed.statements) {
313
+ if (ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly) {
314
+ violations.push(statement.getText(parsed))
315
+ }
316
+ if (ts.isImportEqualsDeclaration(statement)) {
317
+ violations.push(statement.getText(parsed))
318
+ }
319
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && !statement.isTypeOnly) {
320
+ violations.push(statement.getText(parsed))
321
+ }
322
+ }
323
+
324
+ const visit = (node: ts.Node): void => {
325
+ if (
326
+ ts.isCallExpression(node)
327
+ && (node.expression.kind === ts.SyntaxKind.ImportKeyword
328
+ || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))
329
+ ) {
330
+ violations.push(node.getText(parsed))
331
+ }
332
+ ts.forEachChild(node, visit)
333
+ }
334
+ visit(parsed)
335
+
336
+ if (violations.length > 0) {
337
+ throw new Error(
338
+ `Generator plugin ${sourceFile} contains runtime imports. generators.ts may use only \`import type\`: ${violations.join('; ')}`,
339
+ )
340
+ }
341
+ }
342
+
343
+ const GENERATOR_PLUGIN_OUTPUT_MANIFEST = '.generator-plugin-outputs.json'
344
+
345
+ function normalizeGeneratorPluginOutputFileName(pluginId: string, outputFileName: string): string {
346
+ const normalized = outputFileName.replace(/\\/g, '/')
347
+ if (
348
+ path.isAbsolute(outputFileName)
349
+ || outputFileName.includes('\\')
350
+ || normalized.startsWith('/')
351
+ || normalized.startsWith('../')
352
+ || normalized.includes('/../')
353
+ || !normalized.endsWith('.ts')
354
+ ) {
355
+ throw new Error(
356
+ `Generator plugin '${pluginId}' has an invalid outputFileName '${outputFileName}'; expected a relative .ts path inside the generated directory.`,
357
+ )
358
+ }
359
+ return normalized
360
+ }
361
+
362
+ function resolveGeneratorPluginOutputNames(
363
+ plugins: ReadonlyMap<string, import('@open-mercato/shared/modules/generators').GeneratorPlugin>,
364
+ ): Map<string, string> {
365
+ const outputNames = new Map<string, string>()
366
+ const ownersByOutput = new Map<string, string>()
367
+ for (const [pluginId, plugin] of plugins) {
368
+ const outputName = normalizeGeneratorPluginOutputFileName(pluginId, plugin.outputFileName)
369
+ const existingOwner = ownersByOutput.get(outputName)
370
+ if (existingOwner) {
371
+ throw new Error(
372
+ `Generator plugins '${existingOwner}' and '${pluginId}' declare the same output file '${outputName}'.`,
373
+ )
374
+ }
375
+ ownersByOutput.set(outputName, pluginId)
376
+ outputNames.set(pluginId, outputName)
377
+ }
378
+ return outputNames
379
+ }
380
+
381
+ function reconcileGeneratorPluginOutputs(options: {
382
+ outputDir: string
383
+ outputNames: Iterable<string>
384
+ result: GeneratorResult
385
+ }): void {
386
+ const manifestPath = path.join(options.outputDir, GENERATOR_PLUGIN_OUTPUT_MANIFEST)
387
+ let previousOutputs: string[] = []
388
+ if (fs.existsSync(manifestPath)) {
389
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown
390
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== 'string')) {
391
+ throw new Error(`Invalid generator plugin output manifest: ${manifestPath}`)
392
+ }
393
+ previousOutputs = parsed.map((entry) => normalizeGeneratorPluginOutputFileName('manifest', entry))
394
+ }
395
+
396
+ const currentOutputs = new Set(options.outputNames)
397
+ for (const staleOutput of previousOutputs) {
398
+ if (currentOutputs.has(staleOutput)) continue
399
+ for (const stalePath of [
400
+ path.join(options.outputDir, staleOutput),
401
+ path.join(options.outputDir, staleOutput.replace(/\.ts$/, '.checksum')),
402
+ ]) {
403
+ if (!fs.existsSync(stalePath)) continue
404
+ fs.unlinkSync(stalePath)
405
+ options.result.filesWritten.push(stalePath)
406
+ }
407
+ }
408
+
409
+ if (currentOutputs.size === 0) {
410
+ if (fs.existsSync(manifestPath)) {
411
+ fs.unlinkSync(manifestPath)
412
+ options.result.filesWritten.push(manifestPath)
413
+ }
414
+ return
415
+ }
416
+
417
+ const manifestContent = `${JSON.stringify(
418
+ [...currentOutputs].sort((left, right) => left.localeCompare(right)),
419
+ null,
420
+ 2,
421
+ )}\n`
422
+ if (!fs.existsSync(manifestPath) || fs.readFileSync(manifestPath, 'utf8') !== manifestContent) {
423
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
424
+ fs.writeFileSync(manifestPath, manifestContent)
425
+ options.result.filesWritten.push(manifestPath)
426
+ }
427
+ }
428
+
304
429
  function unwrapObjectLiteralExpression(
305
430
  expression: ts.Expression | undefined,
306
431
  ): ts.ObjectLiteralExpression | undefined {
@@ -3143,6 +3268,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3143
3268
  for (const discovered of discovery.modules) {
3144
3269
  const resolved = discovered.resolve('generators.ts')
3145
3270
  if (!resolved) continue
3271
+ assertGeneratorPluginUsesTypeOnlyImports(resolved.absolutePath)
3146
3272
  try {
3147
3273
  const pluginMod = await import(buildCacheBustedSourceImportUrl(resolved.absolutePath))
3148
3274
  const plugins: import('@open-mercato/shared/modules/generators').GeneratorPlugin[] =
@@ -3156,6 +3282,8 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3156
3282
  } catch {}
3157
3283
  }
3158
3284
 
3285
+ const pluginOutputNames = resolveGeneratorPluginOutputNames(pluginRegistry)
3286
+
3159
3287
  const imports: string[] = []
3160
3288
  const runtimeImports: string[] = []
3161
3289
  const frontendRouteManifestImports: string[] = []
@@ -3716,10 +3844,15 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3716
3844
  const importSection = state.imports.join('\n')
3717
3845
  const entriesLiteral = state.configs.join(',\n ')
3718
3846
  const content = plugin.buildOutput({ importSection, entriesLiteral })
3719
- const outFile = path.join(outputDir, plugin.outputFileName)
3720
- const checksumFile = outFile.replace('.ts', '.checksum')
3847
+ const outFile = path.join(outputDir, pluginOutputNames.get(pluginId)!)
3848
+ const checksumFile = outFile.replace(/\.ts$/, '.checksum')
3721
3849
  writeGeneratedFile({ outFile, checksumFile, content, structureChecksum, result, quiet })
3722
3850
  }
3851
+ reconcileGeneratorPluginOutputs({
3852
+ outputDir,
3853
+ outputNames: pluginOutputNames.values(),
3854
+ result,
3855
+ })
3723
3856
 
3724
3857
  // Bootstrap registrations: aggregate core registrations + plugin bootstrap-registration hooks
3725
3858
  // into one file. Always written (with at least the core backend route registration) so bootstrap.ts
@@ -334,7 +334,7 @@ function parseModulesFromSource(source: string, env: NodeJS.ProcessEnv = process
334
334
  return modules
335
335
  }
336
336
 
337
- function readEnabledModulesFromConfig(cfgPath: string): ModuleEntry[] {
337
+ export function readEnabledModulesFromConfig(cfgPath: string): ModuleEntry[] {
338
338
  const source = fs.readFileSync(cfgPath, 'utf8')
339
339
  return parseModulesFromSource(source)
340
340
  }
@@ -489,6 +489,11 @@ function detectMonorepoFromNodeModules(appDir: string): { isMonorepo: boolean; m
489
489
 
490
490
  const corePkgPath = path.join(nodeModulesRoot, 'node_modules', '@open-mercato', 'core')
491
491
 
492
+ const workspaceCoreModules = path.join(nodeModulesRoot, 'packages', 'core', 'src', 'modules')
493
+ if (fs.existsSync(workspaceCoreModules)) {
494
+ return { isMonorepo: true, monorepoRoot: nodeModulesRoot, nodeModulesRoot }
495
+ }
496
+
492
497
  try {
493
498
  const stat = fs.lstatSync(corePkgPath)
494
499
  if (stat.isSymbolicLink()) {
@@ -16,6 +16,7 @@ describe('integration discovery', () => {
16
16
  'OM_TEST_DISCOVERY_AI_KEY',
17
17
  'OM_TEST_DISCOVERY_FALLBACK_AI_KEY',
18
18
  'OM_TEST_DISCOVERY_REQUIRED_KEY',
19
+ 'OM_TEST_APP_ROOT',
19
20
  ] as const
20
21
  const previousTestEnvValues = new Map<string, string | undefined>()
21
22
 
@@ -159,6 +160,40 @@ describe('integration discovery', () => {
159
160
  ])
160
161
  })
161
162
 
163
+ it('uses OM_TEST_APP_ROOT and its actual modules.ts for dependency filtering', async () => {
164
+ const standaloneRoot = path.join(tempRoot, 'standalone-app')
165
+ await writeTestFile(
166
+ tempRoot,
167
+ 'apps/mercato/src/modules/example/__integration__/meta.ts',
168
+ "export const integrationMeta = { requiredModules: ['example'] }\n",
169
+ )
170
+ await writeTestFile(
171
+ tempRoot,
172
+ 'apps/mercato/src/modules/example/__integration__/TC-EXAMPLE-016.spec.ts',
173
+ )
174
+ await writeTestFile(
175
+ tempRoot,
176
+ 'apps/mercato/src/modules/example/__integration__/TC-EXAMPLE-018.meta.ts',
177
+ "export const integrationMeta = { requiredModules: ['design_system'] }\n",
178
+ )
179
+ await writeTestFile(
180
+ tempRoot,
181
+ 'apps/mercato/src/modules/example/__integration__/TC-EXAMPLE-018.spec.ts',
182
+ )
183
+ await writeTestFile(
184
+ tempRoot,
185
+ 'standalone-app/src/modules.ts',
186
+ `const moduleOverrideExamples = [{ id: 'design_system', from: '@open-mercato/core' }]\n\nexport const enabledModules = [{\n id: 'example',\n from: '@app',\n overrides: { routes: { api: { 'GET /api/example/probe': null } } },\n}]\n`,
187
+ )
188
+ await writeTestFile(tempRoot, 'standalone-app/src/modules/design_system/.gitkeep')
189
+ process.env.OM_TEST_APP_ROOT = standaloneRoot
190
+
191
+ const discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
192
+ expect(discovered.map((entry) => entry.path)).toEqual([
193
+ 'apps/mercato/src/modules/example/__integration__/TC-EXAMPLE-016.spec.ts',
194
+ ])
195
+ })
196
+
162
197
  it('applies folder and per-test metadata environment requirements', async () => {
163
198
  await writeTestFile(tempRoot, 'packages/ai-assistant/src/modules/ai_assistant/.gitkeep')
164
199
  await writeTestFile(
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, readdirSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import ts from 'typescript-js'
3
4
 
4
5
  export type IntegrationSpecDiscoveryItem = {
5
6
  path: string
@@ -106,7 +107,56 @@ function collectModuleIdsFromModulesRoot(modulesRoot: string, enabledModules: Se
106
107
  }
107
108
  }
108
109
 
110
+ function readDeclaredModuleIdsFromConfig(registryPath: string): string[] {
111
+ const source = readFileSync(registryPath, 'utf8')
112
+ const sourceFile = ts.createSourceFile(
113
+ registryPath,
114
+ source,
115
+ ts.ScriptTarget.Latest,
116
+ true,
117
+ ts.ScriptKind.TS,
118
+ )
119
+
120
+ for (const statement of sourceFile.statements) {
121
+ if (!ts.isVariableStatement(statement)) continue
122
+ for (const declaration of statement.declarationList.declarations) {
123
+ if (
124
+ !ts.isIdentifier(declaration.name)
125
+ || declaration.name.text !== 'enabledModules'
126
+ || !declaration.initializer
127
+ || !ts.isArrayLiteralExpression(declaration.initializer)
128
+ ) {
129
+ continue
130
+ }
131
+
132
+ return declaration.initializer.elements.flatMap((element) => {
133
+ if (!ts.isObjectLiteralExpression(element)) return []
134
+ const idProperty = element.properties.find((property): property is ts.PropertyAssignment =>
135
+ ts.isPropertyAssignment(property)
136
+ && (
137
+ (ts.isIdentifier(property.name) && property.name.text === 'id')
138
+ || (ts.isStringLiteralLike(property.name) && property.name.text === 'id')
139
+ ),
140
+ )
141
+ return idProperty && ts.isStringLiteralLike(idProperty.initializer)
142
+ ? [idProperty.initializer.text]
143
+ : []
144
+ })
145
+ }
146
+ }
147
+
148
+ return []
149
+ }
150
+
109
151
  function resolveEnabledModuleIds(projectRoot: string): Set<string> {
152
+ const configuredTestAppRoot = process.env.OM_TEST_APP_ROOT?.trim()
153
+ if (configuredTestAppRoot) {
154
+ const registryPath = path.join(path.resolve(configuredTestAppRoot), 'src', 'modules.ts')
155
+ return new Set(
156
+ readDeclaredModuleIdsFromConfig(registryPath).map(normalizeModuleId),
157
+ )
158
+ }
159
+
110
160
  const enabledModules = new Set<string>()
111
161
  const appModulesRoot = path.join(projectRoot, 'src', 'modules')
112
162
  const appsRoot = path.join(projectRoot, 'apps')
@@ -1977,6 +1977,12 @@ function buildReusableEnvironment(
1977
1977
  // not have to call flushPendingCrudAccessLogs() explicitly.
1978
1978
  OM_CRUD_ACCESS_LOG_BLOCKING: process.env.OM_CRUD_ACCESS_LOG_BLOCKING ?? '1',
1979
1979
  OM_WEBHOOKS_ALLOW_PRIVATE_URLS: process.env.OM_WEBHOOKS_ALLOW_PRIVATE_URLS ?? '1',
1980
+ // TC-ONB-001/002 drive the self-service signup flow, whose routes are not
1981
+ // mounted while the feature is off. `apps/mercato/.env` ships it disabled,
1982
+ // so both specs 404'd on every local run while CI stayed green — it exports
1983
+ // the var at the workflow level, the same gap the MOCK_INBOUND_WEBHOOK_SECRET
1984
+ // note below describes. Keep in sync with the app-server env block.
1985
+ SELF_SERVICE_ONBOARDING_ENABLED: process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? 'true',
1980
1986
  // Keep the bus in the Playwright process (used by in-test queue-drain helpers)
1981
1987
  // on the same delivery mode as the app server it drives: inline persistent
1982
1988
  // delivery so event side effects are deterministic for assertions. See the
@@ -1986,7 +1992,6 @@ function buildReusableEnvironment(
1986
1992
  MOCK_GATEWAY_WEBHOOK_SECRET: 'open-mercato-mock-dev-webhook-secret',
1987
1993
  MOCK_CARRIER_WEBHOOK_SECRET: 'open-mercato-mock-dev-carrier-webhook-secret',
1988
1994
  MOCK_INBOUND_WEBHOOK_SECRET: 'open-mercato-mock-dev-inbound-webhook-secret',
1989
- NEXT_PUBLIC_OM_EXAMPLE_INJECTION_WIDGETS_ENABLED: 'true',
1990
1995
  NEXT_PUBLIC_UMES_DEVTOOLS: 'true',
1991
1996
  CI: 'true',
1992
1997
  TENANT_DATA_ENCRYPTION_FALLBACK_KEY: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? 'om-ephemeral-integration-fallback-key',
@@ -3329,6 +3334,10 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
3329
3334
  OM_TEST_AUTH_RATE_LIMIT_MODE: 'opt-in',
3330
3335
  OM_DISABLE_EMAIL_DELIVERY: '1',
3331
3336
  OM_WEBHOOKS_ALLOW_PRIVATE_URLS: process.env.OM_WEBHOOKS_ALLOW_PRIVATE_URLS ?? '1',
3337
+ // Read at build time as well as at runtime, so this block has to carry it:
3338
+ // the app build and `yarn start` both run with this environment. See the
3339
+ // matching note on the Playwright-process env block above.
3340
+ SELF_SERVICE_ONBOARDING_ENABLED: process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? 'true',
3332
3341
  ENABLE_CRUD_API_CACHE: 'true',
3333
3342
  MOCK_GATEWAY_WEBHOOK_SECRET: 'open-mercato-mock-dev-webhook-secret',
3334
3343
  MOCK_CARRIER_WEBHOOK_SECRET: 'open-mercato-mock-dev-carrier-webhook-secret',
@@ -3338,7 +3347,6 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
3338
3347
  // var at the workflow level, masking the gap). Keep in sync with the
3339
3348
  // Playwright-process env block above.
3340
3349
  MOCK_INBOUND_WEBHOOK_SECRET: 'open-mercato-mock-dev-inbound-webhook-secret',
3341
- NEXT_PUBLIC_OM_EXAMPLE_INJECTION_WIDGETS_ENABLED: 'true',
3342
3350
  NEXT_PUBLIC_UMES_DEVTOOLS: 'true',
3343
3351
  CI: 'true',
3344
3352
  TENANT_DATA_ENCRYPTION_FALLBACK_KEY: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? 'om-ephemeral-integration-fallback-key',
package/src/mercato.ts CHANGED
@@ -7,7 +7,7 @@ import { getCliModules, hasCliModules, registerCliModules } from './registry'
7
7
  export { getCliModules, hasCliModules, registerCliModules }
8
8
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
9
9
  import { getSslConfig } from '@open-mercato/shared/lib/db/ssl'
10
- import { getRedisUrl, getRedisUrlOrThrow, REDIS_WIRE_PROTOCOL } from '@open-mercato/shared/lib/redis/connection'
10
+ import { getRedisUrl, getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'
11
11
  import { resolveInitDerivedSecrets } from './lib/init-secrets'
12
12
  import {
13
13
  resolveAutoSpawnWorkersMode,
@@ -994,13 +994,13 @@ export async function run(argv = process.argv) {
994
994
  const redisUrl = getRedisUrl()
995
995
  if (redisUrl) {
996
996
  const Redis = (await import('ioredis')).default
997
- const redis = new Redis(redisUrl, {
997
+ const redis = new Redis({
998
+ ...parseRedisUrl(redisUrl),
998
999
  lazyConnect: true,
999
1000
  connectTimeout: 3000,
1000
1001
  maxRetriesPerRequest: 1,
1001
1002
  retryStrategy: () => null,
1002
1003
  enableOfflineQueue: false,
1003
- protocol: REDIS_WIRE_PROTOCOL,
1004
1004
  })
1005
1005
  redis.on('error', () => {})
1006
1006
  try {