@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,863 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import type { ModuleOverrideDomain } from '@open-mercato/shared/modules/overrides'
|
|
3
|
+
import type { ModuleExtensionContributionFact } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
4
|
+
import type {
|
|
5
|
+
ModuleFactRef,
|
|
6
|
+
ModuleFactSourceKind,
|
|
7
|
+
ModuleFactSourceRef,
|
|
8
|
+
ModuleFactsJsonEntry,
|
|
9
|
+
ModuleOwnedContractFact,
|
|
10
|
+
} from './module-facts'
|
|
11
|
+
import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
|
|
12
|
+
import { getFrameworkOverrideModes } from './module-extension-facts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Exact module-specific unified override targets.
|
|
16
|
+
*
|
|
17
|
+
* Spec: `.ai/specs/2026-08-02-module-facts-exact-override-targets.md`.
|
|
18
|
+
*
|
|
19
|
+
* Projects every concrete key a downstream app can set under
|
|
20
|
+
* `modules.<moduleId>.overrides.<domain>` from the normalized owned facts
|
|
21
|
+
* (spec 1 provenance) and the extension-surface contributions (spec 2). Each
|
|
22
|
+
* target carries the structured runtime `path`, the applicable framework mode,
|
|
23
|
+
* a module-owned `factRef`, and a portable `source`. The framework catalog
|
|
24
|
+
* (`framework-extension-points`) stays the authority for domains/modes; this
|
|
25
|
+
* layer only emits the module's concrete keys. Framework-only settings such as
|
|
26
|
+
* `nav.groupOrder` are never module targets.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type ModuleOverrideMode = 'disable-replace' | 'replace' | 'additive'
|
|
30
|
+
|
|
31
|
+
export type ModuleOverrideTargetNote =
|
|
32
|
+
| 'safe-metadata-only'
|
|
33
|
+
| 'page-middleware-not-mutation-guard'
|
|
34
|
+
|
|
35
|
+
export type ModuleOverrideTarget = {
|
|
36
|
+
id: string
|
|
37
|
+
domain: ModuleOverrideDomain
|
|
38
|
+
key?: string
|
|
39
|
+
path: string[]
|
|
40
|
+
modes: ModuleOverrideMode[]
|
|
41
|
+
factRef: ModuleFactRef
|
|
42
|
+
source: ModuleFactSourceRef
|
|
43
|
+
notes?: ModuleOverrideTargetNote[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ModuleOverrideTargetDiagnosticCode =
|
|
47
|
+
| 'missing-owned-fact'
|
|
48
|
+
| 'missing-source'
|
|
49
|
+
| 'unsupported-dynamic-key'
|
|
50
|
+
| 'unknown-framework-domain'
|
|
51
|
+
|
|
52
|
+
export type ModuleOverrideTargetDiagnostic = {
|
|
53
|
+
code: ModuleOverrideTargetDiagnosticCode
|
|
54
|
+
moduleId: string
|
|
55
|
+
domain: ModuleOverrideDomain
|
|
56
|
+
candidatePath?: string[]
|
|
57
|
+
source?: ModuleFactSourceRef
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Public, spec-shaped adapter view. `collect` mirrors the spec's illustrative
|
|
62
|
+
* signature (targets only); `diagnose` is the additive per-module diagnostic
|
|
63
|
+
* companion. Every adapter is bound to exactly one top-level override domain,
|
|
64
|
+
* or classified `frameworkOnly` (framework-catalog settings that never become
|
|
65
|
+
* module targets, e.g. `nav`).
|
|
66
|
+
*/
|
|
67
|
+
export type OverrideTargetAdapter = {
|
|
68
|
+
domain: ModuleOverrideDomain
|
|
69
|
+
collect(moduleFacts: ModuleFactsJsonEntry): ModuleOverrideTarget[]
|
|
70
|
+
diagnose(moduleFacts: ModuleFactsJsonEntry): ModuleOverrideTargetDiagnostic[]
|
|
71
|
+
frameworkOnly?: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type AdapterResult = {
|
|
75
|
+
targets: ModuleOverrideTarget[]
|
|
76
|
+
diagnostics: ModuleOverrideTargetDiagnostic[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type InternalAdapter = {
|
|
80
|
+
domain: ModuleOverrideDomain
|
|
81
|
+
frameworkOnly?: boolean
|
|
82
|
+
run(moduleFacts: ModuleFactsJsonEntry, resolveFactSource: FactSourceLookup): AdapterResult
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const OVERRIDE_MODES = getFrameworkOverrideModes()
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Supported modes come from the framework catalog only. A host the catalog does not
|
|
89
|
+
* describe is never guessed: the caller emits an `unknown-framework-domain`
|
|
90
|
+
* diagnostic and no target, so a downstream app is not told a key is overridable in
|
|
91
|
+
* a mode the runtime may not support.
|
|
92
|
+
*/
|
|
93
|
+
function modesFor(dottedHost: string): ModuleOverrideMode[] | null {
|
|
94
|
+
const mode = OVERRIDE_MODES[dottedHost]
|
|
95
|
+
return mode ? [mode] : null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The module id equals the last segment of the portable `sourceRoot`
|
|
100
|
+
* (`node_modules/<pkg>/src/modules/<moduleId>`), which the provenance builder
|
|
101
|
+
* derives from the module id itself, so this round-trips losslessly.
|
|
102
|
+
*/
|
|
103
|
+
function moduleIdOf(facts: ModuleFactsJsonEntry): string {
|
|
104
|
+
const segments = facts.sourceRoot.split('/').filter(Boolean)
|
|
105
|
+
return segments[segments.length - 1] ?? ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Deterministic id derived from the JSON-serialized `[moduleId, domain, path]`
|
|
110
|
+
* tuple so segment boundaries cannot collide. `path` remains the lossless
|
|
111
|
+
* authority; this is a compact stable handle for consumers that key by id.
|
|
112
|
+
*/
|
|
113
|
+
export function computeOverrideTargetId(moduleId: string, domain: ModuleOverrideDomain, path: string[]): string {
|
|
114
|
+
return crypto.createHash('sha1').update(JSON.stringify([moduleId, domain, path])).digest('hex').slice(0, 16)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
|
|
118
|
+
return (
|
|
119
|
+
left.sourcePath.localeCompare(right.sourcePath) ||
|
|
120
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
121
|
+
(left.exportName ?? '').localeCompare(right.exportName ?? '')
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function comparePaths(left: string[], right: string[]): number {
|
|
126
|
+
const length = Math.max(left.length, right.length)
|
|
127
|
+
for (let index = 0; index < length; index += 1) {
|
|
128
|
+
const leftSegment = left[index]
|
|
129
|
+
const rightSegment = right[index]
|
|
130
|
+
if (leftSegment === undefined) return -1
|
|
131
|
+
if (rightSegment === undefined) return 1
|
|
132
|
+
const compared = leftSegment.localeCompare(rightSegment)
|
|
133
|
+
if (compared !== 0) return compared
|
|
134
|
+
}
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type MakeTargetInput = {
|
|
139
|
+
moduleId: string
|
|
140
|
+
domain: ModuleOverrideDomain
|
|
141
|
+
path: string[]
|
|
142
|
+
dottedHost: string
|
|
143
|
+
factRef: ModuleFactRef
|
|
144
|
+
source: ModuleFactSourceRef
|
|
145
|
+
key?: string
|
|
146
|
+
notes?: ModuleOverrideTargetNote[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function makeTarget(input: MakeTargetInput): ModuleOverrideTarget | null {
|
|
150
|
+
const modes = modesFor(input.dottedHost)
|
|
151
|
+
if (!modes) return null
|
|
152
|
+
const target: ModuleOverrideTarget = {
|
|
153
|
+
id: computeOverrideTargetId(input.moduleId, input.domain, input.path),
|
|
154
|
+
domain: input.domain,
|
|
155
|
+
...(input.key !== undefined ? { key: input.key } : {}),
|
|
156
|
+
path: input.path,
|
|
157
|
+
modes,
|
|
158
|
+
factRef: input.factRef,
|
|
159
|
+
source: input.source,
|
|
160
|
+
...(input.notes && input.notes.length > 0 ? { notes: input.notes } : {}),
|
|
161
|
+
}
|
|
162
|
+
return target
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Emits the target, or an `unknown-framework-domain` diagnostic when the framework
|
|
167
|
+
* catalog does not describe the target's dotted host.
|
|
168
|
+
*/
|
|
169
|
+
function pushTarget(
|
|
170
|
+
targets: ModuleOverrideTarget[],
|
|
171
|
+
diagnostics: ModuleOverrideTargetDiagnostic[],
|
|
172
|
+
input: MakeTargetInput,
|
|
173
|
+
): void {
|
|
174
|
+
const target = makeTarget(input)
|
|
175
|
+
if (target) {
|
|
176
|
+
targets.push(target)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
diagnostics.push({
|
|
180
|
+
code: 'unknown-framework-domain',
|
|
181
|
+
moduleId: input.moduleId,
|
|
182
|
+
domain: input.domain,
|
|
183
|
+
candidatePath: input.path,
|
|
184
|
+
source: input.source,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Portable contribution source ref → override target source ref. */
|
|
189
|
+
function contributionSource(contribution: ModuleExtensionContributionFact): ModuleFactSourceRef {
|
|
190
|
+
return {
|
|
191
|
+
sourcePath: contribution.source.path,
|
|
192
|
+
...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Owned-contract source ref, dropping the (constant) exportName for compactness. */
|
|
197
|
+
function ownedSource(fact: ModuleOwnedContractFact): ModuleFactSourceRef {
|
|
198
|
+
return {
|
|
199
|
+
sourcePath: fact.source.sourcePath,
|
|
200
|
+
...(fact.source.line ? { line: fact.source.line } : {}),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function contributionsOfKind<K extends ModuleExtensionContributionFact['kind']>(
|
|
205
|
+
facts: ModuleFactsJsonEntry,
|
|
206
|
+
kind: K,
|
|
207
|
+
): Array<Extract<ModuleExtensionContributionFact, { kind: K }>> {
|
|
208
|
+
const contributions = facts.extensionSurfaces?.contributions ?? []
|
|
209
|
+
return contributions.filter((contribution): contribution is Extract<ModuleExtensionContributionFact, { kind: K }> =>
|
|
210
|
+
contribution.kind === kind,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function indexedFactSourcePath(
|
|
215
|
+
resolveFactSource: FactSourceLookup,
|
|
216
|
+
kind: ModuleFactSourceKind,
|
|
217
|
+
id: string,
|
|
218
|
+
): string | null {
|
|
219
|
+
return resolveFactSource(kind, id)?.sourcePath ?? null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Local canonical override-key normalizers.
|
|
224
|
+
//
|
|
225
|
+
// These MIRROR the private `normalizeApiRouteOverrideKey` /
|
|
226
|
+
// `normalizePageRouteOverrideKey` in
|
|
227
|
+
// `packages/shared/src/modules/overrides.ts` (not exported by that module).
|
|
228
|
+
// Round-trip tests feed the emitted keys back through the exported runtime
|
|
229
|
+
// appliers (`applyApiOverridesToManifests` / `applyPageOverridesToManifests`)
|
|
230
|
+
// to prove equivalence. See the implementation report: exporting the shared
|
|
231
|
+
// normalizers would let this file import rather than mirror them.
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
const VALID_HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Canonical `'METHOD /api/path'` public override key for a route method. Route
|
|
238
|
+
* fact paths are stored without the `/api` prefix (e.g. `/customers/x`), so the
|
|
239
|
+
* public key downstream apps write is `METHOD /api<path>`.
|
|
240
|
+
*/
|
|
241
|
+
function apiRouteOverrideKey(method: string, routePath: string): string | null {
|
|
242
|
+
const upper = method.trim().toUpperCase()
|
|
243
|
+
if (!VALID_HTTP_METHODS.has(upper)) return null
|
|
244
|
+
const withLead = routePath.startsWith('/') ? routePath : `/${routePath}`
|
|
245
|
+
const publicPath = withLead.startsWith('/api/') || withLead === '/api' ? withLead : `/api${withLead}`
|
|
246
|
+
const normalized = publicPath.replace(/\/+$/, '') || '/'
|
|
247
|
+
return `${upper} ${normalized}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function pageRouteOverrideKey(routePath: string): string | null {
|
|
251
|
+
const trimmed = routePath.trim()
|
|
252
|
+
if (!trimmed) return null
|
|
253
|
+
if (trimmed.startsWith('backend:') || trimmed.startsWith('frontend:')) return trimmed
|
|
254
|
+
const withLead = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
|
255
|
+
const stripped = withLead.replace(/\/+$/, '') || '/'
|
|
256
|
+
if (stripped === '/frontend') return 'frontend:/'
|
|
257
|
+
if (stripped.startsWith('/frontend/')) return `frontend:/${stripped.slice('/frontend/'.length)}`
|
|
258
|
+
if (stripped === '/backend' || stripped.startsWith('/backend/')) return `backend:${stripped}`
|
|
259
|
+
return `frontend:${stripped}`
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Adapters (one per top-level override domain).
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
const aiAdapter: InternalAdapter = {
|
|
267
|
+
domain: 'ai',
|
|
268
|
+
run(facts) {
|
|
269
|
+
const moduleId = moduleIdOf(facts)
|
|
270
|
+
const targets: ModuleOverrideTarget[] = []
|
|
271
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
272
|
+
|
|
273
|
+
for (const agent of facts.aiAgents) {
|
|
274
|
+
if (!agent.sourcePath) {
|
|
275
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'ai', candidatePath: ['ai', 'agents', agent.id] })
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
const path = ['ai', 'agents', agent.id]
|
|
279
|
+
pushTarget(targets, diagnostics, {
|
|
280
|
+
moduleId, domain: 'ai', path, key: agent.id, dottedHost: 'ai.agents',
|
|
281
|
+
factRef: { factSection: 'aiAgents', factKey: agent.id },
|
|
282
|
+
source: { sourcePath: agent.sourcePath },
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
for (const tool of facts.aiTools) {
|
|
286
|
+
if (!tool.sourcePath) {
|
|
287
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'ai', candidatePath: ['ai', 'tools', tool.name] })
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
const path = ['ai', 'tools', tool.name]
|
|
291
|
+
pushTarget(targets, diagnostics, {
|
|
292
|
+
moduleId, domain: 'ai', path, key: tool.name, dottedHost: 'ai.tools',
|
|
293
|
+
factRef: { factSection: 'aiTools', factKey: tool.name },
|
|
294
|
+
source: { sourcePath: tool.sourcePath },
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
// AI file contracts split by mode: keyed `aiAgentOverrides` / `aiToolOverrides`
|
|
298
|
+
// entries are exact `ai.agents.<id>` / `ai.tools.<id>` targets, while additive
|
|
299
|
+
// `aiAgentExtensions` patches belong to the keyless `ai.extensions` array.
|
|
300
|
+
const aiFileContracts = facts.ownedContracts?.['ai-extension'] ?? []
|
|
301
|
+
const additiveExtensions: ModuleOwnedContractFact[] = []
|
|
302
|
+
for (const fact of aiFileContracts) {
|
|
303
|
+
const metadata = fact.metadata ?? {}
|
|
304
|
+
if (metadata.mode !== 'override') {
|
|
305
|
+
additiveExtensions.push(fact)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
const overrideKey = typeof metadata.overrideKey === 'string' ? metadata.overrideKey : null
|
|
309
|
+
const target = metadata.target === 'agent' ? 'agents' : metadata.target === 'tool' ? 'tools' : null
|
|
310
|
+
if (!overrideKey || !target) {
|
|
311
|
+
diagnostics.push({
|
|
312
|
+
code: 'unsupported-dynamic-key',
|
|
313
|
+
moduleId,
|
|
314
|
+
domain: 'ai',
|
|
315
|
+
candidatePath: ['ai', target ?? 'extensions'],
|
|
316
|
+
source: ownedSource(fact),
|
|
317
|
+
})
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
const path = ['ai', target, overrideKey]
|
|
321
|
+
pushTarget(targets, diagnostics, {
|
|
322
|
+
moduleId, domain: 'ai', path, key: overrideKey, dottedHost: `ai.${target}`,
|
|
323
|
+
factRef: { factSection: 'ownedContracts.ai-extension', factKey: fact.id },
|
|
324
|
+
source: ownedSource(fact),
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
if (additiveExtensions.length > 0) {
|
|
328
|
+
const anchor = [...additiveExtensions].sort((left, right) => left.id.localeCompare(right.id))[0]
|
|
329
|
+
pushTarget(targets, diagnostics, {
|
|
330
|
+
moduleId, domain: 'ai', path: ['ai', 'extensions'], dottedHost: 'ai.extensions',
|
|
331
|
+
factRef: { factSection: 'ownedContracts.ai-extension', factKey: anchor.id },
|
|
332
|
+
source: ownedSource(anchor),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
return { targets, diagnostics }
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const routesAdapter: InternalAdapter = {
|
|
340
|
+
domain: 'routes',
|
|
341
|
+
run(facts) {
|
|
342
|
+
const moduleId = moduleIdOf(facts)
|
|
343
|
+
const targets: ModuleOverrideTarget[] = []
|
|
344
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
345
|
+
|
|
346
|
+
for (const route of facts.apiRoutes) {
|
|
347
|
+
for (const method of route.methods) {
|
|
348
|
+
const key = apiRouteOverrideKey(method, route.path)
|
|
349
|
+
if (!key) {
|
|
350
|
+
diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'routes', candidatePath: ['routes', 'api', `${method} ${route.path}`] })
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (!route.sourcePath) {
|
|
354
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'routes', candidatePath: ['routes', 'api', key] })
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
const path = ['routes', 'api', key]
|
|
358
|
+
pushTarget(targets, diagnostics, {
|
|
359
|
+
moduleId, domain: 'routes', path, key, dottedHost: 'routes.api',
|
|
360
|
+
factRef: { factSection: 'apiRoutes', factKey: route.path },
|
|
361
|
+
source: { sourcePath: route.sourcePath },
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const emitPage = (page: { path: string; sourcePath: string }, section: 'backendPages' | 'frontendPages'): void => {
|
|
367
|
+
const key = pageRouteOverrideKey(page.path)
|
|
368
|
+
if (!key) {
|
|
369
|
+
diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'routes', candidatePath: ['routes', 'pages', page.path] })
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
if (!page.sourcePath) {
|
|
373
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'routes', candidatePath: ['routes', 'pages', key] })
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
const path = ['routes', 'pages', key]
|
|
377
|
+
pushTarget(targets, diagnostics, {
|
|
378
|
+
moduleId, domain: 'routes', path, key, dottedHost: 'routes.pages',
|
|
379
|
+
factRef: { factSection: section, factKey: page.path },
|
|
380
|
+
source: { sourcePath: page.sourcePath },
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
for (const page of facts.backendPages) emitPage(page, 'backendPages')
|
|
384
|
+
for (const page of facts.frontendPages) emitPage(page, 'frontendPages')
|
|
385
|
+
|
|
386
|
+
return { targets, diagnostics }
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const eventsAdapter: InternalAdapter = {
|
|
391
|
+
domain: 'events',
|
|
392
|
+
run(facts) {
|
|
393
|
+
const moduleId = moduleIdOf(facts)
|
|
394
|
+
const targets: ModuleOverrideTarget[] = []
|
|
395
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
396
|
+
for (const contribution of contributionsOfKind(facts, 'subscriber')) {
|
|
397
|
+
const subscriberId = contribution.details.subscriberId || contribution.id
|
|
398
|
+
const path = ['events', 'subscribers', subscriberId]
|
|
399
|
+
if (!contribution.source.path) {
|
|
400
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'events', candidatePath: path })
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
pushTarget(targets, diagnostics, {
|
|
404
|
+
moduleId, domain: 'events', path, key: subscriberId, dottedHost: 'events.subscribers',
|
|
405
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
406
|
+
source: contributionSource(contribution),
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
return { targets, diagnostics }
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const workersAdapter: InternalAdapter = {
|
|
414
|
+
domain: 'workers',
|
|
415
|
+
run(facts) {
|
|
416
|
+
const moduleId = moduleIdOf(facts)
|
|
417
|
+
const targets: ModuleOverrideTarget[] = []
|
|
418
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
419
|
+
for (const fact of facts.ownedContracts?.worker ?? []) {
|
|
420
|
+
const path = ['workers', fact.id]
|
|
421
|
+
if (!fact.source.sourcePath) {
|
|
422
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'workers', candidatePath: path })
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
pushTarget(targets, diagnostics, {
|
|
426
|
+
moduleId, domain: 'workers', path, key: fact.id, dottedHost: 'workers',
|
|
427
|
+
factRef: { factSection: 'ownedContracts.worker', factKey: fact.id },
|
|
428
|
+
source: ownedSource(fact),
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
return { targets, diagnostics }
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const widgetsAdapter: InternalAdapter = {
|
|
436
|
+
domain: 'widgets',
|
|
437
|
+
run(facts) {
|
|
438
|
+
const moduleId = moduleIdOf(facts)
|
|
439
|
+
const targets: ModuleOverrideTarget[] = []
|
|
440
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
441
|
+
|
|
442
|
+
const emitInjection = (registryKey: string, contribution: ModuleExtensionContributionFact): void => {
|
|
443
|
+
const path = ['widgets', 'injection', registryKey]
|
|
444
|
+
pushTarget(targets, diagnostics, {
|
|
445
|
+
moduleId, domain: 'widgets', path, key: registryKey, dottedHost: 'widgets.injection',
|
|
446
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
447
|
+
source: contributionSource(contribution),
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (const contribution of contributionsOfKind(facts, 'widget')) {
|
|
452
|
+
const registryKey = contribution.details.registryKey
|
|
453
|
+
if (!registryKey) {
|
|
454
|
+
diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'widgets', candidatePath: ['widgets', 'injection', contribution.id] })
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
if (contribution.details.payload === 'dashboard') {
|
|
458
|
+
const path = ['widgets', 'dashboard', registryKey]
|
|
459
|
+
pushTarget(targets, diagnostics, {
|
|
460
|
+
moduleId, domain: 'widgets', path, key: registryKey, dottedHost: 'widgets.dashboard',
|
|
461
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
462
|
+
source: contributionSource(contribution),
|
|
463
|
+
})
|
|
464
|
+
} else {
|
|
465
|
+
emitInjection(registryKey, contribution)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// data-table / crud-form contributions are injection-widget entries whose
|
|
470
|
+
// registry key is the id segment before the `@<spot>` suffix.
|
|
471
|
+
for (const contribution of [
|
|
472
|
+
...contributionsOfKind(facts, 'data-table'),
|
|
473
|
+
...contributionsOfKind(facts, 'crud-form'),
|
|
474
|
+
]) {
|
|
475
|
+
const registryKey = contribution.id.split('@')[0]
|
|
476
|
+
if (!registryKey || registryKey === contribution.id) {
|
|
477
|
+
diagnostics.push({ code: 'unsupported-dynamic-key', moduleId, domain: 'widgets', candidatePath: ['widgets', 'injection', contribution.id] })
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
emitInjection(registryKey, contribution)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (const contribution of contributionsOfKind(facts, 'component-override')) {
|
|
484
|
+
const componentId = contribution.details.handle
|
|
485
|
+
const path = ['widgets', 'components', componentId]
|
|
486
|
+
pushTarget(targets, diagnostics, {
|
|
487
|
+
moduleId, domain: 'widgets', path, key: componentId, dottedHost: 'widgets.components',
|
|
488
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
489
|
+
source: contributionSource(contribution),
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { targets, diagnostics }
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const notificationsAdapter: InternalAdapter = {
|
|
498
|
+
domain: 'notifications',
|
|
499
|
+
run(facts, resolveFactSource) {
|
|
500
|
+
const moduleId = moduleIdOf(facts)
|
|
501
|
+
const targets: ModuleOverrideTarget[] = []
|
|
502
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
503
|
+
|
|
504
|
+
const registrySources = new Map<string, ModuleFactSourceRef>()
|
|
505
|
+
for (const contribution of contributionsOfKind(facts, 'specialized-registry')) {
|
|
506
|
+
if (contribution.details.registry !== 'notification') continue
|
|
507
|
+
registrySources.set(contribution.details.registryId, contributionSource(contribution))
|
|
508
|
+
}
|
|
509
|
+
for (const type of facts.notifications) {
|
|
510
|
+
const path = ['notifications', 'types', type]
|
|
511
|
+
const indexedPath = indexedFactSourcePath(resolveFactSource, 'notification', type)
|
|
512
|
+
const source = registrySources.get(type) ?? (indexedPath ? { sourcePath: indexedPath } : null)
|
|
513
|
+
if (!source) {
|
|
514
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'notifications', candidatePath: path })
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
pushTarget(targets, diagnostics, {
|
|
518
|
+
moduleId, domain: 'notifications', path, key: type, dottedHost: 'notifications.types',
|
|
519
|
+
factRef: { factSection: 'notifications', factKey: type },
|
|
520
|
+
source,
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Reactive handlers (`notifications.handlers.ts`) are keyed by their declared
|
|
525
|
+
// handler id; a handler that declares none cannot be addressed by runtime.
|
|
526
|
+
for (const contribution of contributionsOfKind(facts, 'browser-reaction')) {
|
|
527
|
+
if (!contribution.details.transports.includes('notification-effect')) continue
|
|
528
|
+
const overrideKey = contribution.details.overrideKey
|
|
529
|
+
if (!overrideKey) {
|
|
530
|
+
diagnostics.push({
|
|
531
|
+
code: 'unsupported-dynamic-key',
|
|
532
|
+
moduleId,
|
|
533
|
+
domain: 'notifications',
|
|
534
|
+
candidatePath: ['notifications', 'handlers'],
|
|
535
|
+
source: contributionSource(contribution),
|
|
536
|
+
})
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
pushTarget(targets, diagnostics, {
|
|
540
|
+
moduleId,
|
|
541
|
+
domain: 'notifications',
|
|
542
|
+
path: ['notifications', 'handlers', overrideKey],
|
|
543
|
+
key: overrideKey,
|
|
544
|
+
dottedHost: 'notifications.handlers',
|
|
545
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
546
|
+
source: contributionSource(contribution),
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
return { targets, diagnostics }
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function contributionIdAdapter(
|
|
554
|
+
domain: ModuleOverrideDomain,
|
|
555
|
+
kind: ModuleExtensionContributionFact['kind'],
|
|
556
|
+
dottedHost: string,
|
|
557
|
+
): InternalAdapter {
|
|
558
|
+
return {
|
|
559
|
+
domain,
|
|
560
|
+
run(facts) {
|
|
561
|
+
const moduleId = moduleIdOf(facts)
|
|
562
|
+
const targets: ModuleOverrideTarget[] = []
|
|
563
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
564
|
+
for (const contribution of contributionsOfKind(facts, kind)) {
|
|
565
|
+
const path = [domain, contribution.id]
|
|
566
|
+
if (!contribution.source.path) {
|
|
567
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain, candidatePath: path })
|
|
568
|
+
continue
|
|
569
|
+
}
|
|
570
|
+
pushTarget(targets, diagnostics, {
|
|
571
|
+
moduleId, domain, path, key: contribution.id, dottedHost,
|
|
572
|
+
factRef: { factSection: 'extensionSurfaces.contributions', factKey: contribution.id },
|
|
573
|
+
source: contributionSource(contribution),
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
return { targets, diagnostics }
|
|
577
|
+
},
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const interceptorsAdapter = contributionIdAdapter('interceptors', 'api-interceptor', 'interceptors')
|
|
582
|
+
const commandInterceptorsAdapter = contributionIdAdapter('commandInterceptors', 'command-interceptor', 'commandInterceptors')
|
|
583
|
+
const enrichersAdapter = contributionIdAdapter('enrichers', 'response-enricher', 'enrichers')
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* `guards` targets ONLY backend/frontend page route middleware
|
|
587
|
+
* (`ownedContracts['page-middleware']`). `data/guards.ts` mutation guards
|
|
588
|
+
* (contribution kind `mutation-guard`) are a different runtime contract and
|
|
589
|
+
* MUST NEVER surface here — every target carries the closed
|
|
590
|
+
* `page-middleware-not-mutation-guard` note.
|
|
591
|
+
*/
|
|
592
|
+
const guardsAdapter: InternalAdapter = {
|
|
593
|
+
domain: 'guards',
|
|
594
|
+
run(facts) {
|
|
595
|
+
const moduleId = moduleIdOf(facts)
|
|
596
|
+
const targets: ModuleOverrideTarget[] = []
|
|
597
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
598
|
+
for (const fact of facts.ownedContracts?.['page-middleware'] ?? []) {
|
|
599
|
+
const path = ['guards', fact.id]
|
|
600
|
+
if (!fact.source.sourcePath) {
|
|
601
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'guards', candidatePath: path })
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
604
|
+
pushTarget(targets, diagnostics, {
|
|
605
|
+
moduleId, domain: 'guards', path, key: fact.id, dottedHost: 'guards',
|
|
606
|
+
factRef: { factSection: 'ownedContracts.page-middleware', factKey: fact.id },
|
|
607
|
+
source: ownedSource(fact),
|
|
608
|
+
notes: ['page-middleware-not-mutation-guard'],
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
return { targets, diagnostics }
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const cliAdapter: InternalAdapter = {
|
|
616
|
+
domain: 'cli',
|
|
617
|
+
run(facts) {
|
|
618
|
+
const moduleId = moduleIdOf(facts)
|
|
619
|
+
const targets: ModuleOverrideTarget[] = []
|
|
620
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
621
|
+
for (const command of facts.cliCommands) {
|
|
622
|
+
const path = ['cli', command.command]
|
|
623
|
+
if (!command.sourcePath) {
|
|
624
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'cli', candidatePath: path })
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
pushTarget(targets, diagnostics, {
|
|
628
|
+
moduleId, domain: 'cli', path, key: command.command, dottedHost: 'cli',
|
|
629
|
+
factRef: { factSection: 'cliCommands', factKey: command.command },
|
|
630
|
+
source: { sourcePath: command.sourcePath },
|
|
631
|
+
})
|
|
632
|
+
}
|
|
633
|
+
return { targets, diagnostics }
|
|
634
|
+
},
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// `SetupOverridesShape` keys the runtime override resolver accepts. Only these
|
|
638
|
+
// supported hook/profile properties become `setup` targets.
|
|
639
|
+
const SUPPORTED_SETUP_HOOKS = new Set(['seedDefaults', 'seedExamples', 'onTenantCreated'])
|
|
640
|
+
|
|
641
|
+
const setupAdapter: InternalAdapter = {
|
|
642
|
+
domain: 'setup',
|
|
643
|
+
run(facts) {
|
|
644
|
+
const moduleId = moduleIdOf(facts)
|
|
645
|
+
const targets: ModuleOverrideTarget[] = []
|
|
646
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
647
|
+
const setupFacts = facts.ownedContracts?.setup ?? []
|
|
648
|
+
for (const fact of setupFacts) {
|
|
649
|
+
if (!fact.source.sourcePath) {
|
|
650
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'setup', candidatePath: ['setup'] })
|
|
651
|
+
continue
|
|
652
|
+
}
|
|
653
|
+
const source = ownedSource(fact)
|
|
654
|
+
const properties: string[] = []
|
|
655
|
+
const hooks = fact.metadata?.hooks
|
|
656
|
+
if (Array.isArray(hooks)) {
|
|
657
|
+
for (const hook of hooks) {
|
|
658
|
+
if (typeof hook === 'string' && SUPPORTED_SETUP_HOOKS.has(hook)) properties.push(hook)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const roles = fact.metadata?.roles
|
|
662
|
+
if (Array.isArray(roles) && roles.length > 0) properties.push('defaultRoleFeatures')
|
|
663
|
+
const customerRoles = fact.metadata?.customerRoles
|
|
664
|
+
if (Array.isArray(customerRoles) && customerRoles.length > 0) properties.push('defaultCustomerRoleFeatures')
|
|
665
|
+
for (const property of properties) {
|
|
666
|
+
const path = ['setup', property]
|
|
667
|
+
pushTarget(targets, diagnostics, {
|
|
668
|
+
moduleId, domain: 'setup', path, dottedHost: 'setup',
|
|
669
|
+
factRef: { factSection: 'ownedContracts.setup', factKey: fact.id },
|
|
670
|
+
source,
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { targets, diagnostics }
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const aclAdapter: InternalAdapter = {
|
|
679
|
+
domain: 'acl',
|
|
680
|
+
run(facts, resolveFactSource) {
|
|
681
|
+
const moduleId = moduleIdOf(facts)
|
|
682
|
+
const targets: ModuleOverrideTarget[] = []
|
|
683
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
684
|
+
for (const feature of facts.aclFeatures) {
|
|
685
|
+
const path = ['acl', 'features', feature]
|
|
686
|
+
const sourcePath = indexedFactSourcePath(resolveFactSource, 'acl-feature', feature)
|
|
687
|
+
if (!sourcePath) {
|
|
688
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'acl', candidatePath: path })
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
pushTarget(targets, diagnostics, {
|
|
692
|
+
moduleId, domain: 'acl', path, key: feature, dottedHost: 'acl.features',
|
|
693
|
+
factRef: { factSection: 'aclFeatures', factKey: feature },
|
|
694
|
+
source: { sourcePath },
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
return { targets, diagnostics }
|
|
698
|
+
},
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* `di` targets expose the exact registration token and correlate to the rich DI
|
|
703
|
+
* owned fact (which carries the registration kind). They NEVER include the
|
|
704
|
+
* `asValue` payload, constructed service, or factory/class body — the target
|
|
705
|
+
* schema has no such field and carries the `safe-metadata-only` note.
|
|
706
|
+
*/
|
|
707
|
+
const diAdapter: InternalAdapter = {
|
|
708
|
+
domain: 'di',
|
|
709
|
+
run(facts) {
|
|
710
|
+
const moduleId = moduleIdOf(facts)
|
|
711
|
+
const targets: ModuleOverrideTarget[] = []
|
|
712
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
713
|
+
for (const fact of facts.ownedContracts?.['di-registration'] ?? []) {
|
|
714
|
+
const path = ['di', fact.id]
|
|
715
|
+
if (!fact.source.sourcePath) {
|
|
716
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'di', candidatePath: path })
|
|
717
|
+
continue
|
|
718
|
+
}
|
|
719
|
+
pushTarget(targets, diagnostics, {
|
|
720
|
+
moduleId, domain: 'di', path, key: fact.id, dottedHost: 'di',
|
|
721
|
+
factRef: { factSection: 'ownedContracts.di-registration', factKey: fact.id },
|
|
722
|
+
source: ownedSource(fact),
|
|
723
|
+
notes: ['safe-metadata-only'],
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
return { targets, diagnostics }
|
|
727
|
+
},
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const encryptionAdapter: InternalAdapter = {
|
|
731
|
+
domain: 'encryption',
|
|
732
|
+
run(facts) {
|
|
733
|
+
const moduleId = moduleIdOf(facts)
|
|
734
|
+
const targets: ModuleOverrideTarget[] = []
|
|
735
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
736
|
+
for (const fact of facts.ownedContracts?.encryption ?? []) {
|
|
737
|
+
const path = ['encryption', 'maps', fact.id]
|
|
738
|
+
if (!fact.source.sourcePath) {
|
|
739
|
+
diagnostics.push({ code: 'missing-source', moduleId, domain: 'encryption', candidatePath: path })
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
742
|
+
pushTarget(targets, diagnostics, {
|
|
743
|
+
moduleId, domain: 'encryption', path, key: fact.id, dottedHost: 'encryption.maps',
|
|
744
|
+
factRef: { factSection: 'ownedContracts.encryption', factKey: fact.id },
|
|
745
|
+
source: ownedSource(fact),
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
return { targets, diagnostics }
|
|
749
|
+
},
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** `nav.groupOrder` is a framework-only setting; never a module target. */
|
|
753
|
+
const navAdapter: InternalAdapter = {
|
|
754
|
+
domain: 'nav',
|
|
755
|
+
frameworkOnly: true,
|
|
756
|
+
run() {
|
|
757
|
+
return { targets: [], diagnostics: [] }
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const INTERNAL_ADAPTERS: InternalAdapter[] = [
|
|
762
|
+
aiAdapter,
|
|
763
|
+
routesAdapter,
|
|
764
|
+
eventsAdapter,
|
|
765
|
+
workersAdapter,
|
|
766
|
+
widgetsAdapter,
|
|
767
|
+
notificationsAdapter,
|
|
768
|
+
interceptorsAdapter,
|
|
769
|
+
commandInterceptorsAdapter,
|
|
770
|
+
enrichersAdapter,
|
|
771
|
+
guardsAdapter,
|
|
772
|
+
cliAdapter,
|
|
773
|
+
setupAdapter,
|
|
774
|
+
aclAdapter,
|
|
775
|
+
diAdapter,
|
|
776
|
+
encryptionAdapter,
|
|
777
|
+
navAdapter,
|
|
778
|
+
]
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Runtime enumeration of every unified override domain. Typed as a
|
|
782
|
+
* `Record<ModuleOverrideDomain, true>` so adding a member to the runtime
|
|
783
|
+
* `ModuleOverrideDomain` union fails the build here until it is listed — and
|
|
784
|
+
* the coverage test then fails until an adapter (or `frameworkOnly`
|
|
785
|
+
* classification) is registered.
|
|
786
|
+
*/
|
|
787
|
+
const OVERRIDE_DOMAIN_PRESENCE: Record<ModuleOverrideDomain, true> = {
|
|
788
|
+
ai: true,
|
|
789
|
+
routes: true,
|
|
790
|
+
events: true,
|
|
791
|
+
workers: true,
|
|
792
|
+
widgets: true,
|
|
793
|
+
notifications: true,
|
|
794
|
+
interceptors: true,
|
|
795
|
+
commandInterceptors: true,
|
|
796
|
+
enrichers: true,
|
|
797
|
+
guards: true,
|
|
798
|
+
cli: true,
|
|
799
|
+
setup: true,
|
|
800
|
+
acl: true,
|
|
801
|
+
di: true,
|
|
802
|
+
encryption: true,
|
|
803
|
+
nav: true,
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export const ALL_OVERRIDE_DOMAINS = Object.keys(OVERRIDE_DOMAIN_PRESENCE) as ModuleOverrideDomain[]
|
|
807
|
+
|
|
808
|
+
export const MODULE_OVERRIDE_TARGET_ADAPTERS: OverrideTargetAdapter[] = INTERNAL_ADAPTERS.map((adapter) => ({
|
|
809
|
+
domain: adapter.domain,
|
|
810
|
+
...(adapter.frameworkOnly ? { frameworkOnly: true } : {}),
|
|
811
|
+
collect: (moduleFacts) => adapter.run(moduleFacts, buildFactSourceLookup(moduleFacts)).targets,
|
|
812
|
+
diagnose: (moduleFacts) => adapter.run(moduleFacts, buildFactSourceLookup(moduleFacts)).diagnostics,
|
|
813
|
+
}))
|
|
814
|
+
|
|
815
|
+
export type ModuleOverrideTargetsResult = {
|
|
816
|
+
overrideTargets: ModuleOverrideTarget[]
|
|
817
|
+
overrideTargetDiagnostics: ModuleOverrideTargetDiagnostic[]
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Project every exact override target (and per-module diagnostic) for one
|
|
822
|
+
* module's facts. Output sorts by domain, then path segments, then source;
|
|
823
|
+
* diagnostics sort by moduleId, domain, code, then path/source. Targets are
|
|
824
|
+
* deduped by `(domain, path)` keeping the source-first entry.
|
|
825
|
+
*/
|
|
826
|
+
export function collectModuleOverrideTargets(
|
|
827
|
+
facts: ModuleFactsJsonEntry,
|
|
828
|
+
resolveFactSource: FactSourceLookup = buildFactSourceLookup(facts),
|
|
829
|
+
): ModuleOverrideTargetsResult {
|
|
830
|
+
const collected: ModuleOverrideTarget[] = []
|
|
831
|
+
const diagnostics: ModuleOverrideTargetDiagnostic[] = []
|
|
832
|
+
for (const adapter of INTERNAL_ADAPTERS) {
|
|
833
|
+
if (adapter.frameworkOnly) continue
|
|
834
|
+
const result = adapter.run(facts, resolveFactSource)
|
|
835
|
+
collected.push(...result.targets)
|
|
836
|
+
diagnostics.push(...result.diagnostics)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const byPath = new Map<string, ModuleOverrideTarget>()
|
|
840
|
+
for (const target of collected) {
|
|
841
|
+
const pathKey = `${target.domain} ${JSON.stringify(target.path)}`
|
|
842
|
+
const existing = byPath.get(pathKey)
|
|
843
|
+
if (!existing || compareSourceRefs(target.source, existing.source) < 0) byPath.set(pathKey, target)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const overrideTargets = [...byPath.values()].sort(
|
|
847
|
+
(left, right) =>
|
|
848
|
+
left.domain.localeCompare(right.domain) ||
|
|
849
|
+
comparePaths(left.path, right.path) ||
|
|
850
|
+
compareSourceRefs(left.source, right.source),
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
const overrideTargetDiagnostics = diagnostics.sort(
|
|
854
|
+
(left, right) =>
|
|
855
|
+
left.moduleId.localeCompare(right.moduleId) ||
|
|
856
|
+
left.domain.localeCompare(right.domain) ||
|
|
857
|
+
left.code.localeCompare(right.code) ||
|
|
858
|
+
comparePaths(left.candidatePath ?? [], right.candidatePath ?? []) ||
|
|
859
|
+
compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
return { overrideTargets, overrideTargetDiagnostics }
|
|
863
|
+
}
|