@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
@@ -0,0 +1,414 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import {
5
+ assertPackageModuleFactsOnly,
6
+ assertPortableSourceRoot,
7
+ buildModuleFactCoverageLedger,
8
+ computeModuleSourceFingerprint,
9
+ computeModuleTaxonomyFingerprint,
10
+ extractAllModuleFacts,
11
+ extractLocalReferenceModuleFacts,
12
+ extractModuleFacts,
13
+ renderModuleFactsJson,
14
+ renderReferenceModuleFactsJson,
15
+ toModuleFactsJsonEntry,
16
+ type ModuleFactSource,
17
+ } from '../module-facts'
18
+ import {
19
+ appendLocalReferenceModuleSource,
20
+ discoverLocalReferenceModuleSource,
21
+ } from '../module-facts-discovery'
22
+
23
+ function findRepoRoot(): string {
24
+ let dir = __dirname
25
+ for (let depth = 0; depth < 10; depth += 1) {
26
+ if (fs.existsSync(path.join(dir, 'packages', 'core', 'src', 'modules'))) return dir
27
+ dir = path.dirname(dir)
28
+ }
29
+ throw new Error('[internal] could not locate repo root from the test directory')
30
+ }
31
+
32
+ function writeFile(filePath: string, contents: string): void {
33
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
34
+ fs.writeFileSync(filePath, contents)
35
+ }
36
+
37
+ /**
38
+ * A package-provided module that owns an event, so an app-local contribution targeting it
39
+ * exercises real cross-module correlation instead of a self-contained stub.
40
+ */
41
+ function writePackageModule(moduleRoot: string): void {
42
+ writeFile(
43
+ path.join(moduleRoot, 'index.ts'),
44
+ "export const metadata = { name: 'alpha', title: 'Alpha', description: 'Package module' }\n",
45
+ )
46
+ writeFile(
47
+ path.join(moduleRoot, 'events.ts'),
48
+ "export const events = [{ id: 'alpha.thing.created', label: 'Thing Created', entity: 'thing', category: 'crud' }]\n",
49
+ )
50
+ writeFile(
51
+ path.join(moduleRoot, 'acl.ts'),
52
+ "export const features = [{ id: 'alpha.view', title: 'View alpha', module: 'alpha' }]\n",
53
+ )
54
+ }
55
+
56
+ /** An app-local module whose subscriber contributes to the package module's event. */
57
+ function writeLocalModule(moduleRoot: string): void {
58
+ writeFile(
59
+ path.join(moduleRoot, 'index.ts'),
60
+ "export const metadata = { name: 'demo_local', title: 'Demo Local', description: 'App-local reference' }\n",
61
+ )
62
+ writeFile(
63
+ path.join(moduleRoot, 'acl.ts'),
64
+ "export const features = [{ id: 'demo_local.view', title: 'View demo', module: 'demo_local' }]\n",
65
+ )
66
+ writeFile(
67
+ path.join(moduleRoot, 'events.ts'),
68
+ "export const events = [{ id: 'demo_local.pinged', label: 'Pinged', entity: 'ping', category: 'crud' }]\n",
69
+ )
70
+ writeFile(
71
+ path.join(moduleRoot, 'subscribers', 'on-alpha-thing.ts'),
72
+ [
73
+ "export const metadata = { event: 'alpha.thing.created', id: 'demo_local:on-alpha-thing', persistent: false }",
74
+ 'export default async function handler(): Promise<void> {}',
75
+ '',
76
+ ].join('\n'),
77
+ )
78
+ writeFile(
79
+ path.join(moduleRoot, '__tests__', 'excluded.test.ts'),
80
+ "it('is excluded from the source fingerprint', () => { expect(true).toBe(true) })\n",
81
+ )
82
+ }
83
+
84
+ describe('local-reference module fact emission', () => {
85
+ let tmp: string
86
+ let appRoot: string
87
+ let packageSources: ModuleFactSource[]
88
+ let reference: ModuleFactSource
89
+
90
+ beforeAll(() => {
91
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'module-facts-local-reference-'))
92
+ appRoot = path.join(tmp, 'app')
93
+ const packageModuleRoot = path.join(appRoot, 'node_modules', '@acme', 'alpha-pkg', 'src', 'modules', 'alpha')
94
+ writePackageModule(packageModuleRoot)
95
+ writeLocalModule(path.join(appRoot, 'src', 'modules', 'demo_local'))
96
+ packageSources = [{
97
+ moduleId: 'alpha',
98
+ moduleRoot: packageModuleRoot,
99
+ from: '@acme/alpha-pkg',
100
+ packageVersion: '1.2.3',
101
+ }]
102
+ const discovered = discoverLocalReferenceModuleSource({ appRoot, moduleId: 'demo_local' })
103
+ if (!discovered) throw new Error('[internal] fixture local module was not discovered')
104
+ reference = discovered
105
+ })
106
+
107
+ afterAll(() => {
108
+ fs.rmSync(tmp, { recursive: true, force: true })
109
+ })
110
+
111
+ it('discovers the app-local module with portable local-reference provenance', () => {
112
+ expect(reference).toEqual({
113
+ moduleId: 'demo_local',
114
+ moduleRoot: path.join(appRoot, 'src', 'modules', 'demo_local'),
115
+ portableSourceRoot: 'src/modules/demo_local',
116
+ sourceKind: 'local-reference',
117
+ })
118
+ expect(discoverLocalReferenceModuleSource({ appRoot, moduleId: 'absent' })).toBeNull()
119
+ })
120
+
121
+ it('leaves the normal package sidecar byte-identical when the reference bundle is produced', () => {
122
+ const before = renderModuleFactsJson(extractAllModuleFacts({ sources: packageSources }).factsByModule)
123
+ extractLocalReferenceModuleFacts({ packageSources, reference })
124
+ const after = renderModuleFactsJson(extractAllModuleFacts({ sources: packageSources }).factsByModule)
125
+
126
+ expect(after).toBe(before)
127
+ const parsed = JSON.parse(before) as Record<string, Record<string, unknown>>
128
+ expect(Object.keys(parsed)).toEqual(['alpha'])
129
+ expect(parsed.alpha.sourceRoot).toBe('node_modules/@acme/alpha-pkg/src/modules/alpha')
130
+ // The emitted package entry is a STABLE generated-file contract
131
+ // (BACKWARD_COMPATIBILITY.md §14): this slice may add keys to the reference
132
+ // projection but must add none — including `sourceKind` — to a package entry.
133
+ expect(Object.keys(parsed.alpha)).toEqual([
134
+ 'title',
135
+ 'description',
136
+ 'coreVersion',
137
+ 'sourcePackage',
138
+ 'sourceVersion',
139
+ 'sourceRoot',
140
+ 'entities',
141
+ 'events',
142
+ 'aclFeatures',
143
+ 'apiRoutes',
144
+ 'diTokens',
145
+ 'searchEntities',
146
+ 'hostTokens',
147
+ 'notifications',
148
+ 'cli',
149
+ 'backendPages',
150
+ 'frontendPages',
151
+ 'cliCommands',
152
+ 'aiTools',
153
+ 'aiAgents',
154
+ 'extensionSurfaces',
155
+ 'factSources',
156
+ 'ownedContracts',
157
+ 'overrideTargets',
158
+ ])
159
+ })
160
+
161
+ it('resolves the local contribution inside the disposable batch without touching normal output', () => {
162
+ const { entry } = extractLocalReferenceModuleFacts({ packageSources, reference })
163
+ const contribution = entry.facts.extensionSurfaces?.contributions
164
+ .find((candidate) => candidate.id === 'demo_local:on-alpha-thing')
165
+ expect(contribution?.targets).toEqual([
166
+ expect.objectContaining({ id: 'alpha.thing.created', resolution: 'fact-ref' }),
167
+ ])
168
+
169
+ const activatedAlpha = extractAllModuleFacts({
170
+ sources: appendLocalReferenceModuleSource(packageSources, reference),
171
+ }).factsByModule.alpha
172
+ const activatedIncoming = activatedAlpha.extensionSurfaces?.incoming ?? []
173
+ expect(activatedIncoming.map((incoming) => incoming.contributionId)).toContain('demo_local:on-alpha-thing')
174
+
175
+ const normalAlpha = extractAllModuleFacts({ sources: packageSources }).factsByModule.alpha
176
+ expect(normalAlpha.extensionSurfaces?.incoming ?? []).toEqual([])
177
+ })
178
+
179
+ it('matches the disabled reference projection to a fresh activated extraction', () => {
180
+ const { entry } = extractLocalReferenceModuleFacts({ packageSources, reference })
181
+ const activated = extractAllModuleFacts({
182
+ sources: appendLocalReferenceModuleSource(packageSources, reference),
183
+ }).factsByModule.demo_local
184
+
185
+ expect(renderModuleFactsJson({ demo_local: activated }))
186
+ .toBe(renderModuleFactsJson({ demo_local: entry.facts }))
187
+ expect(extractAllModuleFacts({ sources: packageSources }).factsByModule.demo_local).toBeUndefined()
188
+ })
189
+
190
+ it('emits the reference bundle discriminators and never a node_modules path', () => {
191
+ const { entry, markdown } = extractLocalReferenceModuleFacts({ packageSources, reference })
192
+ expect(entry.moduleId).toBe('demo_local')
193
+ expect(entry.projectionKind).toBe('activated-reference')
194
+ expect(entry.sourceKind).toBe('local-reference')
195
+ expect(entry.runtimeSelected).toBe(false)
196
+ expect(entry.facts.sourceRoot).toBe('src/modules/demo_local')
197
+ expect(entry.facts.sourceKind).toBe('local-reference')
198
+ expect(entry.factCoverage).toEqual(buildModuleFactCoverageLedger())
199
+
200
+ const serialized = renderReferenceModuleFactsJson({ demo_local: entry })
201
+ expect(serialized.endsWith('\n')).toBe(true)
202
+ expect(serialized).not.toContain('node_modules')
203
+ expect(JSON.parse(serialized)).toEqual({ demo_local: entry })
204
+
205
+ expect(markdown).not.toContain('node_modules')
206
+ for (const target of [...markdown.matchAll(/\]\(\.\.\/\.\.\/\.\.\/([^)#]+)(?:#L\d+)?\)/g)].map((match) => match[1])) {
207
+ expect(target.startsWith('src/modules/demo_local/')).toBe(true)
208
+ expect(fs.existsSync(path.join(appRoot, target))).toBe(true)
209
+ }
210
+ })
211
+
212
+ it('states the opt-in boundary and both fingerprints in the reference markdown header', () => {
213
+ const { entry, markdown } = extractLocalReferenceModuleFacts({ packageSources, reference })
214
+ const [heading, stamp, notice, sourceRootLine] = markdown.split('\n')
215
+ expect(heading).toBe('# demo_local — module facts (generated reference projection, do not edit)')
216
+ expect(stamp).toContain('generated from local reference src/modules/demo_local')
217
+ expect(stamp).toContain(entry.sourceFingerprint)
218
+ expect(stamp).toContain(entry.taxonomyFingerprint)
219
+ expect(notice).toContain('not selected by the current runtime')
220
+ expect(notice).toContain('only after opt-in')
221
+ expect(sourceRootLine).toBe('Source root: src/modules/demo_local')
222
+
223
+ const packageMarkdown = extractAllModuleFacts({ sources: packageSources }).markdownByModule.alpha
224
+ expect(packageMarkdown.split('\n')[0]).toBe('# alpha — module facts (generated, do not edit)')
225
+ expect(packageMarkdown).toContain('generated from @acme/alpha-pkg 1.2.3')
226
+ expect(packageMarkdown).not.toContain('after opt-in')
227
+ })
228
+
229
+ it('rejects a duplicate module id before the local reference is assigned', () => {
230
+ const collidingPackage: ModuleFactSource[] = [
231
+ ...packageSources,
232
+ { moduleId: 'demo_local', moduleRoot: '/somewhere/pkg/src/modules/demo_local', from: '@acme/demo-pkg' },
233
+ ]
234
+ expect(() => appendLocalReferenceModuleSource(collidingPackage, reference))
235
+ .toThrow(/local reference module "demo_local" collides with the module provided by @acme\/demo-pkg/)
236
+ expect(() => extractLocalReferenceModuleFacts({ packageSources: collidingPackage, reference }))
237
+ .toThrow(/collides with the module provided by/)
238
+ })
239
+
240
+ it('refuses a reference source that is not declared portable and local', () => {
241
+ expect(() => extractLocalReferenceModuleFacts({
242
+ packageSources,
243
+ reference: { ...reference, sourceKind: undefined },
244
+ })).toThrow(/must declare sourceKind "local-reference"/)
245
+ expect(() => extractLocalReferenceModuleFacts({
246
+ packageSources,
247
+ reference: { ...reference, portableSourceRoot: undefined },
248
+ })).toThrow(/must declare portableSourceRoot/)
249
+ for (const invalid of ['/abs/modules/demo', 'C:/modules/demo', 'src\\modules\\demo', '../modules/demo', '']) {
250
+ expect(() => assertPortableSourceRoot('demo_local', invalid))
251
+ .toThrow(/must be a relative POSIX app path/)
252
+ }
253
+ })
254
+
255
+ it('fails the build when an app-local module reaches the normal package output', () => {
256
+ const activated = extractAllModuleFacts({
257
+ sources: appendLocalReferenceModuleSource(packageSources, reference),
258
+ })
259
+ expect(() => assertPackageModuleFactsOnly(activated.factsByModule))
260
+ .toThrow(/must not contain app-local modules: demo_local \(src\/modules\/demo_local\)/)
261
+ expect(() => assertPackageModuleFactsOnly({
262
+ alpha: { ...activated.factsByModule.alpha, sourceRoot: 'src/modules/alpha' },
263
+ })).toThrow(/must not contain app-local modules: alpha \(src\/modules\/alpha\)/)
264
+ expect(() => assertPackageModuleFactsOnly(
265
+ extractAllModuleFacts({ sources: packageSources }).factsByModule,
266
+ )).not.toThrow()
267
+ })
268
+
269
+ it('keeps the strict unresolved-target policy for normal output and collects it for the projection', () => {
270
+ const danglingRoot = path.join(appRoot, 'src', 'modules', 'demo_dangling')
271
+ writeLocalModule(danglingRoot)
272
+ writeFile(
273
+ path.join(danglingRoot, 'subscribers', 'on-missing.ts'),
274
+ [
275
+ "export const metadata = { event: 'demo_dangling.never.declared', id: 'demo_dangling:on-missing', persistent: false }",
276
+ 'export default async function handler(): Promise<void> {}',
277
+ '',
278
+ ].join('\n'),
279
+ )
280
+ const dangling: ModuleFactSource = {
281
+ moduleId: 'demo_dangling',
282
+ moduleRoot: danglingRoot,
283
+ portableSourceRoot: 'src/modules/demo_dangling',
284
+ sourceKind: 'local-reference',
285
+ }
286
+
287
+ expect(() => extractAllModuleFacts({ sources: appendLocalReferenceModuleSource(packageSources, dangling) }))
288
+ .toThrow(/unresolved first-party extension targets: demo_dangling:demo_dangling:on-missing:demo_dangling.never.declared/)
289
+
290
+ const { entry, unresolvedTargets } = extractLocalReferenceModuleFacts({
291
+ packageSources,
292
+ reference: dangling,
293
+ })
294
+ expect(unresolvedTargets).toEqual([
295
+ 'demo_dangling:demo_dangling:on-missing:demo_dangling.never.declared',
296
+ ])
297
+ expect(entry.facts.extensionSurfaces?.unresolved.map((item) => item.key))
298
+ .toContain('demo_dangling:on-missing:demo_dangling.never.declared')
299
+ })
300
+
301
+ it('fingerprints the emitted source tree and the taxonomy independently', () => {
302
+ const first = extractLocalReferenceModuleFacts({ packageSources, reference }).entry
303
+ expect(first.sourceFingerprint).toMatch(/^[0-9a-f]{64}$/)
304
+ expect(first.taxonomyFingerprint).toMatch(/^[0-9a-f]{64}$/)
305
+ expect(first.sourceFingerprint).not.toBe(first.taxonomyFingerprint)
306
+
307
+ const testFilePath = path.join(reference.moduleRoot, '__tests__', 'excluded.test.ts')
308
+ fs.writeFileSync(testFilePath, "it('still excluded', () => { expect(1).toBe(1) })\n")
309
+ expect(computeModuleSourceFingerprint(reference.moduleRoot, 'src/modules/demo_local'))
310
+ .toBe(first.sourceFingerprint)
311
+
312
+ const aclPath = path.join(reference.moduleRoot, 'acl.ts')
313
+ const originalAcl = fs.readFileSync(aclPath, 'utf8')
314
+ try {
315
+ fs.writeFileSync(
316
+ aclPath,
317
+ [
318
+ "export const features = [",
319
+ " { id: 'demo_local.view', title: 'View demo', module: 'demo_local' },",
320
+ " { id: 'demo_local.manage', title: 'Manage demo', module: 'demo_local' },",
321
+ ']',
322
+ '',
323
+ ].join('\n'),
324
+ )
325
+ const changed = extractLocalReferenceModuleFacts({ packageSources, reference }).entry
326
+ expect(changed.sourceFingerprint).not.toBe(first.sourceFingerprint)
327
+ expect(changed.taxonomyFingerprint).not.toBe(first.taxonomyFingerprint)
328
+ expect(changed.facts.aclFeatures).toEqual(['demo_local.view', 'demo_local.manage'])
329
+ } finally {
330
+ fs.writeFileSync(aclPath, originalAcl)
331
+ }
332
+
333
+ const restored = extractLocalReferenceModuleFacts({ packageSources, reference }).entry
334
+ expect(restored.sourceFingerprint).toBe(first.sourceFingerprint)
335
+ expect(restored.taxonomyFingerprint).toBe(first.taxonomyFingerprint)
336
+ })
337
+
338
+ it('canonicalizes the taxonomy fingerprint across key and set ordering but not across values', () => {
339
+ const { entry } = extractLocalReferenceModuleFacts({ packageSources, reference })
340
+ const reordered = {
341
+ ...entry.facts,
342
+ aclFeatures: [...entry.facts.aclFeatures].reverse(),
343
+ coreVersion: '99.99.99',
344
+ sourceVersion: 'ignored-by-taxonomy',
345
+ }
346
+ expect(computeModuleTaxonomyFingerprint(reordered)).toBe(entry.taxonomyFingerprint)
347
+
348
+ const renamed = { ...entry.facts, aclFeatures: ['demo_local.renamed'] }
349
+ expect(computeModuleTaxonomyFingerprint(renamed)).not.toBe(entry.taxonomyFingerprint)
350
+
351
+ const baseline = {
352
+ ...entry.facts,
353
+ apiRoutes: [{ path: '/demo/things', methods: ['GET', 'POST'], sourcePath: 'src/modules/demo_local/api/things/route.ts' }],
354
+ }
355
+ const structurallyReordered = {
356
+ ...entry.facts,
357
+ apiRoutes: [{ sourcePath: 'src/modules/demo_local/api/things/route.ts', methods: ['POST', 'GET'], path: '/demo/things' }],
358
+ }
359
+ expect(computeModuleTaxonomyFingerprint(structurallyReordered)).toBe(computeModuleTaxonomyFingerprint(baseline))
360
+ const methodRemoved = {
361
+ ...entry.facts,
362
+ apiRoutes: [{ path: '/demo/things', methods: ['GET'], sourcePath: 'src/modules/demo_local/api/things/route.ts' }],
363
+ }
364
+ expect(computeModuleTaxonomyFingerprint(methodRemoved)).not.toBe(computeModuleTaxonomyFingerprint(baseline))
365
+
366
+ const orderedTarget = entry.facts.overrideTargets.find((target) => target.path.length > 1)
367
+ expect(orderedTarget).toBeDefined()
368
+ const pathReordered = {
369
+ ...entry.facts,
370
+ overrideTargets: entry.facts.overrideTargets.map((target) => target === orderedTarget
371
+ ? { ...target, path: [...target.path].reverse() }
372
+ : target),
373
+ }
374
+ expect(computeModuleTaxonomyFingerprint(pathReordered)).not.toBe(entry.taxonomyFingerprint)
375
+
376
+ const movedBetweenSets = { ...entry.facts, aclFeatures: [], notifications: [...entry.facts.aclFeatures] }
377
+ expect(computeModuleTaxonomyFingerprint(movedBetweenSets)).not.toBe(entry.taxonomyFingerprint)
378
+ })
379
+ })
380
+
381
+ describe('local-reference projection of the canonical example module', () => {
382
+ const repoRoot = findRepoRoot()
383
+ const templateRoot = path.join(repoRoot, 'packages', 'create-app', 'template')
384
+ const authoringRoot = path.join(repoRoot, 'apps', 'mercato')
385
+
386
+ it('emits every example fact under the portable app-local root', () => {
387
+ const reference = discoverLocalReferenceModuleSource({ appRoot: templateRoot, moduleId: 'example' })
388
+ expect(reference).not.toBeNull()
389
+ const facts = extractModuleFacts({
390
+ moduleId: 'example',
391
+ moduleRoot: reference!.moduleRoot,
392
+ portableSourceRoot: reference!.portableSourceRoot,
393
+ sourceKind: reference!.sourceKind,
394
+ })
395
+ expect(facts.sourceRoot).toBe('src/modules/example')
396
+ expect(facts.sourceKind).toBe('local-reference')
397
+
398
+ const serialized = JSON.stringify(toModuleFactsJsonEntry(facts))
399
+ expect(serialized).not.toContain('node_modules')
400
+ for (const sourcePath of [...serialized.matchAll(/"(?:source)?[Pp]ath":"([^"]+\.tsx?)"/g)].map((m) => m[1])) {
401
+ expect(sourcePath.startsWith('src/modules/example/')).toBe(true)
402
+ expect(fs.existsSync(path.join(templateRoot, sourcePath))).toBe(true)
403
+ }
404
+ })
405
+
406
+ it('fingerprints the authoring and template example trees identically', () => {
407
+ const authoring = discoverLocalReferenceModuleSource({ appRoot: authoringRoot, moduleId: 'example' })
408
+ const template = discoverLocalReferenceModuleSource({ appRoot: templateRoot, moduleId: 'example' })
409
+ expect(authoring).not.toBeNull()
410
+ expect(template).not.toBeNull()
411
+ expect(computeModuleSourceFingerprint(authoring!.moduleRoot, 'src/modules/example'))
412
+ .toBe(computeModuleSourceFingerprint(template!.moduleRoot, 'src/modules/example'))
413
+ })
414
+ })
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { extractModuleFacts } from '../module-facts'
5
+
6
+ /**
7
+ * A search entity declared with a non-literal `entityId` must still reach module facts.
8
+ *
9
+ * `extractSearchEntities` accepted only a bare string literal, but modules reference the
10
+ * entity through `E.<module>.<entity>` (the generated entity-id registry, where
11
+ * `E[module][entity] === '<module>:<entity>'`) or through a same-file `const ENTITY_ID`.
12
+ * Every such entry was dropped with NO diagnostic, so a module shipping `search.ts` scored
13
+ * zero search facts while looking healthy — the same class of defect as the computed DI
14
+ * token. Repo-wide this moved the search family from 47 ids across 9 modules to 52 across 11.
15
+ *
16
+ * The second half of this file pins the other direction: an `entityId` the reader genuinely
17
+ * cannot know statically (an imported constant object) must NOT be guessed, and must produce
18
+ * a warning rather than a silent zero.
19
+ */
20
+
21
+ type Probe = {
22
+ searchEntities: string[]
23
+ warnings: string[]
24
+ searchRegistryIds: string[]
25
+ }
26
+
27
+ function probe(searchSource: string): Probe {
28
+ const moduleRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-search-entity-id-'))
29
+ try {
30
+ fs.writeFileSync(path.join(moduleRoot, 'search.ts'), searchSource)
31
+ const facts = extractModuleFacts({
32
+ moduleId: 'probe',
33
+ moduleRoot,
34
+ sourceRoot: 'src/modules/probe',
35
+ }) as unknown as {
36
+ searchEntities: string[]
37
+ warnings: string[]
38
+ extensionSurfaces?: {
39
+ contributions?: Array<{ kind: string; details?: { registry?: string; registryId?: string } }>
40
+ }
41
+ }
42
+ return {
43
+ searchEntities: facts.searchEntities,
44
+ warnings: facts.warnings.filter((warning) => warning.includes('searchConfig')),
45
+ searchRegistryIds: (facts.extensionSurfaces?.contributions ?? [])
46
+ .filter((contribution) => contribution.details?.registry === 'search')
47
+ .map((contribution) => contribution.details?.registryId ?? ''),
48
+ }
49
+ } finally {
50
+ fs.rmSync(moduleRoot, { recursive: true, force: true })
51
+ }
52
+ }
53
+
54
+ describe('module-facts search entityId resolution', () => {
55
+ it('resolves an entityId declared as a same-file const', () => {
56
+ const result = probe(`
57
+ const ENTITY_ID = 'probe:todo' as const
58
+ export const searchConfig = {
59
+ entities: [{ entityId: ENTITY_ID, enabled: true }],
60
+ }
61
+ `)
62
+
63
+ expect(result.searchEntities).toEqual(['probe:todo'])
64
+ expect(result.warnings).toEqual([])
65
+ // The whole chain matters: `searchEntities` feeds the specialized-registry contribution
66
+ // that downstream consumers actually read.
67
+ expect(result.searchRegistryIds).toEqual(['probe:todo'])
68
+ })
69
+
70
+ it('resolves an entityId read from the generated entity-id registry', () => {
71
+ const result = probe(`
72
+ import { E } from '#generated/entities.ids.generated'
73
+ export const searchConfig = {
74
+ entities: [
75
+ { entityId: E.probe.todo, enabled: true },
76
+ { entityId: E['probe']['note'], enabled: true },
77
+ ],
78
+ }
79
+ `)
80
+
81
+ expect(result.searchEntities).toEqual(['probe:todo', 'probe:note'])
82
+ expect(result.warnings).toEqual([])
83
+ })
84
+
85
+ it('still resolves a plain string literal and de-duplicates repeats', () => {
86
+ const result = probe(`
87
+ export const searchConfig = {
88
+ entities: [
89
+ { entityId: 'probe:todo' },
90
+ { entityId: 'probe:todo' },
91
+ { entityId: 'probe:note' },
92
+ ],
93
+ }
94
+ `)
95
+
96
+ expect(result.searchEntities).toEqual(['probe:todo', 'probe:note'])
97
+ expect(result.warnings).toEqual([])
98
+ })
99
+
100
+ it('reports rather than invents an entityId it cannot resolve statically', () => {
101
+ // `CHECKOUT_ENTITY_IDS` lives in another file, so a single-file reader cannot know the
102
+ // value. Emitting a guessed id would be worse than emitting none — but staying silent is
103
+ // what let this family read zero for years, so the entry must surface as a warning.
104
+ const result = probe(`
105
+ import { CHECKOUT_ENTITY_IDS } from './lib/constants'
106
+ import { CONSTANTS } from './lib/other'
107
+ export const searchConfig = {
108
+ entities: [
109
+ { entityId: CHECKOUT_ENTITY_IDS.link, enabled: true },
110
+ { entityId: CONSTANTS.entities.todo, enabled: true },
111
+ { entityId: 'probe:known' },
112
+ ],
113
+ }
114
+ `)
115
+
116
+ // Neither the two-segment access nor the three-segment access rooted at something other
117
+ // than the generated `E` registry may be folded into an id.
118
+ expect(result.searchEntities).toEqual(['probe:known'])
119
+ expect(result.searchRegistryIds).toEqual(['probe:known'])
120
+ expect(result.warnings).toHaveLength(2)
121
+ for (const warning of result.warnings) {
122
+ expect(warning).toContain('searchConfig entityId is not statically resolvable')
123
+ }
124
+ })
125
+
126
+ it('reports an entity that declares no entityId at all', () => {
127
+ const result = probe(`
128
+ export const searchConfig = {
129
+ entities: [{ enabled: true, priority: 5 }],
130
+ }
131
+ `)
132
+
133
+ expect(result.searchEntities).toEqual([])
134
+ expect(result.warnings).toHaveLength(1)
135
+ expect(result.warnings[0]).toContain('searchConfig entity declares no entityId')
136
+ })
137
+
138
+ it('does not follow a self-referential const into an infinite loop', () => {
139
+ const result = probe(`
140
+ const A = B
141
+ const B = A
142
+ export const searchConfig = {
143
+ entities: [{ entityId: A }],
144
+ }
145
+ `)
146
+
147
+ expect(result.searchEntities).toEqual([])
148
+ expect(result.warnings).toHaveLength(1)
149
+ expect(result.warnings[0]).toContain('searchConfig entityId is not statically resolvable')
150
+ })
151
+ })
@@ -1,7 +1,13 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
- import { extractModuleFacts, renderModuleFactsMarkdown } from '../module-facts'
4
+ import { extractModuleFacts, isExactSourceFilePath, renderModuleFactsMarkdown } from '../module-facts'
5
+
6
+ const MARKDOWN_LINK_TARGET = /\]\(\.\.\/\.\.\/\.\.\/([^)#]+)(?:#L\d+)?\)/g
7
+
8
+ function collectLinkTargets(markdown: string): string[] {
9
+ return [...markdown.matchAll(MARKDOWN_LINK_TARGET)].map((match) => match[1])
10
+ }
5
11
 
6
12
  function writeFixture(root: string, relativePath: string, source: string): void {
7
13
  const filePath = path.join(root, relativePath)
@@ -116,4 +122,39 @@ describe('module-facts source-linked extension surfaces', () => {
116
122
  '(../../../node_modules/@open-mercato/example/src/modules/facts/api/records/route.ts)',
117
123
  )
118
124
  })
125
+
126
+ it('links only exact module code files and never a directory', () => {
127
+ const facts = extractModuleFacts({
128
+ moduleId: 'facts',
129
+ moduleRoot,
130
+ sourcePackage: '@open-mercato/example',
131
+ sourceVersion: '1.2.3',
132
+ })
133
+ const markdown = renderModuleFactsMarkdown(facts)
134
+ const targets = collectLinkTargets(markdown)
135
+
136
+ expect(targets.length).toBeGreaterThan(0)
137
+ expect(targets.filter((target) => !isExactSourceFilePath(target))).toEqual([])
138
+ })
139
+
140
+ it('renders the directory-valued source root as plain text instead of a folder link', () => {
141
+ const facts = extractModuleFacts({
142
+ moduleId: 'facts',
143
+ moduleRoot,
144
+ sourcePackage: '@open-mercato/example',
145
+ sourceVersion: '1.2.3',
146
+ })
147
+ const markdown = renderModuleFactsMarkdown(facts)
148
+
149
+ expect(markdown).toContain('Source root: node_modules/@open-mercato/example/src/modules/facts\n')
150
+ expect(markdown).not.toContain('(../../../node_modules/@open-mercato/example/src/modules/facts)')
151
+ })
152
+
153
+ it('classifies exact files and directories consistently', () => {
154
+ expect(isExactSourceFilePath('packages/core/src/modules/customers/index.ts')).toBe(true)
155
+ expect(isExactSourceFilePath('packages/ui/src/backend/DataTable.tsx')).toBe(true)
156
+ expect(isExactSourceFilePath('node_modules/@open-mercato/core/dist/modules/customers/acl.js')).toBe(true)
157
+ expect(isExactSourceFilePath('packages/ui/src')).toBe(false)
158
+ expect(isExactSourceFilePath('node_modules/@open-mercato/core/src/modules/customers')).toBe(false)
159
+ })
119
160
  })