@open-mercato/cli 0.6.6-develop.6377.1.d26fed7324 → 0.6.6-develop.6382.1.4b9b9091ab

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 (31) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/agentic/guides/core.md +3 -38
  3. package/dist/agentic/guides/module-system.md +130 -0
  4. package/dist/agentic/shared/AGENTS.md.template +4 -12
  5. package/dist/lib/generators/index.js +2 -0
  6. package/dist/lib/generators/index.js.map +2 -2
  7. package/dist/lib/generators/module-facts-generate.js +52 -0
  8. package/dist/lib/generators/module-facts-generate.js.map +7 -0
  9. package/dist/lib/generators/module-facts.js +734 -0
  10. package/dist/lib/generators/module-facts.js.map +7 -0
  11. package/dist/mercato.js +3 -1
  12. package/dist/mercato.js.map +2 -2
  13. package/package.json +5 -5
  14. package/src/__tests__/mercato.test.ts +5 -0
  15. package/src/lib/generators/__tests__/module-facts.auth-source.test.ts +83 -0
  16. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +56 -0
  17. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +69 -0
  18. package/src/lib/generators/__tests__/module-facts.malformed.test.ts +76 -0
  19. package/src/lib/generators/index.ts +1 -0
  20. package/src/lib/generators/module-facts-generate.ts +68 -0
  21. package/src/lib/generators/module-facts.ts +958 -0
  22. package/src/mercato.ts +2 -0
  23. package/dist/agentic/guides/core.auth.md +0 -101
  24. package/dist/agentic/guides/core.catalog.md +0 -79
  25. package/dist/agentic/guides/core.currencies.md +0 -43
  26. package/dist/agentic/guides/core.customer_accounts.md +0 -129
  27. package/dist/agentic/guides/core.customers.md +0 -138
  28. package/dist/agentic/guides/core.data_sync.md +0 -107
  29. package/dist/agentic/guides/core.integrations.md +0 -113
  30. package/dist/agentic/guides/core.sales.md +0 -104
  31. package/dist/agentic/guides/core.workflows.md +0 -152
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.6-develop.6377.1.d26fed7324",
3
+ "version": "0.6.6-develop.6382.1.4b9b9091ab",
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.4",
61
61
  "@mikro-orm/migrations": "^7.1.4",
62
62
  "@mikro-orm/postgresql": "^7.1.4",
63
- "@open-mercato/queue": "0.6.6-develop.6377.1.d26fed7324",
64
- "@open-mercato/shared": "0.6.6-develop.6377.1.d26fed7324",
63
+ "@open-mercato/queue": "0.6.6-develop.6382.1.4b9b9091ab",
64
+ "@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -71,10 +71,10 @@
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "peerDependencies": {
74
- "@open-mercato/shared": "0.6.6-develop.6377.1.d26fed7324"
74
+ "@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab"
75
75
  },
76
76
  "devDependencies": {
77
- "@open-mercato/shared": "0.6.6-develop.6377.1.d26fed7324",
77
+ "@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab",
78
78
  "@types/jest": "^30.0.0",
79
79
  "jest": "^30.4.2",
80
80
  "ts-jest": "^29.4.11"
@@ -359,6 +359,7 @@ describe('init command failure output', () => {
359
359
  generateModuleDi: jest.fn().mockResolvedValue(undefined),
360
360
  generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
361
361
  generateOpenApi: jest.fn().mockResolvedValue(undefined),
362
+ generateModuleFacts: jest.fn().mockResolvedValue(undefined),
362
363
  }))
