@open-mercato/cli 0.6.7-develop.6756.1.5300c41145 → 0.6.7-develop.6766.1.40c9011995

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 (79) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build.mjs +2 -1
  3. package/dist/agentic/guides/extensions.md +20 -3
  4. package/dist/agentic/guides/framework-extension-points.md +36 -0
  5. package/dist/agentic/guides/module-facts.json +39708 -230
  6. package/dist/agentic/guides/modules/ai_assistant.md +49 -9
  7. package/dist/agentic/guides/modules/api_docs.md +11 -1
  8. package/dist/agentic/guides/modules/api_keys.md +24 -1
  9. package/dist/agentic/guides/modules/attachments.md +22 -6
  10. package/dist/agentic/guides/modules/audit_logs.md +35 -1
  11. package/dist/agentic/guides/modules/auth.md +79 -15
  12. package/dist/agentic/guides/modules/business_rules.md +36 -3
  13. package/dist/agentic/guides/modules/catalog.md +160 -22
  14. package/dist/agentic/guides/modules/channel_gmail.md +14 -1
  15. package/dist/agentic/guides/modules/channel_imap.md +14 -1
  16. package/dist/agentic/guides/modules/checkout.md +134 -19
  17. package/dist/agentic/guides/modules/communication_channels.md +110 -20
  18. package/dist/agentic/guides/modules/configs.md +15 -1
  19. package/dist/agentic/guides/modules/content.md +11 -1
  20. package/dist/agentic/guides/modules/currencies.md +50 -9
  21. package/dist/agentic/guides/modules/customer_accounts.md +151 -28
  22. package/dist/agentic/guides/modules/customers.md +299 -52
  23. package/dist/agentic/guides/modules/dashboards.md +30 -1
  24. package/dist/agentic/guides/modules/data_sync.md +40 -7
  25. package/dist/agentic/guides/modules/dictionaries.md +24 -6
  26. package/dist/agentic/guides/modules/directory.md +52 -9
  27. package/dist/agentic/guides/modules/entities.md +41 -1
  28. package/dist/agentic/guides/modules/events.md +11 -1
  29. package/dist/agentic/guides/modules/feature_toggles.md +15 -1
  30. package/dist/agentic/guides/modules/gateway_stripe.md +14 -1
  31. package/dist/agentic/guides/modules/generators.md +11 -1
  32. package/dist/agentic/guides/modules/inbox_ops.md +58 -16
  33. package/dist/agentic/guides/modules/integrations.md +30 -7
  34. package/dist/agentic/guides/modules/messages.md +68 -13
  35. package/dist/agentic/guides/modules/notifications.md +13 -1
  36. package/dist/agentic/guides/modules/onboarding.md +13 -1
  37. package/dist/agentic/guides/modules/payment_gateways.md +51 -12
  38. package/dist/agentic/guides/modules/perspectives.md +16 -1
  39. package/dist/agentic/guides/modules/planner.md +33 -9
  40. package/dist/agentic/guides/modules/portal.md +24 -1
  41. package/dist/agentic/guides/modules/progress.md +34 -9
  42. package/dist/agentic/guides/modules/query_index.md +56 -3
  43. package/dist/agentic/guides/modules/record_locks.md +60 -13
  44. package/dist/agentic/guides/modules/resources.md +79 -18
  45. package/dist/agentic/guides/modules/sales.md +238 -48
  46. package/dist/agentic/guides/modules/scheduler.md +23 -7
  47. package/dist/agentic/guides/modules/search.md +28 -3
  48. package/dist/agentic/guides/modules/security.md +106 -23
  49. package/dist/agentic/guides/modules/shipping_carriers.md +38 -10
  50. package/dist/agentic/guides/modules/sso.md +47 -15
  51. package/dist/agentic/guides/modules/staff.md +117 -38
  52. package/dist/agentic/guides/modules/storage_s3.md +13 -1
  53. package/dist/agentic/guides/modules/sync_akeneo.md +13 -1
  54. package/dist/agentic/guides/modules/sync_excel.md +15 -1
  55. package/dist/agentic/guides/modules/system_status_overlays.md +14 -1
  56. package/dist/agentic/guides/modules/translations.md +25 -5
  57. package/dist/agentic/guides/modules/webhooks.md +85 -15
  58. package/dist/agentic/guides/modules/wms.md +234 -31
  59. package/dist/agentic/guides/modules/workflows.md +114 -30
  60. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +7 -0
  61. package/dist/agentic/guides/upstream/manifest.json +2 -2
  62. package/dist/agentic/shared/AGENTS.md.template +2 -2
  63. package/dist/agentic/shared/ai/harness/README.md +2 -0
  64. package/dist/agentic/shared/ai/harness/cases.json +80 -39
  65. package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +3 -1
  66. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +2 -1
  67. package/dist/agentic/shared/scripts/framework-context.mjs +3 -9
  68. package/dist/lib/generators/module-extension-facts.js +1185 -0
  69. package/dist/lib/generators/module-extension-facts.js.map +7 -0
  70. package/dist/lib/generators/module-facts.js +134 -7
  71. package/dist/lib/generators/module-facts.js.map +2 -2
  72. package/dist/lib/generators/module-registry.js +1 -0
  73. package/dist/lib/generators/module-registry.js.map +2 -2
  74. package/package.json +5 -5
  75. package/src/lib/generators/__tests__/module-extension-facts.test.ts +402 -0
  76. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +39 -2
  77. package/src/lib/generators/module-extension-facts.ts +1428 -0
  78. package/src/lib/generators/module-facts.ts +143 -6
  79. package/src/lib/generators/module-registry.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.7-develop.6756.1.5300c41145",
