@open-mercato/cli 0.6.8-develop.6931.1.4a1e1c786b → 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 +0 -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 +0 -2
  168. package/src/mercato.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.8-develop.6931.1.4a1e1c786b",
3
+ "version": "0.6.8-develop.6940.1.177ea30c6e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,9 +60,9 @@
60
60
  "@mikro-orm/decorators": "^7.1.8",
61
61
  "@mikro-orm/migrations": "^7.1.8",
62
62
  "@mikro-orm/postgresql": "^7.1.8",
63
- "@open-mercato/queue": "0.6.8-develop.6931.1.4a1e1c786b",
64
- "@open-mercato/shared": "0.6.8-develop.6931.1.4a1e1c786b",
65
- "@open-mercato/telemetry": "0.6.8-develop.6931.1.4a1e1c786b",
63
+ "@open-mercato/queue": "0.6.8-develop.6940.1.177ea30c6e",
64
+ "@open-mercato/shared": "0.6.8-develop.6940.1.177ea30c6e",
65
+ "@open-mercato/telemetry": "0.6.8-develop.6940.1.177ea30c6e",
66
66
  "cross-spawn": "^7.0.6",
67
67
  "pg": "8.22.0",
68
68
  "semver": "^7.8.5",
@@ -73,10 +73,10 @@
73
73
  "typescript-js": "npm:typescript@6.0.3"
74
74
  },
75
75
  "peerDependencies": {
76
- "@open-mercato/shared": "0.6.8-develop.6931.1.4a1e1c786b"
76
+ "@open-mercato/shared": "0.6.8-develop.6940.1.177ea30c6e"
77
77
  },
78
78
  "devDependencies": {
79
- "@open-mercato/shared": "0.6.8-develop.6931.1.4a1e1c786b",
79
+ "@open-mercato/shared": "0.6.8-develop.6940.1.177ea30c6e",
80
80
  "@types/jest": "^30.0.0",
81
81
  "jest": "^30.4.2",
82
82
  "ts-jest": "^29.4.12"
@@ -247,12 +247,14 @@ function expectedGuideOutputNames(): string[] {
247
247
  }
248
248
 
249
249
  // Generated fact-sheet artifacts (spec 2026-06-27-ts-morph-module-fact-sheets):
250
- // the module-facts.json sidecar is copied as-is and fact-sheets are filtered to the
251
- // fixture's enabled modules. The legacy core.<module>.md redirect stubs are no longer
250
+ // the v1 module-facts.json and corrected module-facts.v2.json sidecars are copied
251
+ // as-is and fact-sheets are filtered to the fixture's enabled modules. The legacy
252
+ // core.<module>.md redirect stubs are no longer
252
253
  // emitted (#3754). framework-extension-points.md is likewise generated into
253
254
  // dist/agentic/guides by both build.mjs pipelines (#4810) rather than checked in
254
255
  // alongside the static conceptual guides, so it needs an explicit entry here.
255
256
  collected.add('module-facts.json')
257
+ collected.add('module-facts.v2.json')
256
258
  collected.add('framework-extension-points.md')
257
259
  collected.add('upstream/AGENTS.md')
258
260
  collected.add('upstream/BACKWARD_COMPATIBILITY.md')
@@ -208,6 +208,24 @@ describe('resolveEnvironment', () => {
208
208
  })
209
209
  })
210
210
 
