@open-mercato/cli 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
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.
- package/.turbo/turbo-build.log +1 -1
- package/build.mjs +1 -1
- package/dist/agentic/guides/module-facts.json +90266 -20749
- package/dist/agentic/guides/modules/ai_assistant.md +170 -60
- package/dist/agentic/guides/modules/api_docs.md +26 -1
- package/dist/agentic/guides/modules/api_keys.md +57 -17
- package/dist/agentic/guides/modules/attachments.md +72 -19
- package/dist/agentic/guides/modules/audit_logs.md +95 -29
- package/dist/agentic/guides/modules/auth.md +218 -83
- package/dist/agentic/guides/modules/business_rules.md +108 -34
- package/dist/agentic/guides/modules/catalog.md +386 -157
- package/dist/agentic/guides/modules/channel_gmail.md +55 -6
- package/dist/agentic/guides/modules/channel_imap.md +55 -6
- package/dist/agentic/guides/modules/checkout.md +254 -129
- package/dist/agentic/guides/modules/communication_channels.md +280 -109
- package/dist/agentic/guides/modules/configs.md +68 -11
- package/dist/agentic/guides/modules/content.md +26 -1
- package/dist/agentic/guides/modules/currencies.md +109 -44
- package/dist/agentic/guides/modules/customer_accounts.md +350 -156
- package/dist/agentic/guides/modules/customers.md +642 -294
- package/dist/agentic/guides/modules/dashboards.md +122 -28
- package/dist/agentic/guides/modules/data_sync.md +117 -37
- package/dist/agentic/guides/modules/design_system.md +36 -2
- package/dist/agentic/guides/modules/dictionaries.md +78 -20
- package/dist/agentic/guides/modules/directory.md +110 -45
- package/dist/agentic/guides/modules/entities.md +95 -38
- package/dist/agentic/guides/modules/events.md +31 -1
- package/dist/agentic/guides/modules/feature_toggles.md +79 -12
- package/dist/agentic/guides/modules/gateway_stripe.md +61 -6
- package/dist/agentic/guides/modules/generators.md +17 -1
- package/dist/agentic/guides/modules/inbox_ops.md +171 -60
- package/dist/agentic/guides/modules/integrations.md +116 -30
- package/dist/agentic/guides/modules/messages.md +187 -66
- package/dist/agentic/guides/modules/notifications.md +64 -9
- package/dist/agentic/guides/modules/onboarding.md +47 -8
- package/dist/agentic/guides/modules/payment_gateways.md +133 -48
- package/dist/agentic/guides/modules/perspectives.md +50 -12
- package/dist/agentic/guides/modules/planner.md +102 -32
- package/dist/agentic/guides/modules/portal.md +58 -15
- package/dist/agentic/guides/modules/progress.md +78 -30
- package/dist/agentic/guides/modules/query_index.md +115 -53
- package/dist/agentic/guides/modules/record_locks.md +178 -59
- package/dist/agentic/guides/modules/resources.md +155 -78
- package/dist/agentic/guides/modules/sales.md +620 -257
- package/dist/agentic/guides/modules/scheduler.md +95 -19
- package/dist/agentic/guides/modules/search.md +124 -20
- package/dist/agentic/guides/modules/security.md +251 -105
- package/dist/agentic/guides/modules/shipping_carriers.md +106 -37
- package/dist/agentic/guides/modules/sso.md +121 -49
- package/dist/agentic/guides/modules/staff.md +314 -126
- package/dist/agentic/guides/modules/storage_s3.md +55 -6
- package/dist/agentic/guides/modules/sync_akeneo.md +54 -4
- package/dist/agentic/guides/modules/sync_excel.md +49 -11
- package/dist/agentic/guides/modules/system_status_overlays.md +33 -5
- package/dist/agentic/guides/modules/translations.md +76 -21
- package/dist/agentic/guides/modules/webhooks.md +166 -82
- package/dist/agentic/guides/modules/wms.md +383 -223
- package/dist/agentic/guides/modules/workflows.md +248 -112
- package/dist/agentic/guides/upstream/AGENTS.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/AGENTS.md.template +6 -5
- package/dist/agentic/shared/ai/harness/cases.json +3 -0
- package/dist/agentic/shared/ai/lessons/_template.md +16 -0
- package/dist/agentic/shared/ai/lessons.md +26 -3
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +7 -6
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +4 -2
- package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
- package/dist/lib/agentic-setup.js +2 -2
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +652 -24
- package/dist/lib/generators/module-extension-facts.js.map +3 -3
- package/dist/lib/generators/module-fact-sources.js +70 -0
- package/dist/lib/generators/module-fact-sources.js.map +7 -0
- package/dist/lib/generators/module-facts.js +1001 -26
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-override-targets.js +688 -0
- package/dist/lib/generators/module-override-targets.js.map +7 -0
- package/dist/lib/testing/integration.js +5 -0
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +6 -0
- package/src/lib/agentic-setup.ts +6 -2
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +4 -2
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +37 -3
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +304 -0
- package/src/lib/generators/module-extension-facts.ts +923 -27
- package/src/lib/generators/module-fact-sources.ts +108 -0
- package/src/lib/generators/module-facts.ts +1360 -25
- package/src/lib/generators/module-override-targets.ts +863 -0
- package/src/lib/testing/__tests__/integration.test.ts +4 -0
- package/src/lib/testing/integration.ts +5 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyApiOverridesToManifests,
|
|
3
|
+
applyDiOverrides,
|
|
4
|
+
applyDiOverridesToContainer,
|
|
5
|
+
applyInjectionWidgetOverridesToEntries,
|
|
6
|
+
applyPageGuardOverridesToEntries,
|
|
7
|
+
applyPageOverridesToManifests,
|
|
8
|
+
applyWorkerOverridesToDescriptors,
|
|
9
|
+
resetModuleContractOverridesForTests,
|
|
10
|
+
} from '@open-mercato/shared/modules/overrides'
|
|
11
|
+
import type { ModuleFactsJsonEntry } from '../module-facts'
|
|
12
|
+
import { collectModuleOverrideTargets, type ModuleOverrideTarget } from '../module-override-targets'
|
|
13
|
+
|
|
14
|
+
const SOURCE_ROOT = 'node_modules/@open-mercato/core/src/modules/demo'
|
|
15
|
+
|
|
16
|
+
function facts(): ModuleFactsJsonEntry {
|
|
17
|
+
return {
|
|
18
|
+
title: null, description: null, coreVersion: null, sourcePackage: '@open-mercato/core',
|
|
19
|
+
sourceVersion: null, sourceRoot: SOURCE_ROOT,
|
|
20
|
+
entities: [], events: [], aclFeatures: ['demo.view'],
|
|
21
|
+
apiRoutes: [{ path: '/demo/items', methods: ['GET', 'POST'], auth: {}, sourcePath: `${SOURCE_ROOT}/api/items/route.ts` }],
|
|
22
|
+
diTokens: ['demoService'], searchEntities: [], hostTokens: { entityIds: [], tableIds: [] },
|
|
23
|
+
notifications: [], cli: [], backendPages: [{ path: '/backend/demo/list', sourcePath: `${SOURCE_ROOT}/backend/demo/list/page.tsx` }],
|
|
24
|
+
frontendPages: [{ path: '/demo-portal', sourcePath: `${SOURCE_ROOT}/frontend/demo-portal/page.tsx` }],
|
|
25
|
+
cliCommands: [], aiTools: [], aiAgents: [],
|
|
26
|
+
factSources: [{ kind: 'acl-feature', id: 'demo.view', source: { sourcePath: `${SOURCE_ROOT}/acl.ts` } }],
|
|
27
|
+
ownedContracts: {
|
|
28
|
+
worker: [{ kind: 'worker', id: 'demo.worker', source: { sourcePath: `${SOURCE_ROOT}/workers/demo.ts`, line: 3 }, metadata: { queue: 'demo', concurrency: 1 } }],
|
|
29
|
+
'page-middleware': [{ kind: 'page-middleware', id: 'demo.guard', source: { sourcePath: `${SOURCE_ROOT}/backend/middleware.ts`, line: 2 }, metadata: { surface: 'backend' } }],
|
|
30
|
+
'di-registration': [{ kind: 'di-registration', id: 'demoService', source: { sourcePath: `${SOURCE_ROOT}/di.ts`, line: 5 }, metadata: { registrationKind: 'class' } }],
|
|
31
|
+
},
|
|
32
|
+
extensionSurfaces: {
|
|
33
|
+
hosts: [], unresolved: [],
|
|
34
|
+
contributions: [
|
|
35
|
+
{ id: 'demo.widget@profile:demo:slot', kind: 'widget', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/widgets/injection-table.ts`, symbol: 'injectionTable' }, details: { payload: 'render', registryKey: 'demo.widget', executionGuard: 'host' } },
|
|
36
|
+
{ id: 'demo.mutation-guard', kind: 'mutation-guard', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/data/guards.ts`, symbol: 'demo.mutation-guard' }, details: { entityId: 'demo.entity', operations: ['create'], capabilities: ['block'], optimisticLock: 'preserved' } },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TARGETS = collectModuleOverrideTargets(facts()).overrideTargets
|
|
43
|
+
function key(domain: string, pathTail: string): string {
|
|
44
|
+
const target = TARGETS.find((t: ModuleOverrideTarget) => t.domain === domain && t.path[t.path.length - 1] === pathTail)
|
|
45
|
+
if (!target || target.key === undefined) throw new Error(`[internal] no keyed ${domain} target for ${pathTail}`)
|
|
46
|
+
return target.key
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
afterEach(() => resetModuleContractOverridesForTests())
|
|
50
|
+
|
|
51
|
+
describe('runtime resolver round-trip — positive', () => {
|
|
52
|
+
it('selects the intended API route method by the generated public key', () => {
|
|
53
|
+
const getKey = key('routes', 'GET /api/demo/items')
|
|
54
|
+
const routes = [{ moduleId: 'demo', path: '/demo/items', methods: ['GET', 'POST'], load: async () => ({}) }]
|
|
55
|
+
const result = applyApiOverridesToManifests(routes as never, { [getKey]: null })
|
|
56
|
+
expect(result).toHaveLength(1)
|
|
57
|
+
expect(result[0].methods).toEqual(['POST'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('selects the intended backend and frontend page by the generated application-route key', () => {
|
|
61
|
+
const backendKey = key('routes', 'backend:/backend/demo/list')
|
|
62
|
+
const frontendKey = key('routes', 'frontend:/demo-portal')
|
|
63
|
+
const backend = applyPageOverridesToManifests([{ moduleId: 'demo', path: '/backend/demo/list', load: async () => ({}) }] as never, { [backendKey]: null }, 'backend')
|
|
64
|
+
const frontend = applyPageOverridesToManifests([{ moduleId: 'demo', path: '/demo-portal', load: async () => ({}) }] as never, { [frontendKey]: null }, 'frontend')
|
|
65
|
+
expect(backend).toHaveLength(0)
|
|
66
|
+
expect(frontend).toHaveLength(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('selects the intended worker / injection widget / page middleware by exact key', () => {
|
|
70
|
+
const workers = applyWorkerOverridesToDescriptors(
|
|
71
|
+
[{ id: 'demo.worker', queue: 'demo', concurrency: 1, handler: () => undefined }],
|
|
72
|
+
{ [key('workers', 'demo.worker')]: null },
|
|
73
|
+
)
|
|
74
|
+
expect(workers).toHaveLength(0)
|
|
75
|
+
|
|
76
|
+
const widgets = applyInjectionWidgetOverridesToEntries(
|
|
77
|
+
[{ moduleId: 'demo', key: 'demo.widget', loader: async () => ({}) }] as never,
|
|
78
|
+
{ [key('widgets', 'demo.widget')]: null },
|
|
79
|
+
)
|
|
80
|
+
expect(widgets).toHaveLength(0)
|
|
81
|
+
|
|
82
|
+
const guards = applyPageGuardOverridesToEntries(
|
|
83
|
+
[{ moduleId: 'demo', middleware: [{ id: 'demo.guard', run: async () => undefined }] }] as never,
|
|
84
|
+
{ [key('guards', 'demo.guard')]: null },
|
|
85
|
+
)
|
|
86
|
+
expect(guards[0].middleware).toHaveLength(0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('unregisters the exact DI token via the generated key', () => {
|
|
90
|
+
const unregistered: string[] = []
|
|
91
|
+
const container = {
|
|
92
|
+
register: () => container,
|
|
93
|
+
unregister: (name: string) => { unregistered.push(name); return container },
|
|
94
|
+
registrations: { demoService: {} as unknown },
|
|
95
|
+
}
|
|
96
|
+
applyDiOverrides({ [key('di', 'demoService')]: null })
|
|
97
|
+
applyDiOverridesToContainer(container)
|
|
98
|
+
expect(unregistered).toEqual(['demoService'])
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('runtime resolver round-trip — negative (malformed variants do not select)', () => {
|
|
103
|
+
it('a lowercased / method-less API key does not select the manifest entry', () => {
|
|
104
|
+
const getKey = key('routes', 'GET /api/demo/items')
|
|
105
|
+
expect(getKey).toMatch(/^(GET|POST|PUT|PATCH|DELETE) \/api\//)
|
|
106
|
+
const routes = [{ moduleId: 'demo', path: '/demo/items', methods: ['GET', 'POST'], load: async () => ({}) }]
|
|
107
|
+
const lowercased = applyApiOverridesToManifests(routes as never, { 'get /api/demo/items': null })
|
|
108
|
+
expect(lowercased[0].methods).toEqual(['GET', 'POST'])
|
|
109
|
+
const methodless = applyApiOverridesToManifests(routes as never, { '/api/demo/items': null })
|
|
110
|
+
expect(methodless[0].methods).toEqual(['GET', 'POST'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('a UI filesystem path does not select a page; the generated application route does', () => {
|
|
114
|
+
const filesystemPath = `${SOURCE_ROOT}/backend/demo/list/page.tsx`
|
|
115
|
+
const wrong = applyPageOverridesToManifests([{ moduleId: 'demo', path: '/backend/demo/list', load: async () => ({}) }] as never, { [filesystemPath]: null }, 'backend')
|
|
116
|
+
expect(wrong).toHaveLength(1)
|
|
117
|
+
const backendKey = key('routes', 'backend:/backend/demo/list')
|
|
118
|
+
expect(backendKey).not.toContain('.tsx')
|
|
119
|
+
expect(backendKey.startsWith('backend:')).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('a flattened nested widget key does not select the injection entry', () => {
|
|
123
|
+
const flattened = applyInjectionWidgetOverridesToEntries(
|
|
124
|
+
[{ moduleId: 'demo', key: 'demo.widget', loader: async () => ({}) }] as never,
|
|
125
|
+
{ 'widgets.injection.demo.widget': null },
|
|
126
|
+
)
|
|
127
|
+
expect(flattened).toHaveLength(1)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('a mutation-guard id under page guards does not drop the page middleware', () => {
|
|
131
|
+
const guards = applyPageGuardOverridesToEntries(
|
|
132
|
+
[{ moduleId: 'demo', middleware: [{ id: 'demo.guard', run: async () => undefined }] }] as never,
|
|
133
|
+
{ 'demo.mutation-guard': null },
|
|
134
|
+
)
|
|
135
|
+
expect(guards[0].middleware).toHaveLength(1)
|
|
136
|
+
// the mutation guard is never emitted as a guards target
|
|
137
|
+
expect(TARGETS.some((t) => t.domain === 'guards' && t.key === 'demo.mutation-guard')).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('an unknown DI token is never emitted; emitted tokens correspond to real registrations', () => {
|
|
141
|
+
expect(TARGETS.some((t) => t.domain === 'di' && t.key === 'nope')).toBe(false)
|
|
142
|
+
expect(TARGETS.some((t) => t.domain === 'di' && t.key === 'demoService')).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('framework-only nav.groupOrder is never attached to a module target', () => {
|
|
146
|
+
expect(TARGETS.some((t) => t.domain === 'nav')).toBe(false)
|
|
147
|
+
expect(TARGETS.some((t) => JSON.stringify(t.path) === JSON.stringify(['nav', 'groupOrder']))).toBe(false)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { ModuleFactsJsonEntry } from '../module-facts'
|
|
2
|
+
import {
|
|
3
|
+
ALL_OVERRIDE_DOMAINS,
|
|
4
|
+
MODULE_OVERRIDE_TARGET_ADAPTERS,
|
|
5
|
+
collectModuleOverrideTargets,
|
|
6
|
+
computeOverrideTargetId,
|
|
7
|
+
} from '../module-override-targets'
|
|
8
|
+
|
|
9
|
+
const SOURCE_ROOT = 'node_modules/@open-mercato/core/src/modules/demo'
|
|
10
|
+
const MODULE_ID = 'demo'
|
|
11
|
+
|
|
12
|
+
function baseFacts(overrides: Partial<ModuleFactsJsonEntry> = {}): ModuleFactsJsonEntry {
|
|
13
|
+
return {
|
|
14
|
+
title: 'Demo',
|
|
15
|
+
description: null,
|
|
16
|
+
coreVersion: null,
|
|
17
|
+
sourcePackage: '@open-mercato/core',
|
|
18
|
+
sourceVersion: null,
|
|
19
|
+
sourceRoot: SOURCE_ROOT,
|
|
20
|
+
entities: [],
|
|
21
|
+
events: [],
|
|
22
|
+
aclFeatures: ['demo.view'],
|
|
23
|
+
apiRoutes: [
|
|
24
|
+
{ path: '/demo/items', methods: ['GET', 'POST'], auth: {}, sourcePath: `${SOURCE_ROOT}/api/items/route.ts` },
|
|
25
|
+
],
|
|
26
|
+
diTokens: ['demoService'],
|
|
27
|
+
searchEntities: [],
|
|
28
|
+
hostTokens: { entityIds: [], tableIds: [] },
|
|
29
|
+
notifications: ['demo.alert'],
|
|
30
|
+
cli: ['demo:seed'],
|
|
31
|
+
backendPages: [{ path: '/backend/demo/list', sourcePath: `${SOURCE_ROOT}/backend/demo/list/page.tsx` }],
|
|
32
|
+
frontendPages: [{ path: '/demo-portal', sourcePath: `${SOURCE_ROOT}/frontend/demo-portal/page.tsx` }],
|
|
33
|
+
cliCommands: [{ command: 'demo:seed', sourcePath: `${SOURCE_ROOT}/cli.ts` }],
|
|
34
|
+
aiTools: [{ name: 'demo.tool', sourcePath: `${SOURCE_ROOT}/ai-tools.ts` }],
|
|
35
|
+
aiAgents: [{ id: 'demo.agent', sourcePath: `${SOURCE_ROOT}/ai-agents.ts` }],
|
|
36
|
+
factSources: [
|
|
37
|
+
{ kind: 'acl-feature', id: 'demo.view', source: { sourcePath: `${SOURCE_ROOT}/acl.ts` } },
|
|
38
|
+
{ kind: 'notification', id: 'demo.alert', source: { sourcePath: `${SOURCE_ROOT}/notifications.ts` } },
|
|
39
|
+
],
|
|
40
|
+
ownedContracts: {
|
|
41
|
+
worker: [{ kind: 'worker', id: 'demo.worker', source: { sourcePath: `${SOURCE_ROOT}/workers/demo.ts`, line: 3 } }],
|
|
42
|
+
'page-middleware': [{ kind: 'page-middleware', id: 'demo.guard', source: { sourcePath: `${SOURCE_ROOT}/backend/middleware.ts`, line: 2 }, metadata: { surface: 'backend' } }],
|
|
43
|
+
setup: [{ kind: 'setup', id: 'demo:setup', source: { sourcePath: `${SOURCE_ROOT}/setup.ts`, line: 1 }, metadata: { hooks: ['seedDefaults', 'seedExamples', 'onTenantCreated', 'installStuff'], roles: ['admin'] } }],
|
|
44
|
+
'di-registration': [{ kind: 'di-registration', id: 'demoService', source: { sourcePath: `${SOURCE_ROOT}/di.ts`, line: 5 }, metadata: { registrationKind: 'class', lifetime: 'scoped' } }],
|
|
45
|
+
encryption: [{ kind: 'encryption', id: 'demo:secret', source: { sourcePath: `${SOURCE_ROOT}/encryption.ts`, line: 2 }, metadata: { fields: ['token'] } }],
|
|
46
|
+
'ai-extension': [
|
|
47
|
+
{ kind: 'ai-extension', id: 'agent-extension:other.agent', source: { sourcePath: `${SOURCE_ROOT}/ai-agents.extensions.ts`, line: 1 }, metadata: { target: 'agent', mode: 'extension', targetAgentId: 'other.agent' } },
|
|
48
|
+
{ kind: 'ai-extension', id: 'agent-override:other.assistant', source: { sourcePath: `${SOURCE_ROOT}/ai-agents.ts`, line: 7 }, metadata: { target: 'agent', mode: 'override', overrideKey: 'other.assistant' } },
|
|
49
|
+
{ kind: 'ai-extension', id: 'tool-override:other.tool', source: { sourcePath: `${SOURCE_ROOT}/ai-tools.ts`, line: 9 }, metadata: { target: 'tool', mode: 'override', overrideKey: 'other.tool' } },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
extensionSurfaces: {
|
|
53
|
+
hosts: [],
|
|
54
|
+
unresolved: [],
|
|
55
|
+
contributions: [
|
|
56
|
+
{ id: 'demo:sub', kind: 'subscriber', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/subscribers/demo.ts`, symbol: 'metadata' }, details: { event: 'demo.e', subscriberId: 'demo:sub', persistent: true, sync: false } },
|
|
57
|
+
{ id: 'demo.interceptor', kind: 'api-interceptor', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/api/interceptors.ts`, symbol: 'demo.interceptor' }, details: { route: 'items', methods: ['POST'], phases: ['before'], activation: 'crud-pipeline', timeoutMs: 2000, failurePosture: 'fallback' } },
|
|
58
|
+
{ id: 'demo.cmd', kind: 'command-interceptor', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/commands/interceptors.ts`, symbol: 'demo.cmd' }, details: { targetCommand: 'demo.thing.update', phases: ['before-execute'] } },
|
|
59
|
+
{ id: 'demo.enricher', kind: 'response-enricher', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/data/enrichers.ts`, symbol: 'demo.enricher' }, details: { targetEntity: 'demo.entity', surfaces: ['list'], timeoutMs: 2000, fallback: 'none', critical: false, cachePosture: 'record-pure' } },
|
|
60
|
+
{ id: 'demo.widget@profile:demo:slot', kind: 'widget', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/widgets/injection-table.ts`, symbol: 'injectionTable' }, details: { payload: 'render', registryKey: 'demo.widget', executionGuard: 'host' } },
|
|
61
|
+
{ id: 'dashboard:demo.card', kind: 'widget', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/widgets/dashboard/card/widget.ts`, symbol: 'widget.metadata' }, details: { payload: 'dashboard', registryKey: 'demo.card', executionGuard: 'both' } },
|
|
62
|
+
{ id: 'demo.table-widget@data-table:demo.list:columns', kind: 'data-table', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/widgets/injection-table.ts`, symbol: 'injectionTable' }, details: { payload: 'column', tableId: 'demo.list', executionGuard: 'host' } },
|
|
63
|
+
{ id: 'demo.override.0:section:demo.login', kind: 'component-override', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/widgets/components.ts`, symbol: 'componentOverrides' }, details: { handle: 'section:demo.login', mode: 'replace', propsContract: 'x' } },
|
|
64
|
+
{ id: 'demo.mutation-guard', kind: 'mutation-guard', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/data/guards.ts`, symbol: 'demo.mutation-guard' }, details: { entityId: 'demo.entity', operations: ['create'], capabilities: ['block'], optimisticLock: 'preserved' } },
|
|
65
|
+
{ id: 'notification:demo.alert', kind: 'specialized-registry', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/notifications.ts`, symbol: 'demo.alert' }, details: { registry: 'notification', registryId: 'demo.alert', specialistRoute: 'notifications' } },
|
|
66
|
+
{ id: 'demo.alert-handler', kind: 'browser-reaction', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/notifications.handlers.ts`, symbol: 'notificationHandlers' }, details: { transports: ['notification-effect'], hooks: ['useNotificationEffect'], audienceScopeContract: 'tenant', overrideKey: 'demo.alert-handler' } },
|
|
67
|
+
{ id: 'demo.notification-handler.1', kind: 'browser-reaction', targets: [], scopeContract: '', source: { path: `${SOURCE_ROOT}/notifications.handlers.ts`, symbol: 'notificationHandlers' }, details: { transports: ['notification-effect'], hooks: ['useNotificationEffect'], audienceScopeContract: 'tenant' } },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
...overrides,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function targetFor(facts: ModuleFactsJsonEntry, domain: string, key?: string) {
|
|
75
|
+
const { overrideTargets } = collectModuleOverrideTargets(facts)
|
|
76
|
+
return overrideTargets.find((target) => target.domain === domain && (key === undefined || target.key === key))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('module override targets — adapter registry coverage', () => {
|
|
80
|
+
it('covers every runtime override domain with an adapter or explicit framework-only classification', () => {
|
|
81
|
+
const adaptersByDomain = new Map(MODULE_OVERRIDE_TARGET_ADAPTERS.map((adapter) => [adapter.domain, adapter]))
|
|
82
|
+
for (const domain of ALL_OVERRIDE_DOMAINS) {
|
|
83
|
+
const adapter = adaptersByDomain.get(domain)
|
|
84
|
+
expect(adapter).toBeDefined()
|
|
85
|
+
}
|
|
86
|
+
for (const adapter of MODULE_OVERRIDE_TARGET_ADAPTERS) {
|
|
87
|
+
expect(ALL_OVERRIDE_DOMAINS).toContain(adapter.domain)
|
|
88
|
+
}
|
|
89
|
+
// exactly the 16 runtime domains, one adapter each
|
|
90
|
+
expect(ALL_OVERRIDE_DOMAINS).toHaveLength(16)
|
|
91
|
+
expect(MODULE_OVERRIDE_TARGET_ADAPTERS).toHaveLength(16)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('classifies nav as framework-only (never a module target)', () => {
|
|
95
|
+
const navAdapter = MODULE_OVERRIDE_TARGET_ADAPTERS.find((adapter) => adapter.domain === 'nav')
|
|
96
|
+
expect(navAdapter?.frameworkOnly).toBe(true)
|
|
97
|
+
expect(navAdapter?.collect(baseFacts())).toEqual([])
|
|
98
|
+
const { overrideTargets } = collectModuleOverrideTargets(baseFacts())
|
|
99
|
+
expect(overrideTargets.some((target) => target.domain === 'nav')).toBe(false)
|
|
100
|
+
expect(overrideTargets.some((target) => JSON.stringify(target.path) === JSON.stringify(['nav', 'groupOrder']))).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('module override targets — golden target per domain family', () => {
|
|
105
|
+
const { overrideTargets } = collectModuleOverrideTargets(baseFacts())
|
|
106
|
+
const byPath = (path: string[]) => overrideTargets.find((target) => JSON.stringify(target.path) === JSON.stringify(path))
|
|
107
|
+
|
|
108
|
+
it.each([
|
|
109
|
+
['ai', ['ai', 'agents', 'demo.agent'], 'demo.agent', ['disable-replace']],
|
|
110
|
+
['ai', ['ai', 'tools', 'demo.tool'], 'demo.tool', ['disable-replace']],
|
|
111
|
+
['ai', ['ai', 'extensions'], undefined, ['additive']],
|
|
112
|
+
['routes', ['routes', 'api', 'GET /api/demo/items'], 'GET /api/demo/items', ['disable-replace']],
|
|
113
|
+
['routes', ['routes', 'api', 'POST /api/demo/items'], 'POST /api/demo/items', ['disable-replace']],
|
|
114
|
+
['routes', ['routes', 'pages', 'backend:/backend/demo/list'], 'backend:/backend/demo/list', ['disable-replace']],
|
|
115
|
+
['routes', ['routes', 'pages', 'frontend:/demo-portal'], 'frontend:/demo-portal', ['disable-replace']],
|
|
116
|
+
['events', ['events', 'subscribers', 'demo:sub'], 'demo:sub', ['disable-replace']],
|
|
117
|
+
['workers', ['workers', 'demo.worker'], 'demo.worker', ['disable-replace']],
|
|
118
|
+
['widgets', ['widgets', 'injection', 'demo.widget'], 'demo.widget', ['disable-replace']],
|
|
119
|
+
['widgets', ['widgets', 'injection', 'demo.table-widget'], 'demo.table-widget', ['disable-replace']],
|
|
120
|
+
['widgets', ['widgets', 'dashboard', 'demo.card'], 'demo.card', ['disable-replace']],
|
|
121
|
+
['widgets', ['widgets', 'components', 'section:demo.login'], 'section:demo.login', ['disable-replace']],
|
|
122
|
+
['notifications', ['notifications', 'types', 'demo.alert'], 'demo.alert', ['disable-replace']],
|
|
123
|
+
['interceptors', ['interceptors', 'demo.interceptor'], 'demo.interceptor', ['disable-replace']],
|
|
124
|
+
['commandInterceptors', ['commandInterceptors', 'demo.cmd'], 'demo.cmd', ['disable-replace']],
|
|
125
|
+
['enrichers', ['enrichers', 'demo.enricher'], 'demo.enricher', ['disable-replace']],
|
|
126
|
+
['guards', ['guards', 'demo.guard'], 'demo.guard', ['disable-replace']],
|
|
127
|
+
['cli', ['cli', 'demo:seed'], 'demo:seed', ['disable-replace']],
|
|
128
|
+
['setup', ['setup', 'seedDefaults'], undefined, ['replace']],
|
|
129
|
+
['acl', ['acl', 'features', 'demo.view'], 'demo.view', ['disable-replace']],
|
|
130
|
+
['di', ['di', 'demoService'], 'demoService', ['disable-replace']],
|
|
131
|
+
['encryption', ['encryption', 'maps', 'demo:secret'], 'demo:secret', ['disable-replace']],
|
|
132
|
+
])('emits %s target at %j', (domain, path, key, modes) => {
|
|
133
|
+
const target = byPath(path as string[])
|
|
134
|
+
expect(target).toBeDefined()
|
|
135
|
+
expect(target?.domain).toBe(domain)
|
|
136
|
+
expect(target?.key).toBe(key)
|
|
137
|
+
expect(target?.modes).toEqual(modes)
|
|
138
|
+
expect(target?.factRef.factSection.length).toBeGreaterThan(0)
|
|
139
|
+
expect(target?.source.sourcePath.length).toBeGreaterThan(0)
|
|
140
|
+
expect(target?.id).toBe(computeOverrideTargetId(MODULE_ID, domain as never, path as string[]))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('emits the supported setup hook/profile paths only (not unsupported hooks)', () => {
|
|
144
|
+
const setupPaths = overrideTargets.filter((t) => t.domain === 'setup').map((t) => t.path[1]).sort()
|
|
145
|
+
expect(setupPaths).toEqual(['defaultRoleFeatures', 'onTenantCreated', 'seedDefaults', 'seedExamples'])
|
|
146
|
+
expect(setupPaths).not.toContain('installStuff')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('module override targets — invariants', () => {
|
|
151
|
+
const facts = baseFacts()
|
|
152
|
+
const { overrideTargets } = collectModuleOverrideTargets(facts)
|
|
153
|
+
|
|
154
|
+
it('holds the terminal-key invariant for every adapter/target', () => {
|
|
155
|
+
for (const target of overrideTargets) {
|
|
156
|
+
if (target.key !== undefined) {
|
|
157
|
+
expect(target.path[target.path.length - 1]).toBe(target.key)
|
|
158
|
+
} else {
|
|
159
|
+
// keyless targets are structured singleton properties, never a lookup key
|
|
160
|
+
expect(['ai/extensions', 'setup']).toContain(
|
|
161
|
+
target.path[0] === 'setup' ? 'setup' : target.path.join('/'),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('preserves nested widget and AI paths without flattening keys', () => {
|
|
168
|
+
const widget = targetFor(facts, 'widgets', 'demo.widget')
|
|
169
|
+
expect(widget?.path).toEqual(['widgets', 'injection', 'demo.widget'])
|
|
170
|
+
const agent = targetFor(facts, 'ai', 'demo.agent')
|
|
171
|
+
expect(agent?.path).toEqual(['ai', 'agents', 'demo.agent'])
|
|
172
|
+
// never collapse to a single flattened dotted segment
|
|
173
|
+
for (const target of overrideTargets) {
|
|
174
|
+
expect(target.path.some((segment) => segment === `${target.domain}.injection` || segment === `${target.domain}.agents`)).toBe(false)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('points CLI override targets at the source-bearing command fact', () => {
|
|
179
|
+
const cli = targetFor(facts, 'cli', 'demo:seed')
|
|
180
|
+
expect(cli?.factRef).toEqual({ factSection: 'cliCommands', factKey: 'demo:seed' })
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('exposes DI tokens with the safe-metadata-only note and no runtime value/body', () => {
|
|
184
|
+
const di = targetFor(facts, 'di', 'demoService')
|
|
185
|
+
expect(di?.notes).toEqual(['safe-metadata-only'])
|
|
186
|
+
expect(di?.factRef).toEqual({ factSection: 'ownedContracts.di-registration', factKey: 'demoService' })
|
|
187
|
+
const serialized = JSON.stringify(di)
|
|
188
|
+
for (const forbidden of ['register', 'asClass', 'asFunction', 'asValue', 'handler', 'resolve', 'lifetime', 'value']) {
|
|
189
|
+
expect(serialized).not.toContain(forbidden)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('keeps page middleware under guards and never emits a mutation guard there', () => {
|
|
194
|
+
const guard = targetFor(facts, 'guards', 'demo.guard')
|
|
195
|
+
expect(guard?.notes).toEqual(['page-middleware-not-mutation-guard'])
|
|
196
|
+
expect(guard?.factRef.factSection).toBe('ownedContracts.page-middleware')
|
|
197
|
+
const guardsTargets = overrideTargets.filter((t) => t.domain === 'guards')
|
|
198
|
+
expect(guardsTargets.every((t) => t.key !== 'demo.mutation-guard')).toBe(true)
|
|
199
|
+
expect(guardsTargets.every((t) => !t.source.sourcePath.includes('data/guards.ts'))).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('sorts deterministically and regenerates byte-identically', () => {
|
|
203
|
+
const first = collectModuleOverrideTargets(baseFacts())
|
|
204
|
+
const second = collectModuleOverrideTargets(baseFacts())
|
|
205
|
+
expect(JSON.stringify(second)).toBe(JSON.stringify(first))
|
|
206
|
+
const domains = first.overrideTargets.map((t) => t.domain)
|
|
207
|
+
expect(domains).toEqual([...domains].sort((a, b) => a.localeCompare(b)))
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('module override targets — deterministic diagnostics (never a guessed key)', () => {
|
|
212
|
+
it('emits missing-source and no target when the route source cannot be proven', () => {
|
|
213
|
+
const facts = baseFacts({
|
|
214
|
+
apiRoutes: [{ path: '/demo/items', methods: ['GET'], auth: {}, sourcePath: null }],
|
|
215
|
+
})
|
|
216
|
+
const { overrideTargets, overrideTargetDiagnostics } = collectModuleOverrideTargets(facts)
|
|
217
|
+
expect(overrideTargets.some((t) => t.domain === 'routes' && t.path[1] === 'api')).toBe(false)
|
|
218
|
+
const diag = overrideTargetDiagnostics.find((d) => d.domain === 'routes' && d.code === 'missing-source')
|
|
219
|
+
expect(diag).toBeDefined()
|
|
220
|
+
expect(diag?.moduleId).toBe(MODULE_ID)
|
|
221
|
+
expect(diag?.candidatePath).toEqual(['routes', 'api', 'GET /api/demo/items'])
|
|
222
|
+
expect(diag?.source).toBeUndefined()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('emits missing-source when an acl feature has no indexed provenance', () => {
|
|
226
|
+
const facts = baseFacts({ factSources: [] })
|
|
227
|
+
const { overrideTargets, overrideTargetDiagnostics } = collectModuleOverrideTargets(facts)
|
|
228
|
+
expect(overrideTargets.some((t) => t.domain === 'acl')).toBe(false)
|
|
229
|
+
expect(overrideTargetDiagnostics.some((d) => d.domain === 'acl' && d.code === 'missing-source')).toBe(true)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('module override targets — keyed AI, notification handlers, setup profiles', () => {
|
|
234
|
+
it('emits exact ai.agents/ai.tools targets for keyed overrides and keeps extensions additive', () => {
|
|
235
|
+
const { overrideTargets } = collectModuleOverrideTargets(baseFacts())
|
|
236
|
+
const aiPaths = overrideTargets
|
|
237
|
+
.filter((target) => target.domain === 'ai')
|
|
238
|
+
.map((target) => target.path.join('.'))
|
|
239
|
+
.sort()
|
|
240
|
+
expect(aiPaths).toEqual([
|
|
241
|
+
'ai.agents.demo.agent',
|
|
242
|
+
'ai.agents.other.assistant',
|
|
243
|
+
'ai.extensions',
|
|
244
|
+
'ai.tools.demo.tool',
|
|
245
|
+
'ai.tools.other.tool',
|
|
246
|
+
])
|
|
247
|
+
|
|
248
|
+
const agentOverride = overrideTargets.find(
|
|
249
|
+
(target) => target.domain === 'ai' && target.key === 'other.assistant',
|
|
250
|
+
)
|
|
251
|
+
expect(agentOverride?.factRef).toEqual({
|
|
252
|
+
factSection: 'ownedContracts.ai-extension',
|
|
253
|
+
factKey: 'agent-override:other.assistant',
|
|
254
|
+
})
|
|
255
|
+
expect(agentOverride?.source.sourcePath).toBe(`${SOURCE_ROOT}/ai-agents.ts`)
|
|
256
|
+
|
|
257
|
+
const extensions = overrideTargets.find(
|
|
258
|
+
(target) => target.domain === 'ai' && target.path.join('.') === 'ai.extensions',
|
|
259
|
+
)
|
|
260
|
+
expect(extensions?.key).toBeUndefined()
|
|
261
|
+
expect(extensions?.factRef.factKey).toBe('agent-extension:other.agent')
|
|
262
|
+
expect(JSON.stringify(overrideTargets)).not.toContain('replacement')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('emits notifications.handlers targets keyed by the declared handler id', () => {
|
|
266
|
+
const { overrideTargets, overrideTargetDiagnostics } = collectModuleOverrideTargets(baseFacts())
|
|
267
|
+
const handler = overrideTargets.find(
|
|
268
|
+
(target) => target.domain === 'notifications' && target.path[1] === 'handlers',
|
|
269
|
+
)
|
|
270
|
+
expect(handler?.path).toEqual(['notifications', 'handlers', 'demo.alert-handler'])
|
|
271
|
+
expect(handler?.key).toBe('demo.alert-handler')
|
|
272
|
+
expect(handler?.factRef).toEqual({
|
|
273
|
+
factSection: 'extensionSurfaces.contributions',
|
|
274
|
+
factKey: 'demo.alert-handler',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// A handler that declares no id cannot be addressed by runtime.
|
|
278
|
+
expect(overrideTargets.some((target) => target.path[2] === 'demo.notification-handler.1')).toBe(false)
|
|
279
|
+
expect(overrideTargetDiagnostics).toEqual(expect.arrayContaining([
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
code: 'unsupported-dynamic-key',
|
|
282
|
+
domain: 'notifications',
|
|
283
|
+
candidatePath: ['notifications', 'handlers'],
|
|
284
|
+
}),
|
|
285
|
+
]))
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('emits setup.defaultCustomerRoleFeatures when customer-role profiles are declared', () => {
|
|
289
|
+
const withoutProfiles = collectModuleOverrideTargets(baseFacts()).overrideTargets
|
|
290
|
+
expect(withoutProfiles.some((target) => target.path.join('.') === 'setup.defaultCustomerRoleFeatures')).toBe(false)
|
|
291
|
+
|
|
292
|
+
const facts = baseFacts()
|
|
293
|
+
const setupFact = (facts.ownedContracts?.setup ?? [])[0]
|
|
294
|
+
const withProfiles = collectModuleOverrideTargets(baseFacts({
|
|
295
|
+
ownedContracts: {
|
|
296
|
+
...facts.ownedContracts,
|
|
297
|
+
setup: [{ ...setupFact, metadata: { ...setupFact.metadata, customerRoles: ['portal-admin'] } }],
|
|
298
|
+
},
|
|
299
|
+
})).overrideTargets
|
|
300
|
+
const target = withProfiles.find((entry) => entry.path.join('.') === 'setup.defaultCustomerRoleFeatures')
|
|
301
|
+
expect(target).toBeDefined()
|
|
302
|
+
expect(target?.factRef).toEqual({ factSection: 'ownedContracts.setup', factKey: 'demo:setup' })
|
|
303
|
+
})
|
|
304
|
+
})
|