3
+ "version": "0.6.7-develop.6766.1.40c9011995",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.5",
61
61
  "@mikro-orm/migrations": "^7.1.5",
62
62
  "@mikro-orm/postgresql": "^7.1.5",
63
- "@open-mercato/queue": "0.6.7-develop.6756.1.5300c41145",
64
- "@open-mercato/shared": "0.6.7-develop.6756.1.5300c41145",
63
+ "@open-mercato/queue": "0.6.7-develop.6766.1.40c9011995",
64
+ "@open-mercato/shared": "0.6.7-develop.6766.1.40c9011995",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -72,10 +72,10 @@
72
72
  "typescript-js": "npm:typescript@6.0.3"
73
73
  },
74
74
  "peerDependencies": {
75
- "@open-mercato/shared": "0.6.7-develop.6756.1.5300c41145"
75
+ "@open-mercato/shared": "0.6.7-develop.6766.1.40c9011995"
76
76
  },
77
77
  "devDependencies": {
78
- "@open-mercato/shared": "0.6.7-develop.6756.1.5300c41145",
78
+ "@open-mercato/shared": "0.6.7-develop.6766.1.40c9011995",
79
79
  "@types/jest": "^30.0.0",
80
80
  "jest": "^30.4.2",
81
81
  "ts-jest": "^29.4.11"
@@ -0,0 +1,402 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import {
5
+ assertNoUnresolvedExtensionTargets,
6
+ correlateModuleExtensionFacts,
7
+ extractModuleExtensionFacts,
8
+ getFrameworkExtensionHosts,
9
+ renderFrameworkExtensionPointsMarkdown,
10
+ } from '../module-extension-facts'
11
+ import type { ModuleFactsJsonEntry } from '../module-facts'
12
+
13
+ function write(root: string, relativePath: string, source: string): void {
14
+ const filePath = path.join(root, relativePath)
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
16
+ fs.writeFileSync(filePath, source)
17
+ }
18
+
19
+ describe('module extension facts', () => {
20
+ let moduleRoot: string
21
+
22
+ beforeEach(() => {
23
+ moduleRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-extension-facts-'))
24
+ })
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(moduleRoot, { recursive: true, force: true })
28
+ })
29
+
30
+ it('expands declared host families and preserves fact references and browser transports', () => {
31
+ write(moduleRoot, 'extension-points.ts', `
32
+ import { defineModuleExtensionPoints, dataTableExtensionHost, crudFormExtensionHost, injectionExtensionHost } from 'x'
33
+ export const extensionPoints = defineModuleExtensionPoints({
34
+ moduleId: 'alpha',
35
+ hosts: {
36
+ records: dataTableExtensionHost({ tableId: 'alpha.records.list', baseSpotId: 'data-table:alpha.records', source: 'Records.tsx' }),
37
+ editor: crudFormExtensionHost({ entityId: 'alpha.record', source: 'Edit.tsx' }),
38
+ detail: injectionExtensionHost({
39
+ family: 'detail', pattern: 'detail:alpha.record:{recordId}',
40
+ parameters: { recordId: { source: 'record.id', pattern: '^[a-z]+$' } },
41
+ supported: ['render-widget'], source: 'Detail.tsx', fallbacks: ['detail:alpha.record'],
42
+ }),
43
+ },
44
+ })
45
+ `)
46
+ write(moduleRoot, 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
47
+ write(moduleRoot, 'Edit.tsx', 'export const entityId = extensionPoints.hosts.editor.entityId')
48
+ write(moduleRoot, 'Detail.tsx', 'export const spotId = extensionPoints.hosts.detail.pattern')
49
+
50
+ const facts = extractModuleExtensionFacts({
51
+ moduleId: 'alpha',
52
+ moduleRoot,
53
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
54
+ entities: [{ id: 'alpha:record' }],
55
+ events: [{ id: 'alpha.record.updated', clientBroadcast: true, portalBroadcast: true }],
56
+ apiRoutes: [{ path: '/alpha/records', methods: ['GET', 'POST'] }],
57
+ searchEntities: ['alpha:record'],
58
+ })
59
+
60
+ expect(facts.hosts).toEqual(expect.arrayContaining([
61
+ expect.objectContaining({ id: 'data-table:alpha.records:header', bound: true }),
62
+ expect.objectContaining({ id: 'data-table:alpha.records.list:columns', capabilities: ['column-widget'] }),
63
+ expect.objectContaining({ id: 'data-table:alpha.records:empty-state', bound: false }),
64
+ expect.objectContaining({ id: 'crud-form:alpha.record:fields', capabilities: ['field-widget'] }),
65
+ expect.objectContaining({ id: 'detail:alpha.record:{recordId}', resolution: 'pattern' }),
66
+ expect.objectContaining({ id: 'alpha:record', resolution: 'fact-ref' }),
67
+ expect.objectContaining({
68
+ id: 'alpha.record.updated',
69
+ capabilities: ['async-subscriber', 'sync-subscriber', 'browser-client', 'browser-portal'],
70
+ }),
71
+ expect.objectContaining({ id: 'alpha.record.querying', activation: 'caller-opt-in' }),
72
+ expect.objectContaining({ id: 'alpha.record.queried', phases: ['result-transform', 'scope-reapply'] }),
73
+ ]))
74
+ expect(facts.unresolved).toEqual([])
75
+ const hostOrder = facts.hosts.map((host) => `${host.family}\u0000${host.id}`)
76
+ expect(hostOrder).toEqual([...hostOrder].sort((left, right) => left.localeCompare(right)))
77
+ })
78
+
79
+ it('extracts outgoing mechanisms with kind-specific contracts and correlates targets', () => {
80
+ write(moduleRoot, 'extension-points.ts', `
81
+ import { defineModuleExtensionPoints, dataTableExtensionHost } from 'x'
82
+ export const extensionPoints = defineModuleExtensionPoints({
83
+ moduleId: 'alpha', hosts: { records: dataTableExtensionHost({ tableId: 'alpha.records', source: 'Records.tsx' }) },
84
+ })
85
+ `)
86
+ write(moduleRoot, 'Records.tsx', 'export const tableId = extensionPoints.hosts.records.tableId')
87
+ write(moduleRoot, 'widgets/injection-table.ts', `
88
+ export const injectionTable = {
89
+ 'data-table:alpha.records:columns': [{ widgetId: 'alpha.columns', priority: 20, features: ['alpha.view'] }],
90
+ 'menu:backend.sidebar': [{ widgetId: 'alpha.menu' }],
91
+ }
92
+ `)
93
+ write(moduleRoot, 'data/enrichers.ts', `
94
+ const enricher = {
95
+ id: 'alpha.records.enricher', targetEntity: 'alpha:record', features: ['alpha.view'], timeout: 750,
96
+ fallback: {}, queryEngine: { engines: ['json'], applyOn: ['list'] },
97
+ async enrichOne() {}, async enrichMany() {},
98
+ }
99
+ export const enrichers = [enricher]
100
+ `)
101
+ write(moduleRoot, 'api/interceptors.ts', `
102
+ export const interceptors = [{
103
+ id: 'alpha.records.interceptor', targetRoute: 'alpha/records', methods: ['POST'],
104
+ async before() {}, async after() {},
105
+ }]
106
+ `)
107
+ write(moduleRoot, 'commands/interceptors.ts', `
108
+ export const interceptors = [{
109
+ id: 'alpha.records.command', targetCommand: 'alpha.records.update',
110
+ async beforeExecute() {}, async afterUndo() {},
111
+ }]
112
+ `)
113
+ write(moduleRoot, 'commands/update.ts', `
114
+ const updateCommand = { id: 'alpha.records.update', async execute() {} }
115
+ registerCommand(updateCommand)
116
+ `)
117
+ write(moduleRoot, 'data/guards.ts', `
118
+ const guard = { id: 'alpha.records.guard', targetEntity: 'alpha:record', operations: ['update', 'delete'], async validate() {}, async afterSuccess() {} }
119
+ export const guards = [guard]
120
+ `)
121
+ write(moduleRoot, 'data/extensions.ts', `
122
+ export const extensions = [{ base: 'alpha:record', extension: 'beta:record_meta', join: { baseKey: 'id', extensionKey: 'record_id' } }]
123
+ `)
124
+ write(moduleRoot, 'subscribers/updated.ts', `
125
+ export const metadata = { event: 'alpha.record.updated', id: 'alpha:record-updated', persistent: true, priority: 10 }
126
+ `)
127
+
128
+ const surface = extractModuleExtensionFacts({
129
+ moduleId: 'alpha',
130
+ moduleRoot,
131
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
132
+ entities: [{ id: 'alpha:record' }],
133
+ events: [{ id: 'alpha.record.updated', clientBroadcast: true }],
134
+ apiRoutes: [{ path: '/alpha/records', methods: ['POST'] }],
135
+ searchEntities: [],
136
+ })
137
+ const correlated = correlateModuleExtensionFacts({
138
+ surfacesByModule: { alpha: surface },
139
+ entityIds: new Set(['alpha:record']),
140
+ eventIds: new Set(['alpha.record.updated']),
141
+ apiRoutes: new Set(['/alpha/records']),
142
+ commandIds: new Set(['alpha.records.update']),
143
+ }).alpha
144
+
145
+ expect(correlated.contributions).toEqual(expect.arrayContaining([
146
+ expect.objectContaining({
147
+ id: 'alpha.records.enricher',
148
+ kind: 'response-enricher',
149
+ details: expect.objectContaining({
150
+ targetEntity: 'alpha:record', surfaces: ['list', 'detail'], timeoutMs: 750,
151
+ queryEngine: { engines: ['json'], applyOn: ['list'], activation: 'caller-opt-in' },
152
+ }),
153
+ }),
154
+ expect.objectContaining({
155
+ id: 'alpha.records.interceptor', kind: 'api-interceptor',
156
+ targets: [expect.objectContaining({
157
+ resolution: 'fact-ref',
158
+ factRef: { factSection: 'apiRoutes', factKey: '/alpha/records' },
159
+ })],
160
+ details: expect.objectContaining({ phases: ['before', 'after'] }),
161
+ }),
162
+ expect.objectContaining({
163
+ id: 'alpha.records.command',
164
+ targets: [expect.objectContaining({ factRef: { factSection: 'commands', factKey: 'alpha.records.update' } })],
165
+ details: expect.objectContaining({ phases: ['before-execute', 'after-undo'] }),
166
+ }),
167
+ expect.objectContaining({ id: 'alpha.records.guard', details: expect.objectContaining({ optimisticLock: 'preserved' }) }),
168
+ expect.objectContaining({ kind: 'entity-extension', details: expect.objectContaining({ linkId: 'id:record_id' }) }),
169
+ expect.objectContaining({ id: 'alpha:record-updated', details: expect.objectContaining({ persistent: true, sync: false, priority: 10 }) }),
170
+ expect.objectContaining({ id: 'alpha.columns@data-table:alpha.records:columns', targets: [expect.objectContaining({ resolution: 'exact' })] }),
171
+ expect.objectContaining({ id: 'alpha.menu@menu:backend.sidebar', targets: [expect.objectContaining({ resolution: 'framework' })] }),
172
+ expect.objectContaining({ id: 'alpha.record.updated.browser', kind: 'browser-reaction' }),
173
+ ]))
174
+ expect(correlated.unresolved).toEqual([])
175
+ const contributionOrder = correlated.contributions.map((contribution) => `${contribution.kind}\u0000${contribution.id}`)
176
+ expect(contributionOrder).toEqual([...contributionOrder].sort((left, right) => left.localeCompare(right)))
177
+ })
178
+
179
+ it('renders a deterministic separate framework catalog', () => {
180
+ type ExtensionSurfacesAreOptional = {} extends Pick<ModuleFactsJsonEntry, 'extensionSurfaces'> ? true : false
181
+ const extensionSurfacesAreOptional: ExtensionSurfacesAreOptional = true
182
+ const first = renderFrameworkExtensionPointsMarkdown()
183
+ const second = renderFrameworkExtensionPointsMarkdown()
184
+ expect(second).toBe(first)
185
+ expect(first).toContain('# Framework extension points (generated, do not edit)')
186
+ expect(first).toContain('integrations.detail:{integrationId}')
187
+ expect(first).toContain('menu:backend.sidebar')
188
+ expect(extensionSurfacesAreOptional).toBe(true)
189
+ })
190
+
191
+ it('projects every wired unified override key with its supported mode', () => {
192
+ const overrides = getFrameworkExtensionHosts()
193
+ .filter((host) => host.family === 'module-override')
194
+ .map((host) => [host.id, host.operations?.[0]])
195
+
196
+ expect(overrides).toEqual([
197
+ ['module-override:acl.features', 'disable-replace'],
198
+ ['module-override:ai.agents', 'disable-replace'],
199
+ ['module-override:ai.extensions', 'additive'],
200
+ ['module-override:ai.tools', 'disable-replace'],
201
+ ['module-override:cli', 'disable-replace'],
202
+ ['module-override:commandInterceptors', 'disable-replace'],
203
+ ['module-override:di', 'disable-replace'],
204
+ ['module-override:encryption.maps', 'disable-replace'],
205
+ ['module-override:enrichers', 'disable-replace'],
206
+ ['module-override:events.subscribers', 'disable-replace'],
207
+ ['module-override:guards', 'disable-replace'],
208
+ ['module-override:interceptors', 'disable-replace'],
209
+ ['module-override:nav.groupOrder', 'additive'],
210
+ ['module-override:notifications.handlers', 'disable-replace'],
211
+ ['module-override:notifications.types', 'disable-replace'],
212
+ ['module-override:routes.api', 'disable-replace'],
213
+ ['module-override:routes.pages', 'disable-replace'],
214
+ ['module-override:setup', 'replace'],
215
+ ['module-override:widgets.components', 'disable-replace'],
216
+ ['module-override:widgets.dashboard', 'disable-replace'],
217
+ ['module-override:widgets.injection', 'disable-replace'],
218
+ ['module-override:workers', 'disable-replace'],
219
+ ])
220
+ })
221
+
222
+ it('projects dashboard, notification, integration, vector, provider, and workflow registries', () => {
223
+ write(moduleRoot, 'notifications.handlers.ts', `
224
+ export const notificationHandlers = [{
225
+ id: 'alpha.alert-toast', notificationType: 'alpha.alert', features: ['alpha.view'], priority: 80,
226
+ handle() {},
227
+ }]
228
+ `)
229
+ write(moduleRoot, 'integration.ts', `
230
+ export const integration = { id: 'alpha_gateway', category: 'payment', providerKey: 'alpha-pay' }
231
+ export const integrations = [integration]
232
+ `)
233
+ write(moduleRoot, 'vector.ts', `
234
+ export const vectorConfig = { entities: [{ entityId: 'alpha:record' }] }
235
+ export default vectorConfig
236
+ `)
237
+ write(moduleRoot, 'workflows.ts', `
238
+ const approval = defineWorkflow({ workflowId: 'alpha.approval', steps: [], transitions: [] })
239
+ export const workflowsConfig = createWorkflowsModuleConfig({ moduleId: 'alpha', workflows: [approval] })
240
+ `)
241
+ write(moduleRoot, 'di.ts', `
242
+ export function register() {
243
+ registerPaymentGatewayDescriptor({ providerKey: 'alpha-pay', label: 'Alpha Pay' })
244
+ registerShippingAdapter({ providerKey: 'alpha-ship' })
245
+ registerCurrencyProvider({ id: 'alpha-rates' })
246
+ }
247
+ `)
248
+ write(moduleRoot, 'widgets/dashboard/summary/widget.ts', `
249
+ const widget = { metadata: { id: 'alpha.dashboard.summary', features: ['alpha.view'] }, Widget() {} }
250
+ export default widget
251
+ `)
252
+
253
+ const facts = extractModuleExtensionFacts({
254
+ moduleId: 'alpha',
255
+ moduleRoot,
256
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
257
+ entities: [],
258
+ events: [],
259
+ apiRoutes: [],
260
+ searchEntities: ['alpha:record'],
261
+ notifications: ['alpha.alert'],
262
+ aiTools: [{ name: 'alpha_lookup', sourcePath: 'node_modules/pkg/src/modules/alpha/ai-tools.ts' }],
263
+ aiAgents: [{ id: 'alpha_agent', sourcePath: 'node_modules/pkg/src/modules/alpha/ai-agents.ts' }],
264
+ })
265
+
266
+ expect(facts.contributions).toEqual(expect.arrayContaining([
267
+ expect.objectContaining({
268
+ id: 'alpha.alert-toast',
269
+ kind: 'browser-reaction',
270
+ targets: [expect.objectContaining({ id: 'alpha.alert', resolution: 'fact-ref' })],
271
+ details: expect.objectContaining({ transports: ['notification-effect'], hooks: ['useNotificationEffect'] }),
272
+ }),
273
+ expect.objectContaining({ id: 'dashboard:alpha.dashboard.summary', kind: 'widget', details: expect.objectContaining({ payload: 'dashboard' }) }),
274
+ expect.objectContaining({ id: 'integration:alpha_gateway', details: expect.objectContaining({ specialistRoute: 'integrations' }) }),
275
+ expect.objectContaining({ id: 'notification:alpha.alert', details: expect.objectContaining({ registry: 'notification' }) }),
276
+ expect.objectContaining({ id: 'search:alpha:record', details: expect.objectContaining({ registry: 'search' }) }),
277
+ expect.objectContaining({ id: 'vector:alpha:record', details: expect.objectContaining({ registry: 'vector' }) }),
278
+ expect.objectContaining({ id: 'ai:alpha_lookup', details: expect.objectContaining({ specialistRoute: 'aiTools' }) }),
279
+ expect.objectContaining({ id: 'ai:alpha_agent', details: expect.objectContaining({ specialistRoute: 'aiAgents' }) }),
280
+ expect.objectContaining({ id: 'payment:alpha-pay', source: expect.objectContaining({ symbol: 'registerPaymentGatewayDescriptor' }) }),
281
+ expect.objectContaining({ id: 'shipping:alpha-ship', details: expect.objectContaining({ specialistRoute: 'shippingCarriers' }) }),
282
+ expect.objectContaining({ id: 'currency:alpha-rates', details: expect.objectContaining({ specialistRoute: 'currencies' }) }),
283
+ expect.objectContaining({ id: 'workflow:alpha.approval', details: expect.objectContaining({ specialistRoute: 'workflows' }) }),
284
+ ]))
285
+ expect(facts.contributions.filter((fact) => fact.id === 'payment:alpha-pay')).toHaveLength(1)
286
+ })
287
+
288
+ it('resolves factory-built contributions and generated entity registry references', () => {
289
+ write(moduleRoot, 'data/enrichers.ts', `
290
+ function makeEnricher(targetEntity) {
291
+ return { id: \`alpha.image:\${targetEntity}\`, targetEntity, async enrichMany(records) { return records } }
292
+ }
293
+ const registryEnricher = { id: 'alpha.registry', targetEntity: E.alpha.record, async enrichOne(record) { return record } }
294
+ export const enrichers = [makeEnricher('alpha:line'), registryEnricher]
295
+ `)
296
+
297
+ const facts = extractModuleExtensionFacts({
298
+ moduleId: 'alpha',
299
+ moduleRoot,
300
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
301
+ entities: [],
302
+ events: [],
303
+ apiRoutes: [],
304
+ searchEntities: [],
305
+ })
306
+
307
+ expect(facts.contributions).toEqual(expect.arrayContaining([
308
+ expect.objectContaining({ id: 'alpha.image:alpha:line', targets: [expect.objectContaining({ id: 'alpha:line' })] }),
309
+ expect.objectContaining({ id: 'alpha.registry', targets: [expect.objectContaining({ id: 'alpha:record' })] }),
310
+ ]))
311
+ })
312
+
313
+ it('reports declarations whose authoritative source no longer binds the host key', () => {
314
+ write(moduleRoot, 'extension-points.ts', `
315
+ export const extensionPoints = defineModuleExtensionPoints({
316
+ moduleId: 'alpha',
317
+ hosts: { records: dataTableExtensionHost({ tableId: 'alpha.records', source: 'Records.tsx' }) },
318
+ })
319
+ `)
320
+ write(moduleRoot, 'Records.tsx', "export const tableId = 'alpha.records'")
321
+
322
+ const facts = extractModuleExtensionFacts({
323
+ moduleId: 'alpha',
324
+ moduleRoot,
325
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
326
+ entities: [],
327
+ events: [],
328
+ apiRoutes: [],
329
+ searchEntities: [],
330
+ })
331
+
332
+ expect(facts.unresolved).toEqual([
333
+ expect.objectContaining({ key: 'alpha.records', reason: 'unbound-declaration' }),
334
+ ])
335
+ expect(facts.hosts.filter((host) => host.key.startsWith('records.')).every((host) => !host.bound)).toBe(true)
336
+ })
337
+
338
+ it('fails closed when a correlated first-party target remains unresolved', () => {
339
+ expect(() => assertNoUnresolvedExtensionTargets({
340
+ alpha: {
341
+ hosts: [],
342
+ contributions: [],
343
+ unresolved: [{
344
+ key: 'alpha.widget:data-table:alpha.missing',
345
+ source: { path: 'widgets/injection-table.ts' },
346
+ reason: 'unresolved-first-party-target',
347
+ }],
348
+ },
349
+ })).toThrow('alpha:alpha.widget:data-table:alpha.missing')
350
+ })
351
+
352
+ it('does not trust a claimed fact reference before matching a known first-party host', () => {
353
+ write(moduleRoot, 'data/enrichers.ts', `
354
+ export const enrichers = [{
355
+ id: 'alpha.typo', targetEntity: 'alpha.missing', async enrichOne(record) { return record },
356
+ }]
357
+ `)
358
+ const surface = extractModuleExtensionFacts({
359
+ moduleId: 'alpha',
360
+ moduleRoot,
361
+ sourceRoot: 'node_modules/pkg/src/modules/alpha',
362
+ entities: [{ id: 'alpha:record' }],
363
+ events: [],
364
+ apiRoutes: [],
365
+ searchEntities: [],
366
+ })
367
+ const correlated = correlateModuleExtensionFacts({
368
+ surfacesByModule: { alpha: surface },
369
+ entityIds: new Set(['alpha:record']),
370
+ eventIds: new Set(),
371
+ apiRoutes: new Set(),
372
+ commandIds: new Set(),
373
+ })
374
+
375
+ expect(() => assertNoUnresolvedExtensionTargets(correlated)).toThrow('alpha.typo:alpha.missing')
376
+ })
377
+
378
+ it('rejects wildcard targets that match no known first-party contract', () => {
379
+ const correlated = correlateModuleExtensionFacts({
380
+ surfacesByModule: {
381
+ alpha: {
382
+ hosts: [],
383
+ contributions: [{
384
+ id: 'alpha.typo-pattern',
385
+ kind: 'command-interceptor',
386
+ targets: [{ id: 'alpha.typo.*', resolution: 'unresolved' }],
387
+ scopeContract: 'tenant-and-organization',
388
+ source: { path: 'commands/interceptors.ts' },
389
+ details: { targetCommand: 'alpha.typo.*', phases: ['before-execute'] },
390
+ }],
391
+ unresolved: [],
392
+ },
393
+ },
394
+ entityIds: new Set(),
395
+ eventIds: new Set(),
396
+ apiRoutes: new Set(),
397
+ commandIds: new Set(['alpha.records.update']),
398
+ })
399
+
400
+ expect(() => assertNoUnresolvedExtensionTargets(correlated)).toThrow('alpha.typo-pattern:alpha.typo.*')
401
+ })
402
+ })
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { extractAllModuleFacts } from '../module-facts'
3
+ import { extractAllModuleFacts, renderModuleFactsJson } from '../module-facts'
4
4
  import { discoverPackageModuleSources } from '../module-facts-discovery'