211
+ describe('monorepo — materialized workspace node_modules', () => {
212
+ it('prefers checked-out package sources when the workspace package is a real directory', async () => {
213
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
214
+ await makeDir(tempRoot, 'packages', 'core', 'src', 'modules')
215
+ await makeDir(tempRoot, 'apps', 'mercato')
216
+ const packageDir = await makeDir(tempRoot, 'packages', 'create-app')
217
+
218
+ const env = resolveEnvironment(packageDir)
219
+
220
+ expect(env.mode).toBe('monorepo')
221
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
222
+ expect(normalizePath(env.appDir)).toBe(normalizePath(path.join(tempRoot, 'apps', 'mercato')))
223
+ expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
224
+ normalizePath(path.join(tempRoot, 'packages', 'core')),
225
+ )
226
+ })
227
+ })
228
+
211
229
  describe('no @open-mercato packages present', () => {
212
230
  it('falls back to standalone mode with rootDir = cwd', async () => {
213
231
  const env = resolveEnvironment(tempRoot)
@@ -439,7 +439,9 @@ function finalizeHarnessManifest(config: AgenticConfig, selectedTools: string[])
439
439
  ...targetPathsForTree(join(srcDir, 'scripts'), join(targetDir, 'scripts')),
440
440
  ])
441
441
  for (const file of readdirSync(GUIDES_DIR)) {
442
- if (file.endsWith('.md') || file === 'module-facts.json') paths.add(join(targetDir, '.ai', 'guides', file))
442
+ if (file.endsWith('.md') || file === 'module-facts.json' || file === 'module-facts.v2.json') {
443
+ paths.add(join(targetDir, '.ai', 'guides', file))
444
+ }
443
445
  }
444
446
  for (const file of listFiles(join(GUIDES_DIR, 'upstream'))) {
445
447
  paths.add(join(targetDir, '.ai', 'guides', 'upstream', relative(join(GUIDES_DIR, 'upstream'), file)))
@@ -619,7 +621,7 @@ function generateShared(config: AgenticConfig): void {
619
621
 
620
622
  // Routed conceptual guides are copied wholesale (framework-wide). Per-module
621
623
  // fact-sheets (.ai/guides/modules/<module>.md) are filtered to the app's enabled
622
- // module set; the combined module-facts.json sidecar is copied as-is.
624
+ // module set; the combined v1 and corrected v2 module-facts sidecars are copied as-is.
623
625
  if (existsSync(GUIDES_DIR)) {
624
626
  const guidesDestDir = join(targetDir, '.ai', 'guides')
625
627
  for (const file of readdirSync(GUIDES_DIR)) {
@@ -637,6 +639,12 @@ function generateShared(config: AgenticConfig): void {
637
639
  copyFileSync(moduleFactsPath, destPath)
638
640
  }
639
641
 
642
+ const moduleFactsV2Path = join(GUIDES_DIR, 'module-facts.v2.json')
643
+ if (existsSync(moduleFactsV2Path)) {
644
+ const destPath = join(guidesDestDir, 'module-facts.v2.json')
645
+ ensureDir(destPath)
646
+ copyFileSync(moduleFactsV2Path, destPath)
647
+ }
640
648
 
641
649
  copyTree(join(GUIDES_DIR, 'upstream'), join(guidesDestDir, 'upstream'), config)
642
650
 
@@ -0,0 +1,150 @@
1
+ import type { ModuleFactsJsonEntry } from '../../module-facts'
2
+ import { getFrameworkOverrideHostOperations } from '../../module-extension-facts'
3
+ import {
4
+ collectModuleOverrideTargets,
5
+ type ModuleOverrideTargetDiagnostic,
6
+ type ModuleOverrideTargetDiagnosticCode,
7
+ type ModuleOverrideTargetsResult,
8
+ } from '../../module-override-targets'
9
+
10
+ /**
11
+ * Real producers for every override-target diagnostic the projector can emit.
12
+ *
13
+ * The `factCoverage` ledger classifies four diagnostic codes as `negative-fixture`,
14
+ * which is a claim that a fixture really drives that code out of
15
+ * {@link collectModuleOverrideTargets}. These fixtures ARE that claim's evidence, and
16
+ * the coverage test asserts the produced set matches the ledger rows exactly — so a
17
+ * row can never name a fixture that does not exist, and a code no fixture reaches
18
+ * cannot be published as `negative-fixture`.
19
+ */
20
+
21
+ export const FIXTURE_MODULE_ID = 'fixture'
22
+ export const FIXTURE_SOURCE_ROOT = `node_modules/@open-mercato/core/src/modules/${FIXTURE_MODULE_ID}`
23
+
24
+ export function fixtureFacts(overrides: Partial<ModuleFactsJsonEntry> = {}): ModuleFactsJsonEntry {
25
+ return {
26
+ title: 'Fixture',
27
+ description: null,
28
+ coreVersion: null,
29
+ sourcePackage: '@open-mercato/core',
30
+ sourceVersion: null,
31
+ sourceRoot: FIXTURE_SOURCE_ROOT,
32
+ entities: [],
33
+ events: [],
34
+ aclFeatures: [],
35
+ apiRoutes: [],
36
+ diTokens: [],
37
+ searchEntities: [],
38
+ hostTokens: { entityIds: [], tableIds: [] },
39
+ notifications: [],
40
+ cli: ['fixture:seed'],
41
+ backendPages: [],
42
+ frontendPages: [],
43
+ cliCommands: [{ command: 'fixture:seed', sourcePath: `${FIXTURE_SOURCE_ROOT}/cli.ts` }],
44
+ aiTools: [],
45
+ aiAgents: [],
46
+ ...overrides,
47
+ }
48
+ }
49
+
50
+ function fixtureWorkerFacts(): ModuleFactsJsonEntry {
51
+ return fixtureFacts({
52
+ ownedContracts: {
53
+ worker: [
54
+ {
55
+ kind: 'worker',
56
+ id: 'fixture.worker',
57
+ source: { sourcePath: `${FIXTURE_SOURCE_ROOT}/workers/fixture.ts`, line: 1 },
58
+ },
59
+ ],
60
+ },
61
+ })
62
+ }
63
+
64
+ /**
65
+ * The framework catalog's real declared operations, which the projector reads at
66
+ * import time. Tests derive their stub catalogs from this so a stub differs from
67
+ * production in exactly the one host under test.
68
+ */
69
+ export function realCatalogOperations(): Record<string, string> {
70
+ return getFrameworkOverrideHostOperations()
71
+ }
72
+
73
+ export function catalogWithout(dottedHost: string): Record<string, string> {
74
+ const operations = realCatalogOperations()
75
+ delete operations[dottedHost]
76
+ return operations
77
+ }
78
+
79
+ export function catalogWith(dottedHost: string, operation: string): Record<string, string> {
80
+ return { ...realCatalogOperations(), [dottedHost]: operation }
81
+ }
82
+
83
+ /**
84
+ * Re-imports the projector against a stubbed framework catalog, so the two "no
85
+ * modes" causes reach the real `pushTarget` wiring instead of only the pure
86
+ * resolver. Every catalog host ships a valid operation today, so a stub is the only
87
+ * way to drive the unknown-domain/unknown-mode branches through production code.
88
+ */
89
+ export function collectWithCatalogOperations(
90
+ hostOperations: Record<string, string>,
91
+ facts: ModuleFactsJsonEntry,
92
+ ): ModuleOverrideTargetsResult {
93
+ let result: ModuleOverrideTargetsResult | undefined
94
+ jest.isolateModules(() => {
95
+ jest.doMock('../../module-extension-facts', () => ({
96
+ ...jest.requireActual('../../module-extension-facts'),
97
+ getFrameworkOverrideHostOperations: () => hostOperations,
98
+ }))
99
+ const projector = require('../../module-override-targets') as typeof import('../../module-override-targets')
100
+ result = projector.collectModuleOverrideTargets(facts)
101
+ })
102
+ jest.dontMock('../../module-extension-facts')
103
+ if (!result) throw new Error('[internal] isolated override-target collection produced no result')
104
+ return result
105
+ }
106
+
107
+ export type OverrideTargetDiagnosticFixture = {
108
+ code: ModuleOverrideTargetDiagnosticCode
109
+ description: string
110
+ run: () => ModuleOverrideTargetDiagnostic[]
111
+ }
112
+
113
+ export const OVERRIDE_TARGET_DIAGNOSTIC_FIXTURES: OverrideTargetDiagnosticFixture[] = [
114
+ {
115
+ code: 'missing-source',
116
+ description: 'a cli command whose provenance cannot be proven',
117
+ run: () =>
118
+ collectModuleOverrideTargets(fixtureFacts({ cliCommands: [{ command: 'fixture:seed', sourcePath: null }] }))
119
+ .overrideTargetDiagnostics,
120
+ },
121
+ {
122
+ code: 'unsupported-dynamic-key',
123
+ description: 'an api route whose method the runtime cannot address',
124
+ run: () =>
125
+ collectModuleOverrideTargets(
126
+ fixtureFacts({
127
+ apiRoutes: [
128
+ {
129
+ path: '/fixture/items',
130
+ methods: ['FETCH'],
131
+ auth: {},
132
+ sourcePath: `${FIXTURE_SOURCE_ROOT}/api/items/route.ts`,
133
+ },
134
+ ],
135
+ }),
136
+ ).overrideTargetDiagnostics,
137
+ },
138
+ {
139
+ code: 'unknown-framework-domain',
140
+ description: 'the framework catalog does not describe the cli host at all',
141
+ run: () => collectWithCatalogOperations(catalogWithout('cli'), fixtureFacts()).overrideTargetDiagnostics,
142
+ },
143
+ {
144
+ code: 'unknown-framework-mode',
145
+ description: 'the framework catalog describes the workers host with an operation that is not a public mode',
146
+ run: () =>
147
+ collectWithCatalogOperations(catalogWith('workers', 'merge-deep'), fixtureWorkerFacts())
148
+ .overrideTargetDiagnostics,
149
+ },
150
+ ]
@@ -312,6 +312,296 @@ describe('module extension facts', () => {
312
312
  ]))
313
313
  })
314
314
 
315
+ it('fails closed on an unknown member call instead of publishing its first argument', () => {
316
+ // The generic call fallback used to forward the FIRST ARGUMENT of any call expression.
317
+ // For a member call that COMPUTES its value from the arguments — a handle builder, an
318
+ // enum lookup, anything namespaced — that publishes an id the argument merely contributed
319
+ // to, which resolves to nothing. `ComponentReplacementHandles.*` is intercepted by an
320
+ // explicit formula above, so only a builder the reader does NOT know exercises this path.
321
+ // The failure is silent (the fact vanishes rather than erroring), so it needs its own test.
322
+ write(moduleRoot, 'widgets/components.ts', `
323
+ import { SomeOtherRegistry } from '@open-mercato/shared/modules/widgets/component-registry'
324
+ export const componentOverrides = [
325
+ {
326
+ target: { componentId: SomeOtherRegistry.derive('alpha.records.list') },
327
+ priority: 50,
328
+ wrapper: (Original) => Original,
329
+ },
330
+ ]
331
+ `)
332
+
333
+ const facts = extractModuleExtensionFacts({
334
+ moduleId: 'alpha',
335
+ moduleRoot,
336
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
337
+ entities: [],
338
+ events: [],
339
+ apiRoutes: [],
340
+ searchEntities: [],
341
+ })
342
+
343
+ const overrides = facts.contributions.filter((contribution) => contribution.kind === 'component-override')
344
+ expect(overrides).toEqual([])
345
+ expect(JSON.stringify(facts)).not.toContain('alpha.records.list')
346
+
347
+ const legacyFacts = extractModuleExtensionFacts({
348
+ moduleId: 'alpha',
349
+ moduleRoot,
350
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
351
+ entities: [],
352
+ events: [],
353
+ apiRoutes: [],
354
+ searchEntities: [],
355
+ factsContractVersion: 1,
356
+ })
357
+ expect(legacyFacts.contributions).toEqual(expect.arrayContaining([
358
+ expect.objectContaining({
359
+ id: 'alpha.component-override.0:alpha.records.list',
360
+ details: expect.objectContaining({ handle: 'alpha.records.list', mode: 'replace' }),
361
+ }),
362
+ ]))
363
+ })
364
+
365
+ it('classifies component overrides by the ComponentOverride discriminant and resolves handle builders', () => {
366
+ write(moduleRoot, 'widgets/components.ts', `
367
+ import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
368
+ function wrapRecords(Original) { return Original }
369
+ export const componentOverrides = [
370
+ {
371
+ target: { componentId: ComponentReplacementHandles.section('ui.detail', 'NotesSection') },
372
+ priority: 50,
373
+ wrapper: (Original) => Original,
374
+ },
375
+ {
376
+ target: { componentId: ComponentReplacementHandles.dataTable('alpha.records.list') },
377
+ priority: 50,
378
+ wrapper: wrapRecords,
379
+ },
380
+ {
381
+ target: { componentId: ComponentReplacementHandles.crudForm('alpha.record') },
382
+ priority: 50,
383
+ propsTransform: (props) => props,
384
+ },
385
+ {
386
+ target: { componentId: ComponentReplacementHandles.page('/backend/alpha') },
387
+ priority: 50,
388
+ replacement: AlphaPage,
389
+ propsSchema: alphaPropsSchema,
390
+ },
391
+ {
392
+ target: { componentId: ComponentReplacementHandles.mystery('alpha') },
393
+ priority: 50,
394
+ wrapper: (Original) => Original,
395
+ },
396
+ ]
397
+ `)
398
+
399
+ const facts = extractModuleExtensionFacts({
400
+ moduleId: 'alpha',
401
+ moduleRoot,
402
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
403
+ entities: [],
404
+ events: [],
405
+ apiRoutes: [],
406
+ searchEntities: [],
407
+ })
408
+
409
+ const overrides = facts.contributions
410
+ .filter((contribution) => contribution.kind === 'component-override')
411
+ .map((contribution) => contribution.kind === 'component-override'
412
+ ? { id: contribution.id, ...contribution.details }
413
+ : null)
414
+
415
+ expect(overrides).toEqual([
416
+ {
417
+ id: 'alpha.component-override.0:section:ui.detail.NotesSection',
418
+ handle: 'section:ui.detail.NotesSection',
419
+ mode: 'wrapper',
420
+ propsContract: 'component-props-schema',
421
+ },
422
+ {
423
+ id: 'alpha.component-override.1:data-table:alpha.records.list',
424
+ handle: 'data-table:alpha.records.list',
425
+ mode: 'wrapper',
426
+ propsContract: 'component-props-schema',
427
+ },
428
+ {
429
+ id: 'alpha.component-override.2:crud-form:alpha.record',
430
+ handle: 'crud-form:alpha.record',
431
+ mode: 'props',
432
+ propsContract: 'component-props-schema',
433
+ },
434
+ {
435
+ id: 'alpha.component-override.3:page:/backend/alpha',
436
+ handle: 'page:/backend/alpha',
437
+ mode: 'replace',
438
+ propsContract: 'component-props-schema',
439
+ },
440
+ ])
441
+ // A handle the framework builder catalog does not describe never becomes a
442
+ // guessed id built from the call's first argument.
443
+ expect(JSON.stringify(facts.contributions)).not.toContain('ui.detail"')
444
+ expect(JSON.stringify(facts.contributions)).not.toContain('"alpha"')
445
+
446
+ const legacyFacts = extractModuleExtensionFacts({
447
+ moduleId: 'alpha',
448
+ moduleRoot,
449
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
450
+ entities: [],
451
+ events: [],
452
+ apiRoutes: [],
453
+ searchEntities: [],
454
+ factsContractVersion: 1,
455
+ })
456
+ expect(legacyFacts.contributions
457
+ .filter((contribution) => contribution.kind === 'component-override')
458
+ .map((contribution) => contribution.kind === 'component-override'
459
+ ? { id: contribution.id, ...contribution.details }
460
+ : null)).toEqual([
461
+ {
462
+ id: 'alpha.component-override.0:ui.detail',
463
+ handle: 'ui.detail',
464
+ mode: 'replace',
465
+ propsContract: 'component-props-schema',
466
+ },
467
+ {
468
+ id: 'alpha.component-override.1:alpha.records.list',
469
+ handle: 'alpha.records.list',
470
+ mode: 'replace',
471
+ propsContract: 'component-props-schema',
472
+ },
473
+ {
474
+ id: 'alpha.component-override.2:alpha.record',
475
+ handle: 'alpha.record',
476
+ mode: 'replace',
477
+ propsContract: 'component-props-schema',
478
+ },
479
+ {
480
+ id: 'alpha.component-override.3:/backend/alpha',
481
+ handle: '/backend/alpha',
482
+ mode: 'replace',
483
+ propsContract: 'component-props-schema',
484
+ },
485
+ {
486
+ id: 'alpha.component-override.4:alpha',
487
+ handle: 'alpha',
488
+ mode: 'replace',
489
+ propsContract: 'component-props-schema',
490
+ },
491
+ ])
492
+ })
493
+
494
+ it('reads arrow-function convention hooks as declared surfaces', () => {
495
+ write(moduleRoot, 'data/enrichers.ts', `
496
+ const enrichList = async (records) => records
497
+ export const enrichers = [{
498
+ id: 'alpha.arrow', targetEntity: 'alpha:record',
499
+ enrichMany: enrichList,
500
+ enrichOne: async (record) => record,
501
+ }]
502
+ `)
503
+
504
+ const facts = extractModuleExtensionFacts({
505
+ moduleId: 'alpha',
506
+ moduleRoot,
507
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
508
+ entities: [{ id: 'alpha:record' }],
509
+ events: [],
510
+ apiRoutes: [],
511
+ searchEntities: [],
512
+ })
513
+
514
+ expect(facts.contributions).toEqual(expect.arrayContaining([
515
+ expect.objectContaining({
516
+ id: 'alpha.arrow',
517
+ details: expect.objectContaining({ surfaces: ['list', 'detail'] }),
518
+ }),
519
+ ]))
520
+
521
+ const legacyFacts = extractModuleExtensionFacts({
522
+ moduleId: 'alpha',
523
+ moduleRoot,
524
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
525
+ entities: [{ id: 'alpha:record' }],
526
+ events: [],
527
+ apiRoutes: [],
528
+ searchEntities: [],
529
+ factsContractVersion: 1,
530
+ })
531
+ expect(legacyFacts.contributions).toEqual(expect.arrayContaining([
532
+ expect.objectContaining({ id: 'alpha.arrow', details: expect.objectContaining({ surfaces: [] }) }),
533
+ ]))
534
+ })
535
+
536
+ it('reads every injection-table slot form the runtime loader accepts', () => {
537
+ write(moduleRoot, 'extension-points.ts', `
538
+ export const extensionPoints = defineModuleExtensionPoints({
539
+ moduleId: 'alpha',
540
+ hosts: {
541
+ records: dataTableExtensionHost({ tableId: 'alpha.records', source: 'Records.tsx' }),
542
+ editor: crudFormExtensionHost({ entityId: 'alpha.record', source: 'Edit.tsx' }),
543
+ },
544
+ })
545
+ `)
546
+ write(moduleRoot, 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
547
+ write(moduleRoot, 'Edit.tsx', 'export const entityId = extensionPoints.hosts.editor.entityId')
548
+ write(moduleRoot, 'widgets/injection-table.ts', `
549
+ export const injectionTable = {
550
+ 'crud-form:alpha.record': 'alpha.bare-string',
551
+ 'data-table:alpha.records:bulk-actions': { widgetId: 'alpha.single-object', priority: 40, features: ['alpha.manage'] },
552
+ 'data-table:alpha.records:columns': [{ widgetId: 'alpha.array-entry', priority: 20 }],
553
+ }
554
+ `)
555
+
556
+ const facts = extractModuleExtensionFacts({
557
+ moduleId: 'alpha',
558
+ moduleRoot,
559
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
560
+ entities: [],
561
+ events: [],
562
+ apiRoutes: [],
563
+ searchEntities: [],
564
+ })
565
+ const injected = facts.contributions.filter((contribution) => contribution.source.symbol === 'injectionTable')
566
+
567
+ expect(injected).toEqual(expect.arrayContaining([
568
+ expect.objectContaining({
569
+ id: 'alpha.bare-string@crud-form:alpha.record',
570
+ kind: 'crud-form',
571
+ features: [],
572
+ placement: undefined,
573
+ details: expect.objectContaining({ entityId: 'alpha.record', payload: 'render' }),
574
+ }),
575
+ expect.objectContaining({
576
+ id: 'alpha.single-object@data-table:alpha.records:bulk-actions',
577
+ kind: 'data-table',
578
+ features: ['alpha.manage'],
579
+ placement: { priority: 40 },
580
+ details: expect.objectContaining({ payload: 'bulk-action', tableId: 'alpha.records', executionGuard: 'both' }),
581
+ }),
582
+ expect.objectContaining({
583
+ id: 'alpha.array-entry@data-table:alpha.records:columns',
584
+ kind: 'data-table',
585
+ placement: { priority: 20 },
586
+ details: expect.objectContaining({ payload: 'column', executionGuard: 'host' }),
587
+ }),
588
+ ]))
589
+ expect(injected).toHaveLength(3)
590
+
591
+ const legacyFacts = extractModuleExtensionFacts({
592
+ moduleId: 'alpha',
593
+ moduleRoot,
594
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
595
+ entities: [],
596
+ events: [],
597
+ apiRoutes: [],
598
+ searchEntities: [],
599
+ factsContractVersion: 1,
600
+ })
601
+ expect(legacyFacts.contributions.filter((contribution) => contribution.source.symbol === 'injectionTable'))
602
+ .toEqual([expect.objectContaining({ id: 'alpha.array-entry@data-table:alpha.records:columns' })])
603
+ })
604
+
315
605
  it('reports declarations whose authoritative source no longer binds the host key', () => {
316
606
  write(moduleRoot, 'extension-points.ts', `
317
607
  export const extensionPoints = defineModuleExtensionPoints({
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { extractAllModuleFacts, renderModuleFactsJson } from '../module-facts'
3
+ import { extractAllModuleFacts, isExactSourceFilePath, renderModuleFactsJson } from '../module-facts'
4
4
  import { discoverPackageModuleSources } from '../module-facts-discovery'
5
5
  import { createResolver } from '../../resolver'
6
6
 
@@ -17,11 +17,18 @@ function isUnique(values: string[]): boolean {
17
17
  return values.length === new Set(values).size
18
18
  }
19
19
 
20
+ const MARKDOWN_LINK_TARGET = /\]\(\.\.\/\.\.\/\.\.\/([^)#]+)(?:#L\d+)?\)/g
21
+
22
+ function collectLinkTargets(markdown: string): string[] {
23
+ return [...markdown.matchAll(MARKDOWN_LINK_TARGET)].map((match) => match[1])
24
+ }
25
+
20
26
  describe('module-facts BC resolve guard (T2)', () => {
21
27
  const repoRoot = findRepoRoot()
22
28
  const sources = discoverPackageModuleSources(createResolver(repoRoot))
23
29
  const extractionStartedAt = process.cpuUsage()
24
30
  const { factsByModule, markdownByModule, frameworkMarkdown } = extractAllModuleFacts({ sources })
31
+ const legacyFactsByModule = extractAllModuleFacts({ sources, factsContractVersion: 1 }).factsByModule
25
32
  const extractionCpuUsage = process.cpuUsage(extractionStartedAt)
26
33
  const extractionCpuDurationMs = (extractionCpuUsage.user + extractionCpuUsage.system) / 1_000
27
34
 
@@ -36,6 +43,23 @@ describe('module-facts BC resolve guard (T2)', () => {
36
43
  }
37
44
  })
38
45
 
46
+ it('preserves stable v1 extension arrays while exposing corrected v2 facts separately', () => {
47
+ const legacySecurity = legacyFactsByModule.security.extensionSurfaces?.contributions.find(
48
+ (contribution) => contribution.id.includes('section:auth.login.form'),
49
+ )
50
+ const v2Security = factsByModule.security.extensionSurfaces?.contributions.find(
51
+ (contribution) => contribution.id.includes('section:auth.login.form'),
52
+ )
53
+ expect(legacySecurity?.kind === 'component-override' ? legacySecurity.details.mode : null).toBe('replace')
54
+ expect(v2Security?.kind === 'component-override' ? v2Security.details.mode : null).toBe('wrapper')
55
+
56
+ const recoveredContribution = 'catalog.injection.product-bulk-delete@data-table:catalog.products.list:bulk-actions'
57
+ expect(legacyFactsByModule.catalog.extensionSurfaces?.contributions.map((entry) => entry.id))
58
+ .not.toContain(recoveredContribution)
59
+ expect(factsByModule.catalog.extensionSurfaces?.contributions.map((entry) => entry.id))
60
+ .toContain(recoveredContribution)
61
+ })
62
+
39
63
  it('keeps generated extension facts within bounded build-time and context budgets', () => {
40
64
  const completeJson = renderModuleFactsJson(factsByModule)
41
65
  const legacyJson = renderModuleFactsJson(Object.fromEntries(
@@ -82,13 +106,16 @@ describe('module-facts BC resolve guard (T2)', () => {
82
106
  // pathological regression from ordinary hardware variance and failed
83
107
  // unrelated PRs at random. 90s keeps it meaningful — a real blow-up here is
84
108
  // multiplicative, not a few percent — while leaving CI roughly 3x headroom.
85
- //
86
- // Whole-repo output caps include the additive EUDR module. Measured with the
87
- // same extractor, EUDR adds 147,395 JSON bytes and 66,037 Markdown bytes for
88
- // its real routes, ACL, events, entities, and extension surfaces; removing
89
- // only EUDR leaves 3,486,786 JSON bytes and 1,519,856 Markdown bytes. The
90
- // extension-only delta remains below its existing 1.8 MB blow-up guard.
91
109
  expect(extractionCpuDurationMs).toBeLessThan(90_000)
110
+ // JSON cap raised a fourth time by the injection-table slot normalization:
111
+ // `extractInjectionTable` previously did `if (!Array.isArray(entries)) continue`,
112
+ // silently dropping every string and single-object slot form that
113
+ // `ModuleInjectionTable` allows. Twelve real contributions across catalog, sales,
114
+ // wms, staff, integrations and checkout were therefore invisible to every fact
115
+ // consumer — `integrations` published no contributions at all. Reading them costs
116
+ // ~28KB, which is the fix working, not drift.
117
+ // The additive EUDR module contributes its real routes, ACL, events,
118
+ // entities, and extension surfaces without changing the extraction shape.
92
119
  expect(Buffer.byteLength(completeJson)).toBeLessThan(3_750_000)
93
120
  expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(1_800_000)
94
121
  // Markdown cap raised with the source-link contract: entities, events, ACL
@@ -98,6 +125,44 @@ describe('module-facts BC resolve guard (T2)', () => {
98
125
  expect(markdownBytes).toBeLessThan(1_650_000)
99
126
  })
100
127
 
128
+ it('links every generated fact to an exact resolvable file, never a directory', () => {
129
+ const packageLinkRoot = path.join(repoRoot, 'node_modules', '@open-mercato')
130
+ const canCheckDisk = fs.existsSync(packageLinkRoot)
131
+ const nonExactTargets = new Set<string>()
132
+ const unresolvedTargets = new Set<string>()
133
+ let checkedTargets = 0
134
+
135
+ for (const markdown of Object.values(markdownByModule)) {
136
+ for (const target of collectLinkTargets(markdown)) {
137
+ checkedTargets += 1
138
+ if (!isExactSourceFilePath(target)) {
139
+ nonExactTargets.add(target)
140
+ continue
141
+ }
142
+ if (!canCheckDisk) continue
143
+ const absolute = path.join(repoRoot, target)
144
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) unresolvedTargets.add(target)
145
+ }
146
+ }
147
+
148
+ expect(checkedTargets).toBeGreaterThan(1_000)
149
+ expect([...nonExactTargets]).toEqual([])
150
+ expect([...unresolvedTargets]).toEqual([])
151
+ })
152
+
153
+ it('keeps directory-valued provenance readable as plain text', () => {
154
+ const frameworkHostMarkdowns = Object.values(markdownByModule)
155
+ .filter((markdown) => markdown.includes('packages/ui/src'))
156
+ expect(frameworkHostMarkdowns.length).toBeGreaterThan(0)
157
+ for (const markdown of frameworkHostMarkdowns) {
158
+ expect(markdown).not.toContain('(../../../packages/ui/src)')
159
+ }
160
+ for (const [moduleId, facts] of Object.entries(factsByModule)) {
161
+ expect(markdownByModule[moduleId]).toContain(`Source root: ${facts.sourceRoot}\n`)
162
+ expect(markdownByModule[moduleId]).not.toContain(`(../../../${facts.sourceRoot})`)
163
+ }
164
+ })
165
+
101
166
  it('discovers a superset of the historical core modules', () => {
102
167
  const discovered = new Set(Object.keys(factsByModule))
103
168
  for (const moduleId of ['auth', 'catalog', 'customers', 'sales', 'workflows']) {