363
364
  jest.doMock('../lib/resolver', () => ({
364
365
  createResolver: () => ({
@@ -407,6 +408,7 @@ describe('init command failure output', () => {
407
408
  generateModuleDi: jest.fn().mockResolvedValue(undefined),
408
409
  generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
409
410
  generateOpenApi: jest.fn().mockResolvedValue(undefined),
411
+ generateModuleFacts: jest.fn().mockResolvedValue(undefined),
410
412
  }))
411
413
  jest.doMock('../lib/resolver', () => ({
412
414
  createResolver: () => ({
@@ -458,6 +460,7 @@ describe('init command failure output', () => {
458
460
  generateModuleDi: jest.fn().mockResolvedValue(undefined),
459
461
  generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
460
462
  generateOpenApi: jest.fn().mockResolvedValue(undefined),
463
+ generateModuleFacts: jest.fn().mockResolvedValue(undefined),
461
464
  }))
462
465
  jest.doMock('../lib/db', () => ({
463
466
  dbMigrate: jest.fn().mockResolvedValue(undefined),
@@ -566,6 +569,7 @@ describe('generate post-step structural cache purge', () => {
566
569
  generateModuleDi,
567
570
  generateModulePackageSources,
568
571
  generateOpenApi,
572
+ generateModuleFacts: jest.fn().mockResolvedValue(undefined),
569
573
  }))
570
574
  jest.doMock('../lib/resolver', () => ({
571
575
  createResolver: () => ({
@@ -615,6 +619,7 @@ describe('generate post-step structural cache purge', () => {
615
619
  generateModuleDi,
616
620
  generateModulePackageSources,
617
621
  generateOpenApi,
622
+ generateModuleFacts: jest.fn().mockResolvedValue(undefined),
618
623
  }))
619
624
  jest.doMock('../lib/resolver', () => ({
620
625
  createResolver: () => ({
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { extractModuleFacts } from '../module-facts'
4
+
5
+ function findCoreSrcRoot(): string {
6
+ let dir = __dirname
7
+ for (let depth = 0; depth < 10; depth += 1) {
8
+ const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
9
+ if (fs.existsSync(candidate)) return candidate
10
+ dir = path.dirname(dir)
11
+ }
12
+ throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
13
+ }
14
+
15
+ const REGISTRY_SOURCE = `
16
+ export const modules = [
17
+ {
18
+ id: 'customers',
19
+ apis: [
20
+ {
21
+ path: '/api/customers/people',
22
+ handlers: { GET: handlerGet, POST: handlerPost },
23
+ metadata: {
24
+ GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },
25
+ POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },
26
+ },
27
+ },
28
+ {
29
+ path: '/api/customers/people/:id',
30
+ handlers: { GET: handlerGet, PUT: handlerPut, DELETE: handlerDelete },
31
+ metadata: {
32
+ GET: { requireFeatures: ['customers.people.view'] },
33
+ PUT: { requireFeatures: ['customers.people.manage'] },
34
+ DELETE: { requireFeatures: ['customers.people.manage'] },
35
+ },
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ id: 'sales',
41
+ apis: [
42
+ {
43
+ path: '/api/sales/orders',
44
+ handlers: { GET: handlerGet },
45
+ metadata: { GET: { requireFeatures: ['sales.orders.view'] } },
46
+ },
47
+ ],
48
+ },
49
+ ]
50
+ `
51
+
52
+ describe('module-facts API route auth source (T2)', () => {
53
+ const coreSrcRoot = findCoreSrcRoot()
54
+ const facts = extractModuleFacts({ moduleId: 'customers', coreSrcRoot, registrySource: REGISTRY_SOURCE })
55
+
56
+ it('reads per-method auth from the registry apis[].metadata for the requested module', () => {
57
+ const list = facts.apiRoutes.find((route) => route.path === '/api/customers/people')
58
+ expect(list).toBeDefined()
59
+ expect(list?.methods).toEqual(['GET', 'POST'])
60
+ expect(list?.auth.GET).toEqual({ requireAuth: true, requireFeatures: ['customers.people.view'] })
61
+ expect(list?.auth.POST).toEqual({ requireAuth: true, requireFeatures: ['customers.people.manage'] })
62
+
63
+ const detail = facts.apiRoutes.find((route) => route.path === '/api/customers/people/:id')
64
+ expect(detail?.methods).toEqual(['GET', 'PUT', 'DELETE'])
65
+ expect(detail?.auth.PUT).toEqual({ requireFeatures: ['customers.people.manage'] })
66
+ expect(detail?.auth.DELETE).toEqual({ requireFeatures: ['customers.people.manage'] })
67
+ })
68
+
69
+ it('scopes routes to the requested module and ignores other modules in the registry', () => {
70
+ expect(facts.apiRoutes.some((route) => route.path.startsWith('/api/sales/'))).toBe(false)
71
+ expect(facts.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(false)
72
+ })
73
+
74
+ it('omits API routes (and warns) when a registry path is provided but the file is missing', () => {
75
+ const missing = extractModuleFacts({
76
+ moduleId: 'customers',
77
+ coreSrcRoot,
78
+ registryPath: path.join(coreSrcRoot, '__does_not_exist__', 'modules.runtime.generated.ts'),
79
+ })
80
+ expect(missing.apiRoutes).toEqual([])
81
+ expect(missing.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(true)
82
+ })
83
+ })
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { extractAllModuleFacts, MODULE_FACTS_ALLOWLIST } from '../module-facts'
4
+
5
+ function findCoreSrcRoot(): string {
6
+ let dir = __dirname
7
+ for (let depth = 0; depth < 10; depth += 1) {
8
+ const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
9
+ if (fs.existsSync(candidate)) return candidate
10
+ dir = path.dirname(dir)
11
+ }
12
+ throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
13
+ }
14
+
15
+ function isUnique(values: string[]): boolean {
16
+ return values.length === new Set(values).size
17
+ }
18
+
19
+ describe('module-facts BC resolve guard (T3)', () => {
20
+ const coreSrcRoot = findCoreSrcRoot()
21
+ const { factsByModule } = extractAllModuleFacts({ coreSrcRoot })
22
+
23
+ it('emits facts for every allowlisted module', () => {
24
+ expect(Object.keys(factsByModule).sort()).toEqual([...MODULE_FACTS_ALLOWLIST].sort())
25
+ })
26
+
27
+ for (const moduleId of MODULE_FACTS_ALLOWLIST) {
28
+ describe(`${moduleId}`, () => {
29
+ it('namespaces entity / event / acl / notification ids under the module and keeps them unique', () => {
30
+ const facts = factsByModule[moduleId]
31
+ const entityIds = facts.entities.map((entity) => entity.id)
32
+
33
+ expect(entityIds.every((id) => id.startsWith(`${moduleId}:`))).toBe(true)
34
+ expect(isUnique(entityIds)).toBe(true)
35
+ expect(facts.events.every((event) => event.id.startsWith(`${moduleId}.`))).toBe(true)
36
+ expect(isUnique(facts.events.map((event) => event.id))).toBe(true)
37
+ expect(facts.aclFeatures.every((feature) => feature.startsWith(`${moduleId}.`))).toBe(true)
38
+ expect(isUnique(facts.aclFeatures)).toBe(true)
39
+ expect(facts.notifications.every((notification) => notification.startsWith(`${moduleId}.`))).toBe(true)
40
+ })
41
+
42
+ it('resolves search and host-token references against the module entity set', () => {
43
+ const facts = factsByModule[moduleId]
44
+ const entityIds = new Set(facts.entities.map((entity) => entity.id))
45
+
46
+ for (const searchEntity of facts.searchEntities) {
47
+ expect(entityIds.has(searchEntity)).toBe(true)
48
+ }
49
+ for (const hostEntityId of facts.hostTokens.entityIds) {
50
+ expect(entityIds.has(hostEntityId)).toBe(true)
51
+ expect(hostEntityId.endsWith('_entity')).toBe(true)
52
+ }
53
+ })
54
+ })
55
+ }
56
+ })
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { extractModuleFacts } from '../module-facts'
4
+
5
+ function findCoreSrcRoot(): string {
6
+ let dir = __dirname
7
+ for (let depth = 0; depth < 10; depth += 1) {
8
+ const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
9
+ if (fs.existsSync(candidate)) return candidate
10
+ dir = path.dirname(dir)
11
+ }
12
+ throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
13
+ }
14
+
15
+ describe('module-facts customers fixture (T1 anti-drift guard)', () => {
16
+ const coreSrcRoot = findCoreSrcRoot()
17
+ const facts = extractModuleFacts({ moduleId: 'customers', coreSrcRoot })
18
+
19
+ it('locks the source-derived customers entity surface in colon-form ids', () => {
20
+ expect(facts.module).toBe('customers')
21
+ expect(facts.entities).toHaveLength(25)
22
+ expect(facts.entities.every((entity) => entity.id.startsWith('customers:'))).toBe(true)
23
+ expect(facts.entities[0]).toMatchObject({ id: 'customers:customer_entity' })
24
+ for (const entity of facts.entities) {
25
+ expect(entity.class.length).toBeGreaterThan(0)
26
+ expect(entity.table.length).toBeGreaterThan(0)
27
+ }
28
+ })
29
+
30
+ it('locks customers events, acl, search, and notification surfaces', () => {
31
+ expect(facts.events).toHaveLength(49)
32
+ expect(facts.aclFeatures).toHaveLength(21)
33
+ expect(facts.searchEntities).toEqual([
34
+ 'customers:customer_person_profile',
35
+ 'customers:customer_company_profile',
36
+ 'customers:customer_comment',
37
+ 'customers:customer_deal',
38
+ 'customers:customer_activity',
39
+ 'customers:customer_todo_link',
40
+ ])
41
+ expect(facts.notifications).toEqual(['customers.deal.won', 'customers.deal.lost'])
42
+ })
43
+
44
+ it('locks the real cli commands and host table ids, not the abbreviated spec example', () => {
45
+ expect(facts.cli).toEqual([
46
+ 'seed-dictionaries',
47
+ 'seed-examples',
48
+ 'seed-stresstest',
49
+ 'interactions:backfill',
50
+ ])
51
+ expect(facts.hostTokens.tableIds).toEqual([
52
+ 'customers.companies.list',
53
+ 'customers.deals.list',
54
+ 'customers.people.list',
55
+ ])
56
+ expect(facts.hostTokens.entityIds).toEqual(['customers:customer_entity'])
57
+ expect(facts.diTokens).toEqual([])
58
+ })
59
+
60
+ it('omits API route auth and warns when no module registry is provided', () => {
61
+ expect(facts.apiRoutes).toEqual([])
62
+ expect(facts.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(true)
63
+ })
64
+
65
+ it('locks the module metadata surface extracted from index.ts', () => {
66
+ expect(facts.title).toBe('Customer Relationship Management')
67
+ expect(facts.description).toBe('Core CRM capabilities for people, companies, deals, and activities.')
68
+ })
69
+ })
@@ -0,0 +1,76 @@
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
+ let tmpRoot: string
7
+
8
+ function writeModuleFile(moduleId: string, relativePath: string, content: string): void {
9
+ const fullPath = path.join(tmpRoot, moduleId, relativePath)
10
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true })
11
+ fs.writeFileSync(fullPath, content)
12
+ }
13
+
14
+ beforeEach(() => {
15
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-facts-malformed-'))
16
+ })
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tmpRoot, { recursive: true, force: true })
20
+ })
21
+
22
+ describe('module-facts malformed source handling (T4)', () => {
23
+ it('returns empty sections plus warnings (never throws) for malformed convention files', () => {
24
+ writeModuleFile('broken', 'data/entities.ts', 'export const notAnEntity = 1\nexport class LooseClass {}\n')
25
+ writeModuleFile('broken', 'search.ts', 'export const unrelatedConfig = { entities: [] }\n')
26
+ writeModuleFile('broken', 'notifications.ts', 'export const notifications = "not-an-array"\n')
27
+ writeModuleFile('broken', 'cli.ts', 'export const commands = []\n')
28
+
29
+ const run = (): ReturnType<typeof extractModuleFacts> =>
30
+ extractModuleFacts({ moduleId: 'broken', coreSrcRoot: tmpRoot })
31
+ expect(run).not.toThrow()
32
+
33
+ const facts = run()
34
+ expect(facts.entities).toEqual([])
35
+ expect(facts.events).toEqual([])
36
+ expect(facts.aclFeatures).toEqual([])
37
+ expect(facts.searchEntities).toEqual([])
38
+ expect(facts.notifications).toEqual([])
39
+ expect(facts.cli).toEqual([])
40
+ expect(facts.apiRoutes).toEqual([])
41
+
42
+ const warnings = facts.warnings.join('\n')
43
+ expect(warnings).toContain('search.ts present but no searchConfig')
44
+ expect(warnings).toContain('notifications.ts present but no notificationTypes array')
45
+ expect(warnings).toContain('cli.ts present but no default export')
46
+ expect(warnings).toContain('module registry unavailable')
47
+ })
48
+
49
+ it('survives a syntactically broken source file without throwing', () => {
50
+ writeModuleFile('broken', 'search.ts', 'export const searchConfig = { entities: [ this is not valid typescript\n')
51
+
52
+ const run = (): ReturnType<typeof extractModuleFacts> =>
53
+ extractModuleFacts({ moduleId: 'broken', coreSrcRoot: tmpRoot })
54
+ expect(run).not.toThrow()
55
+ expect(run().searchEntities).toEqual([])
56
+ })
57
+
58
+ it('returns all-empty facts (never throws) for a module with no convention files', () => {
59
+ fs.mkdirSync(path.join(tmpRoot, 'empty'), { recursive: true })
60
+
61
+ const run = (): ReturnType<typeof extractModuleFacts> =>
62
+ extractModuleFacts({ moduleId: 'empty', coreSrcRoot: tmpRoot })
63
+ expect(run).not.toThrow()
64
+
65
+ expect(run()).toMatchObject({
66
+ module: 'empty',
67
+ entities: [],
68
+ events: [],
69
+ aclFeatures: [],
70
+ searchEntities: [],
71
+ notifications: [],
72
+ cli: [],
73
+ diTokens: [],
74
+ })
75
+ })
76
+ })
@@ -4,3 +4,4 @@ export { generateModuleEntities, type ModuleEntitiesOptions } from './module-ent
4
4
  export { generateModuleDi, type ModuleDiOptions } from './module-di'
5
5
  export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
6
6
  export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
7
+ export { generateModuleFacts, type ModuleFactsOptions } from './module-facts-generate'
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { PackageResolver } from '../resolver'
4
+ import {
5
+ createGeneratorResult,
6
+ ensureDir,
7
+ logGenerationResult,
8
+ type GeneratorResult,
9
+ } from '../utils'
10
+ import { extractAllModuleFacts, renderModuleFactsJson } from './module-facts'
11
+
12
+ export interface ModuleFactsOptions {
13
+ resolver: PackageResolver
14
+ quiet?: boolean
15
+ }
16
+
17
+ function readCoreVersion(coreSrcRoot: string): string | null {
18
+ const corePackageJsonPath = path.resolve(coreSrcRoot, '..', '..', 'package.json')
19
+ if (!fs.existsSync(corePackageJsonPath)) return null
20
+ try {
21
+ const parsed = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8')) as { version?: unknown }
22
+ return typeof parsed.version === 'string' ? parsed.version : null
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Emits the versioned `apps/mercato/src/module-facts.generated.json` artifact from
30
+ * monorepo core module sources. Must run AFTER `generateModuleRegistry` because
31
+ * per-route API auth is read from the generated `modules.runtime.generated.ts`
32
+ * registry (`apis[].metadata`). Registry-derived warnings are non-fatal.
33
+ */
34
+ export async function generateModuleFacts(options: ModuleFactsOptions): Promise<GeneratorResult> {
35
+ const { resolver, quiet } = options
36
+ const result = createGeneratorResult()
37
+
38
+ const rootDir = resolver.getRootDir()
39
+ const coreSrcRoot = path.join(rootDir, 'packages', 'core', 'src', 'modules')
40
+ if (!fs.existsSync(coreSrcRoot)) {
41
+ if (!quiet) {
42
+ console.warn(`[module-facts] core module sources not found at ${coreSrcRoot}; skipping module-facts generation`)
43
+ }
44
+ return result
45
+ }
46
+
47
+ const registryPath = path.join(resolver.getOutputDir(), 'modules.runtime.generated.ts')
48
+ const coreVersion = readCoreVersion(coreSrcRoot)
49
+
50
+ const { factsByModule, warnings } = extractAllModuleFacts({ coreSrcRoot, registryPath, coreVersion })
51
+ for (const warning of warnings) {
52
+ console.warn(warning)
53
+ }
54
+
55
+ const content = renderModuleFactsJson(factsByModule)
56
+ const outFile = path.join(resolver.getAppDir(), 'src', 'module-facts.generated.json')
57
+ const existing = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : null
58
+ if (existing === content) {
59
+ result.filesUnchanged.push(outFile)
60
+ return result
61
+ }
62
+
63
+ ensureDir(outFile)
64
+ fs.writeFileSync(outFile, content)
65
+ result.filesWritten.push(outFile)
66
+ logGenerationResult(path.relative(process.cwd(), outFile), true)
67
+ return result
68
+ }