5
5
  import { createResolver } from '../../resolver'
6
6
 
@@ -20,7 +20,34 @@ function isUnique(values: string[]): boolean {
20
20
  describe('module-facts BC resolve guard (T2)', () => {
21
21
  const repoRoot = findRepoRoot()
22
22
  const sources = discoverPackageModuleSources(createResolver(repoRoot))
23
- const { factsByModule } = extractAllModuleFacts({ sources })
23
+ const extractionStartedAt = performance.now()
24
+ const { factsByModule, markdownByModule, frameworkMarkdown } = extractAllModuleFacts({ sources })
25
+ const extractionDurationMs = performance.now() - extractionStartedAt
26
+
27
+ it('emits complete, deterministic extension catalogs for every resolved module', () => {
28
+ const repeated = extractAllModuleFacts({ sources })
29
+ expect(renderModuleFactsJson(repeated.factsByModule)).toBe(renderModuleFactsJson(factsByModule))
30
+ expect(repeated.markdownByModule).toEqual(markdownByModule)
31
+ expect(repeated.frameworkMarkdown).toBe(frameworkMarkdown)
32
+ for (const facts of Object.values(factsByModule)) {
33
+ expect(facts.extensionSurfaces).toBeDefined()
34
+ expect(facts.extensionSurfaces?.unresolved).toEqual([])
35
+ }
36
+ })
37
+
38
+ it('keeps generated extension facts within bounded build-time and context budgets', () => {
39
+ const completeJson = renderModuleFactsJson(factsByModule)
40
+ const legacyJson = renderModuleFactsJson(Object.fromEntries(
41
+ Object.entries(factsByModule).map(([moduleId, facts]) => [moduleId, { ...facts, extensionSurfaces: undefined }]),
42
+ ))
43
+ const markdownBytes = Object.values(markdownByModule)
44
+ .reduce((total, markdown) => total + Buffer.byteLength(markdown), Buffer.byteLength(frameworkMarkdown))
45
+
46
+ expect(extractionDurationMs).toBeLessThan(30_000)
47
+ expect(Buffer.byteLength(completeJson)).toBeLessThan(2_000_000)
48
+ expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(1_500_000)
49
+ expect(markdownBytes).toBeLessThan(1_000_000)
50
+ })
24
51
 
25
52
  it('discovers a superset of the historical core modules', () => {
26
53
  const discovered = new Set(Object.keys(factsByModule))
@@ -30,6 +57,16 @@ describe('module-facts BC resolve guard (T2)', () => {
30
57
  expect(discovered.size).toBeGreaterThan(9)
31
58
  })
32
59
 
60
+ it('keeps factory-built and generated-registry enricher contributions visible', () => {
61
+ expect(factsByModule.sales.extensionSurfaces?.contributions).toEqual(expect.arrayContaining([
62
+ expect.objectContaining({ id: 'sales.catalog-image:sales:sales_quote_line', kind: 'response-enricher' }),
63
+ expect.objectContaining({ id: 'sales.catalog-image:sales:sales_order_line', kind: 'response-enricher' }),
64
+ ]))
65
+ expect(factsByModule.wms.extensionSurfaces?.contributions).toEqual(expect.arrayContaining([
66
+ expect.objectContaining({ id: 'wms.sales-order-inventory', kind: 'response-enricher' }),
67
+ ]))
68
+ })
69
+
33
70
  for (const source of sources) {
34
71
  const moduleId = source.moduleId
35
72
  describe(`${moduleId}`, () => {