@open-mercato/cli 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682
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/BACKWARD_COMPATIBILITY.md +2 -0
- package/dist/agentic/guides/upstream/manifest.json +3 -3
- package/dist/agentic/shared/AGENTS.md.template +6 -5
- package/dist/agentic/shared/ai/harness/README.md +2 -2
- package/dist/agentic/shared/ai/harness/RELEASE.md +4 -4
- package/dist/agentic/shared/ai/harness/cases.json +94 -1
- package/dist/agentic/shared/ai/harness/cases.schema.json +3 -3
- package/dist/agentic/shared/ai/harness/validators.json +1 -1
- 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/SKILL.md +1 -1
- 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/agentic/shared/scripts/evaluate-agent-harness.mjs +37 -12
- 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
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import ts from 'typescript-js'
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ModuleExtensionContributionFact,
|
|
6
|
+
ModuleExtensionHostFact,
|
|
7
|
+
ModuleExtensionSurfaceFacts,
|
|
8
|
+
ModuleExtensionTargetRef,
|
|
9
|
+
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
5
10
|
import { toSnake } from '../utils'
|
|
11
|
+
import { extractCommandIdsFromSource } from './module-registry'
|
|
12
|
+
import { MODULE_CODE_EXTENSIONS } from './scanner'
|
|
13
|
+
import type { ApiRouteInterceptorBridge } from './module-extension-facts'
|
|
6
14
|
import {
|
|
7
15
|
assertNoUnresolvedExtensionTargets,
|
|
16
|
+
correlateIncomingExtensions,
|
|
8
17
|
correlateModuleExtensionFacts,
|
|
18
|
+
extractActivationTargetOwners,
|
|
9
19
|
extractKnownApiRouteIds,
|
|
10
20
|
extractKnownCommandIds,
|
|
11
21
|
extractModuleExtensionFacts,
|
|
12
22
|
renderFrameworkExtensionPointsMarkdown,
|
|
13
23
|
withModuleExtensionFactExtractionCache,
|
|
14
24
|
} from './module-extension-facts'
|
|
25
|
+
import {
|
|
26
|
+
collectModuleOverrideTargets,
|
|
27
|
+
type ModuleOverrideTarget,
|
|
28
|
+
type ModuleOverrideTargetDiagnostic,
|
|
29
|
+
} from './module-override-targets'
|
|
30
|
+
import { buildFactSourceLookup, type FactSourceLookup } from './module-fact-sources'
|
|
15
31
|
|
|
16
32
|
export interface ModuleEntityFact {
|
|
17
33
|
id: string
|
|
@@ -34,9 +50,31 @@ export interface ModuleApiRouteFact {
|
|
|
34
50
|
sourcePath: string | null
|
|
35
51
|
}
|
|
36
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Statically declared page metadata that affects extensibility or access, using the
|
|
55
|
+
* runtime `PageMetadata` contract keys and the same alias precedence as
|
|
56
|
+
* `resolvePageRouteMetadata` (`pageGroup ?? group`, `pageGroupKey ?? groupKey`,
|
|
57
|
+
* `pageOrder ?? order`). Executable guards, icons, titles and translation results are
|
|
58
|
+
* never serialized.
|
|
59
|
+
*/
|
|
60
|
+
export interface ModulePageMetadata {
|
|
61
|
+
requireAuth?: boolean
|
|
62
|
+
requireCustomerAuth?: boolean
|
|
63
|
+
requireFeatures?: string[]
|
|
64
|
+
requireCustomerFeatures?: string[]
|
|
65
|
+
navHidden?: boolean
|
|
66
|
+
navGroup?: string
|
|
67
|
+
navGroupKey?: string
|
|
68
|
+
navOrder?: number
|
|
69
|
+
pageContext?: 'main' | 'admin' | 'settings' | 'profile'
|
|
70
|
+
}
|
|
71
|
+
|
|
37
72
|
export interface ModulePageFact {
|
|
38
73
|
path: string
|
|
39
74
|
sourcePath: string
|
|
75
|
+
/** Companion metadata file (`page.meta.*` / `meta.*`) when runtime reads metadata from one. */
|
|
76
|
+
metadataSourcePath?: string
|
|
77
|
+
metadata?: ModulePageMetadata
|
|
40
78
|
}
|
|
41
79
|
|
|
42
80
|
export interface ModuleCliCommandFact {
|
|
@@ -68,6 +106,108 @@ export interface ModuleHostTokens {
|
|
|
68
106
|
tableIds: string[]
|
|
69
107
|
}
|
|
70
108
|
|
|
109
|
+
export type ModuleFactSourceRef = {
|
|
110
|
+
sourcePath: string
|
|
111
|
+
exportName?: string
|
|
112
|
+
line?: number
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ModuleFactRef = {
|
|
116
|
+
factSection: string
|
|
117
|
+
factKey: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type ModuleFactSourceKind =
|
|
121
|
+
| 'module-metadata'
|
|
122
|
+
| 'entity'
|
|
123
|
+
| 'event'
|
|
124
|
+
| 'acl-feature'
|
|
125
|
+
| 'api-route'
|
|
126
|
+
| 'di-registration'
|
|
127
|
+
| 'search'
|
|
128
|
+
| 'vector'
|
|
129
|
+
| 'notification'
|
|
130
|
+
| 'cli-command'
|
|
131
|
+
| 'backend-page'
|
|
132
|
+
| 'frontend-page'
|
|
133
|
+
| 'ai-tool'
|
|
134
|
+
| 'ai-agent'
|
|
135
|
+
| 'ai-extension'
|
|
136
|
+
| 'command'
|
|
137
|
+
| 'subscriber'
|
|
138
|
+
| 'worker'
|
|
139
|
+
| 'page-middleware'
|
|
140
|
+
| 'setup'
|
|
141
|
+
| 'encryption'
|
|
142
|
+
| 'custom-entity'
|
|
143
|
+
| 'integration'
|
|
144
|
+
| 'generator-plugin'
|
|
145
|
+
| 'extension-host'
|
|
146
|
+
| 'extension-contribution'
|
|
147
|
+
|
|
148
|
+
export type ModuleOwnedContractKind =
|
|
149
|
+
| 'module-metadata'
|
|
150
|
+
| 'command'
|
|
151
|
+
| 'worker'
|
|
152
|
+
| 'page-middleware'
|
|
153
|
+
| 'setup'
|
|
154
|
+
| 'encryption'
|
|
155
|
+
| 'di-registration'
|
|
156
|
+
| 'custom-entity'
|
|
157
|
+
| 'ai-extension'
|
|
158
|
+
| 'generator-plugin'
|
|
159
|
+
|
|
160
|
+
export type ModuleFactSafeScalar = string | number | boolean | null
|
|
161
|
+
|
|
162
|
+
export type ModuleOwnedContractFact = {
|
|
163
|
+
kind: ModuleOwnedContractKind
|
|
164
|
+
id: string
|
|
165
|
+
source: ModuleFactSourceRef
|
|
166
|
+
metadata?: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ModuleOwnedContracts = Partial<
|
|
170
|
+
Record<ModuleOwnedContractKind, ModuleOwnedContractFact[]>
|
|
171
|
+
>
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Provenance pointer into a fact section that already serializes the declaration site.
|
|
175
|
+
* `factKey` is omitted when it equals the owning entry's `id`, which is the common case.
|
|
176
|
+
*/
|
|
177
|
+
export type ModuleFactIndexRef = {
|
|
178
|
+
factSection: string
|
|
179
|
+
factKey?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* One provenance entry per proven `(kind, id)` in the module. Exactly one of `source`
|
|
184
|
+
* (the declaration site, for facts that have no other home in the JSON) or `factRef`
|
|
185
|
+
* (a deterministic pointer into a fact section that already carries the source inline)
|
|
186
|
+
* is present, so a consumer resolves any fact's origin through this single index.
|
|
187
|
+
*/
|
|
188
|
+
export type ModuleFactSourceEntry = {
|
|
189
|
+
kind: ModuleFactSourceKind
|
|
190
|
+
id: string
|
|
191
|
+
source?: ModuleFactSourceRef
|
|
192
|
+
factRef?: ModuleFactIndexRef
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ModuleFactDiagnostic = {
|
|
196
|
+
code: 'duplicate-source' | 'unresolved-static-contract'
|
|
197
|
+
kind: ModuleFactSourceKind
|
|
198
|
+
id: string
|
|
199
|
+
source?: ModuleFactSourceRef
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ModuleDiRegistrationFact = {
|
|
203
|
+
token: string
|
|
204
|
+
registrationKind: 'function' | 'class' | 'value' | 'alias'
|
|
205
|
+
providerSymbol?: string
|
|
206
|
+
lifetime?: 'singleton' | 'scoped' | 'transient'
|
|
207
|
+
injectionMode?: 'classic' | 'proxy'
|
|
208
|
+
source: ModuleFactSourceRef
|
|
209
|
+
}
|
|
210
|
+
|
|
71
211
|
export interface ModuleFacts {
|
|
72
212
|
module: string
|
|
73
213
|
title: string | null
|
|
@@ -91,6 +231,11 @@ export interface ModuleFacts {
|
|
|
91
231
|
aiTools: ModuleAiToolFact[]
|
|
92
232
|
aiAgents: ModuleAiAgentFact[]
|
|
93
233
|
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
234
|
+
factSources?: ModuleFactSourceEntry[]
|
|
235
|
+
ownedContracts?: ModuleOwnedContracts
|
|
236
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
237
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
238
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
94
239
|
warnings: string[]
|
|
95
240
|
}
|
|
96
241
|
|
|
@@ -833,7 +978,7 @@ function extractModulePages(
|
|
|
833
978
|
const fileName = segments.pop() as string
|
|
834
979
|
const fileStem = fileName.replace(/\.tsx$/, '')
|
|
835
980
|
const isModernPage = fileStem === 'page'
|
|
836
|
-
if (!isModernPage && (fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
|
|
981
|
+
if (!isModernPage && (fileStem === 'meta' || fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
|
|
837
982
|
const sourceFile = readSourceFile(filePath)
|
|
838
983
|
if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
|
|
839
984
|
const routeSegments = isModernPage ? segments : [...segments, fileStem]
|
|
@@ -846,9 +991,16 @@ function extractModulePages(
|
|
|
846
991
|
: [moduleId, ...routeSegments].filter(Boolean).join('/')}`
|
|
847
992
|
if (seen.has(routePath)) continue
|
|
848
993
|
seen.add(routePath)
|
|
994
|
+
const metadataFilePath = resolvePageMetadataFile(path.dirname(filePath))
|
|
995
|
+
const metadataSourceFile = metadataFilePath ? readSourceFile(metadataFilePath) : sourceFile
|
|
996
|
+
const metadata = metadataSourceFile ? readPageMetadata(metadataSourceFile) : undefined
|
|
849
997
|
pages.push({
|
|
850
998
|
path: routePath,
|
|
851
999
|
sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
|
|
1000
|
+
...(metadataFilePath
|
|
1001
|
+
? { metadataSourcePath: toPortableSourcePath(moduleRoot, sourceRoot, metadataFilePath) }
|
|
1002
|
+
: {}),
|
|
1003
|
+
...(metadata ? { metadata } : {}),
|
|
852
1004
|
})
|
|
853
1005
|
}
|
|
854
1006
|
return pages.sort((left, right) => left.path.localeCompare(right.path))
|
|
@@ -1002,6 +1154,742 @@ function extractModuleMeta(indexFilePath: string | null): { title: string | null
|
|
|
1002
1154
|
}
|
|
1003
1155
|
}
|
|
1004
1156
|
|
|
1157
|
+
function nodeLine(sourceFile: ts.SourceFile, node: ts.Node): number {
|
|
1158
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function readPropertyNameText(name: ts.PropertyName): string | undefined {
|
|
1162
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text
|
|
1163
|
+
return undefined
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function readMethodBearingPropertyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1167
|
+
const names: string[] = []
|
|
1168
|
+
for (const property of objectLiteral.properties) {
|
|
1169
|
+
if (ts.isMethodDeclaration(property)) {
|
|
1170
|
+
const name = readPropertyNameText(property.name)
|
|
1171
|
+
if (name) names.push(name)
|
|
1172
|
+
continue
|
|
1173
|
+
}
|
|
1174
|
+
if (ts.isPropertyAssignment(property)) {
|
|
1175
|
+
const name = readPropertyNameText(property.name)
|
|
1176
|
+
if (!name) continue
|
|
1177
|
+
const initializer = unwrapExpression(property.initializer)
|
|
1178
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) names.push(name)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function readObjectKeyNames(objectLiteral: ts.ObjectLiteralExpression): string[] {
|
|
1185
|
+
const names: string[] = []
|
|
1186
|
+
for (const property of objectLiteral.properties) {
|
|
1187
|
+
const name = getPropertyName(property)
|
|
1188
|
+
if (name) names.push(name)
|
|
1189
|
+
}
|
|
1190
|
+
return names.sort((left, right) => left.localeCompare(right))
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function mapDiRegistrationKind(base: string | null): ModuleDiRegistrationFact['registrationKind'] | null {
|
|
1194
|
+
switch (base) {
|
|
1195
|
+
case 'asFunction':
|
|
1196
|
+
return 'function'
|
|
1197
|
+
case 'asClass':
|
|
1198
|
+
return 'class'
|
|
1199
|
+
case 'asValue':
|
|
1200
|
+
return 'value'
|
|
1201
|
+
case 'aliasTo':
|
|
1202
|
+
return 'alias'
|
|
1203
|
+
default:
|
|
1204
|
+
return null
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function readDiChainMetadata(initializer: ts.Expression): {
|
|
1209
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1210
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1211
|
+
} {
|
|
1212
|
+
const result: {
|
|
1213
|
+
lifetime?: ModuleDiRegistrationFact['lifetime']
|
|
1214
|
+
injectionMode?: ModuleDiRegistrationFact['injectionMode']
|
|
1215
|
+
} = {}
|
|
1216
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1217
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1218
|
+
const method = current.expression.name.text
|
|
1219
|
+
if ((method === 'singleton' || method === 'scoped' || method === 'transient') && !result.lifetime) {
|
|
1220
|
+
result.lifetime = method
|
|
1221
|
+
} else if (method === 'proxy' && !result.injectionMode) {
|
|
1222
|
+
result.injectionMode = 'proxy'
|
|
1223
|
+
} else if (method === 'classic' && !result.injectionMode) {
|
|
1224
|
+
result.injectionMode = 'classic'
|
|
1225
|
+
}
|
|
1226
|
+
current = unwrapExpression(current.expression.expression)
|
|
1227
|
+
}
|
|
1228
|
+
return result
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function readDiProviderSymbol(
|
|
1232
|
+
initializer: ts.Expression,
|
|
1233
|
+
kind: ModuleDiRegistrationFact['registrationKind'],
|
|
1234
|
+
): string | undefined {
|
|
1235
|
+
let current: ts.Expression = unwrapExpression(initializer)
|
|
1236
|
+
while (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
1237
|
+
current = unwrapExpression(current.expression.expression)
|
|
1238
|
+
}
|
|
1239
|
+
if (!ts.isCallExpression(current)) return undefined
|
|
1240
|
+
const argument = current.arguments[0]
|
|
1241
|
+
if (!argument) return undefined
|
|
1242
|
+
if (kind === 'value') return undefined
|
|
1243
|
+
if (kind === 'alias' && ts.isStringLiteralLike(argument)) return argument.text
|
|
1244
|
+
if ((kind === 'function' || kind === 'class') && ts.isIdentifier(argument)) return argument.text
|
|
1245
|
+
return undefined
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function extractDiRegistrationFacts(
|
|
1249
|
+
diFilePath: string | null,
|
|
1250
|
+
sourcePath: string,
|
|
1251
|
+
): { facts: ModuleDiRegistrationFact[]; unresolvedTokens: string[] } {
|
|
1252
|
+
if (!diFilePath) return { facts: [], unresolvedTokens: [] }
|
|
1253
|
+
const sourceFile = readSourceFile(diFilePath)
|
|
1254
|
+
if (!sourceFile) return { facts: [], unresolvedTokens: [] }
|
|
1255
|
+
|
|
1256
|
+
const facts: ModuleDiRegistrationFact[] = []
|
|
1257
|
+
const unresolvedTokens: string[] = []
|
|
1258
|
+
const visit = (node: ts.Node): void => {
|
|
1259
|
+
if (
|
|
1260
|
+
ts.isCallExpression(node) &&
|
|
1261
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
1262
|
+
node.expression.name.text === 'register'
|
|
1263
|
+
) {
|
|
1264
|
+
const argument = node.arguments[0]
|
|
1265
|
+
if (argument && ts.isObjectLiteralExpression(argument)) {
|
|
1266
|
+
for (const property of argument.properties) {
|
|
1267
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
1268
|
+
const token = getPropertyName(property)
|
|
1269
|
+
if (!token) continue
|
|
1270
|
+
const kind = mapDiRegistrationKind(detectAwilixRegistrationKind(property.initializer))
|
|
1271
|
+
if (!kind) {
|
|
1272
|
+
if (!unresolvedTokens.includes(token)) unresolvedTokens.push(token)
|
|
1273
|
+
continue
|
|
1274
|
+
}
|
|
1275
|
+
const chain = readDiChainMetadata(property.initializer)
|
|
1276
|
+
const providerSymbol = readDiProviderSymbol(property.initializer, kind)
|
|
1277
|
+
facts.push({
|
|
1278
|
+
token,
|
|
1279
|
+
registrationKind: kind,
|
|
1280
|
+
...(providerSymbol ? { providerSymbol } : {}),
|
|
1281
|
+
...(chain.lifetime ? { lifetime: chain.lifetime } : {}),
|
|
1282
|
+
...(chain.injectionMode ? { injectionMode: chain.injectionMode } : {}),
|
|
1283
|
+
source: { sourcePath, exportName: token, line: nodeLine(sourceFile, property) },
|
|
1284
|
+
})
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
node.forEachChild(visit)
|
|
1289
|
+
}
|
|
1290
|
+
sourceFile.forEachChild(visit)
|
|
1291
|
+
return { facts, unresolvedTokens }
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function deriveLegacyDiTokens(facts: readonly ModuleDiRegistrationFact[]): string[] {
|
|
1295
|
+
const tokens: string[] = []
|
|
1296
|
+
const seen = new Set<string>()
|
|
1297
|
+
for (const fact of facts) {
|
|
1298
|
+
if (fact.registrationKind !== 'function' && fact.registrationKind !== 'class') continue
|
|
1299
|
+
if (seen.has(fact.token)) continue
|
|
1300
|
+
seen.add(fact.token)
|
|
1301
|
+
tokens.push(fact.token)
|
|
1302
|
+
}
|
|
1303
|
+
return tokens
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function dedupeOwnedContractFacts(facts: readonly ModuleOwnedContractFact[]): ModuleOwnedContractFact[] {
|
|
1307
|
+
const byId = new Map<string, ModuleOwnedContractFact>()
|
|
1308
|
+
for (const fact of facts) {
|
|
1309
|
+
const existing = byId.get(fact.id)
|
|
1310
|
+
if (!existing) {
|
|
1311
|
+
byId.set(fact.id, fact)
|
|
1312
|
+
continue
|
|
1313
|
+
}
|
|
1314
|
+
if (fact.source.sourcePath.localeCompare(existing.source.sourcePath) < 0) byId.set(fact.id, fact)
|
|
1315
|
+
}
|
|
1316
|
+
return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id))
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const PAGE_CONTEXT_VALUES = new Set(['main', 'admin', 'settings', 'profile'])
|
|
1320
|
+
|
|
1321
|
+
function readAliasedStringProperty(
|
|
1322
|
+
metadata: ts.ObjectLiteralExpression,
|
|
1323
|
+
preferred: string,
|
|
1324
|
+
fallback: string,
|
|
1325
|
+
): string | undefined {
|
|
1326
|
+
return readStringPropertyInitializer(metadata, preferred) ?? readStringPropertyInitializer(metadata, fallback)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function readNumericProperty(metadata: ts.ObjectLiteralExpression, propertyName: string): number | undefined {
|
|
1330
|
+
const initializer = getObjectPropertyInitializer(metadata, propertyName)
|
|
1331
|
+
if (!initializer || !ts.isNumericLiteral(initializer)) return undefined
|
|
1332
|
+
return Number(initializer.text)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function readPageMetadata(sourceFile: ts.SourceFile): ModulePageMetadata | undefined {
|
|
1336
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1337
|
+
if (!metadata) return undefined
|
|
1338
|
+
const result: ModulePageMetadata = {}
|
|
1339
|
+
const requireAuth = readBooleanPropertyInitializer(metadata, 'requireAuth')
|
|
1340
|
+
if (requireAuth !== undefined) result.requireAuth = requireAuth
|
|
1341
|
+
const requireCustomerAuth = readBooleanPropertyInitializer(metadata, 'requireCustomerAuth')
|
|
1342
|
+
if (requireCustomerAuth !== undefined) result.requireCustomerAuth = requireCustomerAuth
|
|
1343
|
+
const requireFeatures = readStringArrayPropertyInitializer(metadata, 'requireFeatures')
|
|
1344
|
+
if (requireFeatures && requireFeatures.length > 0) result.requireFeatures = requireFeatures
|
|
1345
|
+
const requireCustomerFeatures = readStringArrayPropertyInitializer(metadata, 'requireCustomerFeatures')
|
|
1346
|
+
if (requireCustomerFeatures && requireCustomerFeatures.length > 0) {
|
|
1347
|
+
result.requireCustomerFeatures = requireCustomerFeatures
|
|
1348
|
+
}
|
|
1349
|
+
const navHidden = readBooleanPropertyInitializer(metadata, 'navHidden')
|
|
1350
|
+
if (navHidden !== undefined) result.navHidden = navHidden
|
|
1351
|
+
const navGroup = readAliasedStringProperty(metadata, 'pageGroup', 'group')
|
|
1352
|
+
if (navGroup) result.navGroup = navGroup
|
|
1353
|
+
const navGroupKey = readAliasedStringProperty(metadata, 'pageGroupKey', 'groupKey')
|
|
1354
|
+
if (navGroupKey) result.navGroupKey = navGroupKey
|
|
1355
|
+
const navOrder = readNumericProperty(metadata, 'pageOrder') ?? readNumericProperty(metadata, 'order')
|
|
1356
|
+
if (navOrder !== undefined) result.navOrder = navOrder
|
|
1357
|
+
const pageContext = readStringPropertyInitializer(metadata, 'pageContext')
|
|
1358
|
+
if (pageContext && PAGE_CONTEXT_VALUES.has(pageContext)) {
|
|
1359
|
+
result.pageContext = pageContext as ModulePageMetadata['pageContext']
|
|
1360
|
+
}
|
|
1361
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Mirrors the runtime companion-file convention: a page's metadata comes from
|
|
1366
|
+
* `page.meta.*` (or `meta.*`) in the page directory when one exists, and only
|
|
1367
|
+
* otherwise from the page component itself.
|
|
1368
|
+
*/
|
|
1369
|
+
function resolvePageMetadataFile(pageDirectory: string): string | null {
|
|
1370
|
+
for (const baseName of ['page.meta', 'meta']) {
|
|
1371
|
+
for (const extension of MODULE_CODE_EXTENSIONS) {
|
|
1372
|
+
const candidate = path.join(pageDirectory, `${baseName}${extension}`)
|
|
1373
|
+
if (fs.existsSync(candidate)) return candidate
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return null
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function extractModuleMetadataContract(
|
|
1380
|
+
indexFilePath: string | null,
|
|
1381
|
+
moduleId: string,
|
|
1382
|
+
sourcePath: string | null,
|
|
1383
|
+
): ModuleOwnedContractFact | null {
|
|
1384
|
+
if (!indexFilePath || !sourcePath) return null
|
|
1385
|
+
const sourceFile = readSourceFile(indexFilePath)
|
|
1386
|
+
if (!sourceFile) return null
|
|
1387
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1388
|
+
if (!metadata) return null
|
|
1389
|
+
const facts: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1390
|
+
const name = readStringPropertyInitializer(metadata, 'name')
|
|
1391
|
+
if (name) facts.name = name
|
|
1392
|
+
const version = readStringPropertyInitializer(metadata, 'version')
|
|
1393
|
+
if (version) facts.version = version
|
|
1394
|
+
const requires = readStringArrayPropertyInitializer(metadata, 'requires')
|
|
1395
|
+
if (requires && requires.length > 0) facts.requires = requires
|
|
1396
|
+
const ejectable = readBooleanPropertyInitializer(metadata, 'ejectable')
|
|
1397
|
+
if (ejectable !== undefined) facts.ejectable = ejectable
|
|
1398
|
+
return {
|
|
1399
|
+
kind: 'module-metadata',
|
|
1400
|
+
id: moduleId,
|
|
1401
|
+
source: { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) },
|
|
1402
|
+
...(Object.keys(facts).length > 0 ? { metadata: facts } : {}),
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function extractCommandContractCandidates(
|
|
1407
|
+
moduleId: string,
|
|
1408
|
+
moduleRoot: string,
|
|
1409
|
+
sourceRoot: string,
|
|
1410
|
+
): Array<{ id: string; sourcePath: string }> {
|
|
1411
|
+
const commandsRoot = path.join(moduleRoot, 'commands')
|
|
1412
|
+
if (!fs.existsSync(commandsRoot)) return []
|
|
1413
|
+
const candidates: Array<{ id: string; sourcePath: string }> = []
|
|
1414
|
+
for (const filePath of listSourceFilesRecursive(commandsRoot)) {
|
|
1415
|
+
if (path.basename(filePath).replace(/\.tsx?$/, '') === 'interceptors') continue
|
|
1416
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1417
|
+
for (const id of extractCommandIdsFromSource(filePath)) candidates.push({ id, sourcePath })
|
|
1418
|
+
}
|
|
1419
|
+
return candidates
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Runtime worker id: `metadata.id` when declared, otherwise the path-derived
|
|
1424
|
+
* `<moduleId>:workers:<...segments>:<fileStem>` that `discoverWorkers` builds.
|
|
1425
|
+
*/
|
|
1426
|
+
function deriveWorkerId(moduleId: string, workersRoot: string, filePath: string): string {
|
|
1427
|
+
const relativePath = path.relative(workersRoot, filePath).split(path.sep).join('/')
|
|
1428
|
+
const segments = relativePath.split('/')
|
|
1429
|
+
const fileName = segments.pop() as string
|
|
1430
|
+
const fileStem = fileName.replace(/\.tsx?$/, '')
|
|
1431
|
+
return [moduleId, 'workers', ...segments, fileStem].filter(Boolean).join(':')
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/** Resolves a string property directly or through a local `const` in the same file. */
|
|
1435
|
+
function readResolvedStringProperty(
|
|
1436
|
+
objectLiteral: ts.ObjectLiteralExpression,
|
|
1437
|
+
propertyName: string,
|
|
1438
|
+
initializers: Map<string, ts.Expression>,
|
|
1439
|
+
): string | undefined {
|
|
1440
|
+
const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
|
|
1441
|
+
if (!initializer) return undefined
|
|
1442
|
+
const resolved = unwrapExpression(initializer)
|
|
1443
|
+
if (ts.isStringLiteralLike(resolved)) return resolved.text
|
|
1444
|
+
if (ts.isIdentifier(resolved)) {
|
|
1445
|
+
const declaration = initializers.get(resolved.text)
|
|
1446
|
+
if (declaration) {
|
|
1447
|
+
const declarationValue = unwrapExpression(declaration)
|
|
1448
|
+
if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return undefined
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function extractWorkerContracts(
|
|
1455
|
+
moduleId: string,
|
|
1456
|
+
moduleRoot: string,
|
|
1457
|
+
sourceRoot: string,
|
|
1458
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1459
|
+
const workersRoot = path.join(moduleRoot, 'workers')
|
|
1460
|
+
if (!fs.existsSync(workersRoot)) return { facts: [], unresolved: [] }
|
|
1461
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1462
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1463
|
+
for (const filePath of listSourceFilesRecursive(workersRoot)) {
|
|
1464
|
+
const sourceFile = readSourceFile(filePath)
|
|
1465
|
+
if (!sourceFile) continue
|
|
1466
|
+
const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
|
|
1467
|
+
if (!metadata) continue
|
|
1468
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1469
|
+
const source = { sourcePath, exportName: 'metadata', line: nodeLine(sourceFile, metadata) }
|
|
1470
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1471
|
+
const workerId = readResolvedStringProperty(metadata, 'id', initializers)
|
|
1472
|
+
?? deriveWorkerId(moduleId, workersRoot, filePath)
|
|
1473
|
+
|
|
1474
|
+
// Runtime skips any worker without a queue, so a worker missing the required
|
|
1475
|
+
// property is not a contract; a present-but-dynamic queue is reported instead.
|
|
1476
|
+
const hasQueueProperty = getObjectPropertyInitializer(metadata, 'queue') !== undefined
|
|
1477
|
+
const queue = readResolvedStringProperty(metadata, 'queue', initializers)
|
|
1478
|
+
if (!hasQueueProperty) {
|
|
1479
|
+
unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1480
|
+
continue
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const workerMetadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1484
|
+
if (queue) workerMetadata.queue = queue
|
|
1485
|
+
else unresolved.push({ code: 'unresolved-static-contract', kind: 'worker', id: workerId, source })
|
|
1486
|
+
const name = readResolvedStringProperty(metadata, 'name', initializers)
|
|
1487
|
+
if (name) workerMetadata.name = name
|
|
1488
|
+
const concurrencyInitializer = getObjectPropertyInitializer(metadata, 'concurrency')
|
|
1489
|
+
if (concurrencyInitializer && ts.isNumericLiteral(concurrencyInitializer)) {
|
|
1490
|
+
workerMetadata.concurrency = Number(concurrencyInitializer.text)
|
|
1491
|
+
}
|
|
1492
|
+
facts.push({
|
|
1493
|
+
kind: 'worker',
|
|
1494
|
+
id: workerId,
|
|
1495
|
+
source,
|
|
1496
|
+
...(Object.keys(workerMetadata).length > 0 ? { metadata: workerMetadata } : {}),
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function extractPageMiddlewareContracts(
|
|
1503
|
+
moduleRoot: string,
|
|
1504
|
+
sourceRoot: string,
|
|
1505
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1506
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1507
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1508
|
+
for (const surface of ['backend', 'frontend'] as const) {
|
|
1509
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, surface), 'middleware')
|
|
1510
|
+
if (!filePath) continue
|
|
1511
|
+
const sourceFile = readSourceFile(filePath)
|
|
1512
|
+
if (!sourceFile) continue
|
|
1513
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1514
|
+
const middlewareArray =
|
|
1515
|
+
findArrayLiteralDeclaration(sourceFile, 'middleware') ??
|
|
1516
|
+
(() => {
|
|
1517
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1518
|
+
return defaultExport ? unwrapArrayLiteral(defaultExport) : null
|
|
1519
|
+
})()
|
|
1520
|
+
if (!middlewareArray) continue
|
|
1521
|
+
let index = 0
|
|
1522
|
+
for (const element of middlewareArray.elements) {
|
|
1523
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1524
|
+
const middlewareId = readStringPropertyInitializer(element, 'id')
|
|
1525
|
+
if (!middlewareId) {
|
|
1526
|
+
unresolved.push({
|
|
1527
|
+
code: 'unresolved-static-contract',
|
|
1528
|
+
kind: 'page-middleware',
|
|
1529
|
+
id: `${sourcePath}#${index}`,
|
|
1530
|
+
source: { sourcePath, line: nodeLine(sourceFile, element) },
|
|
1531
|
+
})
|
|
1532
|
+
index += 1
|
|
1533
|
+
continue
|
|
1534
|
+
}
|
|
1535
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = { surface }
|
|
1536
|
+
const mode = readStringPropertyInitializer(element, 'mode')
|
|
1537
|
+
if (mode) metadata.mode = mode
|
|
1538
|
+
const priorityInitializer = getObjectPropertyInitializer(element, 'priority')
|
|
1539
|
+
if (priorityInitializer && ts.isNumericLiteral(priorityInitializer)) {
|
|
1540
|
+
metadata.priority = Number(priorityInitializer.text)
|
|
1541
|
+
}
|
|
1542
|
+
facts.push({
|
|
1543
|
+
kind: 'page-middleware',
|
|
1544
|
+
id: middlewareId,
|
|
1545
|
+
source: { sourcePath, exportName: 'middleware', line: nodeLine(sourceFile, element) },
|
|
1546
|
+
metadata,
|
|
1547
|
+
})
|
|
1548
|
+
index += 1
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function extractSetupContract(
|
|
1555
|
+
moduleId: string,
|
|
1556
|
+
setupFilePath: string | null,
|
|
1557
|
+
sourcePath: string | null,
|
|
1558
|
+
): ModuleOwnedContractFact | null {
|
|
1559
|
+
if (!setupFilePath || !sourcePath) return null
|
|
1560
|
+
const sourceFile = readSourceFile(setupFilePath)
|
|
1561
|
+
if (!sourceFile) return null
|
|
1562
|
+
const setupObject =
|
|
1563
|
+
findObjectLiteralDeclaration(sourceFile, 'setup') ??
|
|
1564
|
+
(() => {
|
|
1565
|
+
const defaultExport = findDefaultExportExpression(sourceFile)
|
|
1566
|
+
if (!defaultExport) return null
|
|
1567
|
+
const unwrapped = unwrapExpression(defaultExport)
|
|
1568
|
+
return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : null
|
|
1569
|
+
})()
|
|
1570
|
+
if (!setupObject) return null
|
|
1571
|
+
const hooks = readMethodBearingPropertyNames(setupObject)
|
|
1572
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1573
|
+
if (hooks.length > 0) metadata.hooks = hooks
|
|
1574
|
+
const roleFeatures = getObjectPropertyInitializer(setupObject, 'defaultRoleFeatures')
|
|
1575
|
+
if (roleFeatures && ts.isObjectLiteralExpression(roleFeatures)) {
|
|
1576
|
+
const roles = readObjectKeyNames(roleFeatures)
|
|
1577
|
+
if (roles.length > 0) metadata.roles = roles
|
|
1578
|
+
}
|
|
1579
|
+
const customerRoleFeatures = getObjectPropertyInitializer(setupObject, 'defaultCustomerRoleFeatures')
|
|
1580
|
+
if (customerRoleFeatures && ts.isObjectLiteralExpression(customerRoleFeatures)) {
|
|
1581
|
+
const customerRoles = readObjectKeyNames(customerRoleFeatures)
|
|
1582
|
+
if (customerRoles.length > 0) metadata.customerRoles = customerRoles
|
|
1583
|
+
}
|
|
1584
|
+
return {
|
|
1585
|
+
kind: 'setup',
|
|
1586
|
+
id: `${moduleId}:setup`,
|
|
1587
|
+
source: { sourcePath, exportName: 'setup', line: nodeLine(sourceFile, setupObject) },
|
|
1588
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function extractEncryptionContracts(
|
|
1593
|
+
encryptionFilePath: string | null,
|
|
1594
|
+
sourcePath: string | null,
|
|
1595
|
+
): ModuleOwnedContractFact[] {
|
|
1596
|
+
if (!encryptionFilePath || !sourcePath) return []
|
|
1597
|
+
const sourceFile = readSourceFile(encryptionFilePath)
|
|
1598
|
+
if (!sourceFile) return []
|
|
1599
|
+
const mapsArray =
|
|
1600
|
+
findArrayLiteralDeclaration(sourceFile, 'defaultEncryptionMaps') ??
|
|
1601
|
+
findArrayLiteralDeclaration(sourceFile, 'encryptionMaps')
|
|
1602
|
+
if (!mapsArray) return []
|
|
1603
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1604
|
+
for (const element of mapsArray.elements) {
|
|
1605
|
+
if (!ts.isObjectLiteralExpression(element)) continue
|
|
1606
|
+
const entityId = readStringPropertyInitializer(element, 'entityId')
|
|
1607
|
+
if (!entityId) continue
|
|
1608
|
+
const fieldsInitializer = getObjectPropertyInitializer(element, 'fields')
|
|
1609
|
+
const fields: string[] = []
|
|
1610
|
+
if (fieldsInitializer) {
|
|
1611
|
+
const fieldsArray = unwrapArrayLiteral(fieldsInitializer)
|
|
1612
|
+
if (fieldsArray) {
|
|
1613
|
+
for (const fieldElement of fieldsArray.elements) {
|
|
1614
|
+
if (!ts.isObjectLiteralExpression(fieldElement)) continue
|
|
1615
|
+
const field = readStringPropertyInitializer(fieldElement, 'field')
|
|
1616
|
+
if (field) fields.push(field)
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
facts.push({
|
|
1621
|
+
kind: 'encryption',
|
|
1622
|
+
id: entityId,
|
|
1623
|
+
source: { sourcePath, exportName: 'defaultEncryptionMaps', line: nodeLine(sourceFile, element) },
|
|
1624
|
+
...(fields.length > 0 ? { metadata: { fields: fields.sort((left, right) => left.localeCompare(right)) } } : {}),
|
|
1625
|
+
})
|
|
1626
|
+
}
|
|
1627
|
+
return dedupeOwnedContractFacts(facts)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function extractCustomEntityContracts(
|
|
1631
|
+
ceFilePath: string | null,
|
|
1632
|
+
sourcePath: string | null,
|
|
1633
|
+
): ModuleOwnedContractFact[] {
|
|
1634
|
+
if (!ceFilePath || !sourcePath) return []
|
|
1635
|
+
const entityIds = [...collectCustomFieldEntityIds(ceFilePath)].sort((left, right) => left.localeCompare(right))
|
|
1636
|
+
return entityIds.map((id) => ({
|
|
1637
|
+
kind: 'custom-entity' as const,
|
|
1638
|
+
id,
|
|
1639
|
+
source: { sourcePath, exportName: 'default' },
|
|
1640
|
+
}))
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Keyed AI override exports. Agent *extensions* (`aiAgentExtensions`) patch an agent
|
|
1645
|
+
* additively and belong to the `ai.extensions` array; these two replace or disable an
|
|
1646
|
+
* entry by key and map to exact `ai.agents.<id>` / `ai.tools.<id>` override targets.
|
|
1647
|
+
*/
|
|
1648
|
+
const AI_OVERRIDE_EXPORTS = [
|
|
1649
|
+
{ exportName: 'aiAgentOverrides', target: 'agent' },
|
|
1650
|
+
{ exportName: 'aiToolOverrides', target: 'tool' },
|
|
1651
|
+
] as const
|
|
1652
|
+
|
|
1653
|
+
function extractAiExtensionContracts(
|
|
1654
|
+
moduleRoot: string,
|
|
1655
|
+
sourceRoot: string,
|
|
1656
|
+
): ModuleOwnedContractFact[] {
|
|
1657
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1658
|
+
const sources: Array<{ basename: string; relativeDir: string }> = [
|
|
1659
|
+
{ basename: 'ai-agents', relativeDir: '.' },
|
|
1660
|
+
{ basename: 'ai-tools', relativeDir: '.' },
|
|
1661
|
+
{ basename: 'ai-agents.extensions', relativeDir: '.' },
|
|
1662
|
+
{ basename: 'ai-tools.extensions', relativeDir: '.' },
|
|
1663
|
+
]
|
|
1664
|
+
for (const entry of sources) {
|
|
1665
|
+
const filePath = resolveConventionFile(path.join(moduleRoot, entry.relativeDir), entry.basename)
|
|
1666
|
+
if (!filePath) continue
|
|
1667
|
+
const sourceFile = readSourceFile(filePath)
|
|
1668
|
+
if (!sourceFile) continue
|
|
1669
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1670
|
+
const extensionsArray = findArrayLiteralDeclaration(sourceFile, 'aiAgentExtensions')
|
|
1671
|
+
if (extensionsArray) {
|
|
1672
|
+
for (const element of extensionsArray.elements) {
|
|
1673
|
+
const objectLiteral = ts.isObjectLiteralExpression(element)
|
|
1674
|
+
? element
|
|
1675
|
+
: ts.isCallExpression(element) && element.arguments[0] && ts.isObjectLiteralExpression(element.arguments[0])
|
|
1676
|
+
? element.arguments[0]
|
|
1677
|
+
: null
|
|
1678
|
+
if (!objectLiteral) continue
|
|
1679
|
+
const targetAgentId = readStringPropertyInitializer(objectLiteral, 'targetAgentId')
|
|
1680
|
+
if (!targetAgentId) continue
|
|
1681
|
+
facts.push({
|
|
1682
|
+
kind: 'ai-extension',
|
|
1683
|
+
id: `agent-extension:${targetAgentId}`,
|
|
1684
|
+
source: { sourcePath, exportName: 'aiAgentExtensions', line: nodeLine(sourceFile, objectLiteral) },
|
|
1685
|
+
metadata: { target: 'agent', mode: 'extension', targetAgentId },
|
|
1686
|
+
})
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
// `aiAgentOverrides` / `aiToolOverrides` are keyed replace-or-disable maps; only
|
|
1690
|
+
// their keys are contract facts. Values (agent/tool definitions, `null` disables)
|
|
1691
|
+
// are never read or serialized.
|
|
1692
|
+
for (const { exportName, target } of AI_OVERRIDE_EXPORTS) {
|
|
1693
|
+
const overridesObject = findObjectLiteralDeclaration(sourceFile, exportName)
|
|
1694
|
+
if (!overridesObject) continue
|
|
1695
|
+
for (const property of overridesObject.properties) {
|
|
1696
|
+
const overrideId = getPropertyName(property)
|
|
1697
|
+
if (!overrideId) continue
|
|
1698
|
+
facts.push({
|
|
1699
|
+
kind: 'ai-extension',
|
|
1700
|
+
id: `${target}-override:${overrideId}`,
|
|
1701
|
+
source: { sourcePath, exportName, line: nodeLine(sourceFile, property) },
|
|
1702
|
+
metadata: { target, mode: 'override', overrideKey: overrideId },
|
|
1703
|
+
})
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
return dedupeOwnedContractFacts(facts)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function extractGeneratorPluginContracts(
|
|
1711
|
+
moduleRoot: string,
|
|
1712
|
+
sourceRoot: string,
|
|
1713
|
+
): { facts: ModuleOwnedContractFact[]; unresolved: ModuleFactDiagnostic[] } {
|
|
1714
|
+
const filePath = resolveConventionFile(moduleRoot, 'generators')
|
|
1715
|
+
if (!filePath) return { facts: [], unresolved: [] }
|
|
1716
|
+
const sourceFile = readSourceFile(filePath)
|
|
1717
|
+
if (!sourceFile) return { facts: [], unresolved: [] }
|
|
1718
|
+
const pluginsArray = findArrayLiteralDeclaration(sourceFile, 'generatorPlugins')
|
|
1719
|
+
if (!pluginsArray) return { facts: [], unresolved: [] }
|
|
1720
|
+
const sourcePath = toPortableSourcePath(moduleRoot, sourceRoot, filePath)
|
|
1721
|
+
const initializers = buildVariableInitializerMap(sourceFile)
|
|
1722
|
+
const facts: ModuleOwnedContractFact[] = []
|
|
1723
|
+
const unresolved: ModuleFactDiagnostic[] = []
|
|
1724
|
+
let index = 0
|
|
1725
|
+
for (const element of pluginsArray.elements) {
|
|
1726
|
+
const objectLiteral = resolveToObjectLiteral(element, initializers)
|
|
1727
|
+
if (!objectLiteral) {
|
|
1728
|
+
index += 1
|
|
1729
|
+
continue
|
|
1730
|
+
}
|
|
1731
|
+
const pluginId = readStringPropertyInitializer(objectLiteral, 'id')
|
|
1732
|
+
if (!pluginId) {
|
|
1733
|
+
unresolved.push({
|
|
1734
|
+
code: 'unresolved-static-contract',
|
|
1735
|
+
kind: 'generator-plugin',
|
|
1736
|
+
id: `${sourcePath}#${index}`,
|
|
1737
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1738
|
+
})
|
|
1739
|
+
index += 1
|
|
1740
|
+
continue
|
|
1741
|
+
}
|
|
1742
|
+
const metadata: Record<string, ModuleFactSafeScalar | ModuleFactSafeScalar[]> = {}
|
|
1743
|
+
const conventionFile = readStringPropertyInitializer(objectLiteral, 'conventionFile')
|
|
1744
|
+
if (conventionFile) metadata.conventionFile = conventionFile
|
|
1745
|
+
const outputFileName = readStringPropertyInitializer(objectLiteral, 'outputFileName')
|
|
1746
|
+
if (outputFileName) metadata.outputFileName = outputFileName
|
|
1747
|
+
facts.push({
|
|
1748
|
+
kind: 'generator-plugin',
|
|
1749
|
+
id: pluginId,
|
|
1750
|
+
source: { sourcePath, exportName: 'generatorPlugins', line: nodeLine(sourceFile, objectLiteral) },
|
|
1751
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
1752
|
+
})
|
|
1753
|
+
index += 1
|
|
1754
|
+
}
|
|
1755
|
+
return { facts: dedupeOwnedContractFacts(facts), unresolved }
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function compareSourceRefs(left: ModuleFactSourceRef, right: ModuleFactSourceRef): number {
|
|
1759
|
+
return (
|
|
1760
|
+
left.sourcePath.localeCompare(right.sourcePath) ||
|
|
1761
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
1762
|
+
(left.exportName ?? '').localeCompare(right.exportName ?? '')
|
|
1763
|
+
)
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function sameSourceRef(left: ModuleFactSourceRef, right: ModuleFactSourceRef): boolean {
|
|
1767
|
+
return (
|
|
1768
|
+
left.sourcePath === right.sourcePath &&
|
|
1769
|
+
(left.line ?? null) === (right.line ?? null) &&
|
|
1770
|
+
(left.exportName ?? null) === (right.exportName ?? null)
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Every proven `(kind, id)` reaches `factSources`. Facts whose declaration site is already
|
|
1775
|
+
// serialized inline (pages, CLI commands, routes, AI tools/agents, reused contributions) and
|
|
1776
|
+
// the owned families (which carry `source` inside `ownedContracts`) are emitted as a typed
|
|
1777
|
+
// `factRef` pointer instead of a duplicated source ref, so the index stays uniform without
|
|
1778
|
+
// copying provenance payloads past the determinism byte guard. Duplicate detection runs over
|
|
1779
|
+
// ALL candidates regardless of which projection they take.
|
|
1780
|
+
type ProvenanceCandidate = {
|
|
1781
|
+
kind: ModuleFactSourceKind
|
|
1782
|
+
id: string
|
|
1783
|
+
source?: ModuleFactSourceRef
|
|
1784
|
+
factRef?: ModuleFactRef
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function provenanceCandidateOrderKey(candidate: ProvenanceCandidate): string {
|
|
1788
|
+
if (candidate.source) {
|
|
1789
|
+
return JSON.stringify([
|
|
1790
|
+
0,
|
|
1791
|
+
candidate.source.sourcePath,
|
|
1792
|
+
candidate.source.line ?? 0,
|
|
1793
|
+
candidate.source.exportName ?? '',
|
|
1794
|
+
])
|
|
1795
|
+
}
|
|
1796
|
+
return JSON.stringify([1, candidate.factRef?.factSection ?? '', candidate.factRef?.factKey ?? ''])
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function buildProvenanceIndex(candidates: readonly ProvenanceCandidate[]): {
|
|
1800
|
+
factSources: ModuleFactSourceEntry[]
|
|
1801
|
+
diagnostics: ModuleFactDiagnostic[]
|
|
1802
|
+
} {
|
|
1803
|
+
const grouped = new Map<string, ProvenanceCandidate[]>()
|
|
1804
|
+
for (const candidate of candidates) {
|
|
1805
|
+
const key = JSON.stringify([candidate.kind, candidate.id])
|
|
1806
|
+
const group = grouped.get(key) ?? []
|
|
1807
|
+
group.push(candidate)
|
|
1808
|
+
grouped.set(key, group)
|
|
1809
|
+
}
|
|
1810
|
+
// Keep the provenance index compact: sourcePath is the provenance and (kind,id) the
|
|
1811
|
+
// stable identity, so exportName and line (recoverable from source) are dropped here.
|
|
1812
|
+
// Owned families retain the richer source ref inside `ownedContracts`.
|
|
1813
|
+
const factSources: ModuleFactSourceEntry[] = []
|
|
1814
|
+
const diagnostics: ModuleFactDiagnostic[] = []
|
|
1815
|
+
for (const group of grouped.values()) {
|
|
1816
|
+
const sorted = [...group].sort(
|
|
1817
|
+
(left, right) => provenanceCandidateOrderKey(left).localeCompare(provenanceCandidateOrderKey(right)),
|
|
1818
|
+
)
|
|
1819
|
+
const canonical = sorted[0]
|
|
1820
|
+
factSources.push(
|
|
1821
|
+
canonical.factRef
|
|
1822
|
+
? {
|
|
1823
|
+
kind: canonical.kind,
|
|
1824
|
+
id: canonical.id,
|
|
1825
|
+
factRef: {
|
|
1826
|
+
factSection: canonical.factRef.factSection,
|
|
1827
|
+
...(canonical.factRef.factKey === canonical.id
|
|
1828
|
+
? {}
|
|
1829
|
+
: { factKey: canonical.factRef.factKey }),
|
|
1830
|
+
},
|
|
1831
|
+
}
|
|
1832
|
+
: {
|
|
1833
|
+
kind: canonical.kind,
|
|
1834
|
+
id: canonical.id,
|
|
1835
|
+
source: { sourcePath: (canonical.source as ModuleFactSourceRef).sourcePath },
|
|
1836
|
+
},
|
|
1837
|
+
)
|
|
1838
|
+
const canonicalKey = provenanceCandidateOrderKey(canonical)
|
|
1839
|
+
for (const rejected of sorted.slice(1)) {
|
|
1840
|
+
if (provenanceCandidateOrderKey(rejected) === canonicalKey) continue
|
|
1841
|
+
diagnostics.push({
|
|
1842
|
+
code: 'duplicate-source',
|
|
1843
|
+
kind: rejected.kind,
|
|
1844
|
+
id: rejected.id,
|
|
1845
|
+
...(rejected.source ? { source: rejected.source } : {}),
|
|
1846
|
+
})
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
factSources.sort(
|
|
1850
|
+
(left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id),
|
|
1851
|
+
)
|
|
1852
|
+
diagnostics.sort(
|
|
1853
|
+
(left, right) =>
|
|
1854
|
+
left.code.localeCompare(right.code) ||
|
|
1855
|
+
left.kind.localeCompare(right.kind) ||
|
|
1856
|
+
left.id.localeCompare(right.id) ||
|
|
1857
|
+
compareSourceRefs(left.source ?? { sourcePath: '' }, right.source ?? { sourcePath: '' }),
|
|
1858
|
+
)
|
|
1859
|
+
return { factSources, diagnostics }
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function buildOwnedContracts(
|
|
1863
|
+
families: ReadonlyArray<[ModuleOwnedContractKind, ModuleOwnedContractFact[]]>,
|
|
1864
|
+
): ModuleOwnedContracts | undefined {
|
|
1865
|
+
const result: ModuleOwnedContracts = {}
|
|
1866
|
+
for (const [kind, facts] of families) {
|
|
1867
|
+
if (facts.length === 0) continue
|
|
1868
|
+
// Drop the (constant/redundant) exportName from the emitted source ref; the line
|
|
1869
|
+
// anchor plus sourcePath already pin the declaration and keep the JSON compact.
|
|
1870
|
+
result[kind] = facts.map((fact) => ({
|
|
1871
|
+
...fact,
|
|
1872
|
+
source: {
|
|
1873
|
+
sourcePath: fact.source.sourcePath,
|
|
1874
|
+
...(fact.source.line ? { line: fact.source.line } : {}),
|
|
1875
|
+
},
|
|
1876
|
+
}))
|
|
1877
|
+
}
|
|
1878
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const CONTRIBUTION_KIND_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
1882
|
+
subscriber: 'subscriber',
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const REGISTRY_TO_SOURCE_KIND: Partial<Record<string, ModuleFactSourceKind>> = {
|
|
1886
|
+
vector: 'vector',
|
|
1887
|
+
integration: 'integration',
|
|
1888
|
+
payment: 'integration',
|
|
1889
|
+
shipping: 'integration',
|
|
1890
|
+
currency: 'integration',
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1005
1893
|
export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFacts {
|
|
1006
1894
|
const { moduleId, coreVersion = null, sourcePackage = null, sourceVersion = null } = options
|
|
1007
1895
|
const moduleRoot = options.moduleRoot
|
|
@@ -1023,6 +1911,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1023
1911
|
const notificationsFilePath = resolveConventionFile(moduleRoot, 'notifications')
|
|
1024
1912
|
const cliFilePath = resolveConventionFile(moduleRoot, 'cli')
|
|
1025
1913
|
const indexFilePath = resolveConventionFile(moduleRoot, 'index')
|
|
1914
|
+
const setupFilePath = resolveConventionFile(moduleRoot, 'setup')
|
|
1915
|
+
const encryptionFilePath = resolveConventionFile(moduleRoot, 'encryption')
|
|
1916
|
+
|
|
1917
|
+
const portableOf = (filePath: string | null): string | null =>
|
|
1918
|
+
filePath ? toPortableSourcePath(moduleRoot, sourceRoot, filePath) : null
|
|
1026
1919
|
|
|
1027
1920
|
const warnings: string[] = []
|
|
1028
1921
|
const { title, description } = extractModuleMeta(indexFilePath)
|
|
@@ -1034,7 +1927,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1034
1927
|
const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
|
|
1035
1928
|
const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
|
|
1036
1929
|
const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
|
|
1037
|
-
const
|
|
1930
|
+
const diRegistrations = extractDiRegistrationFacts(diFilePath, portableOf(diFilePath) ?? '')
|
|
1931
|
+
const diTokens = deriveLegacyDiTokens(diRegistrations.facts)
|
|
1932
|
+
for (const legacyToken of extractDiTokens(diFilePath)) {
|
|
1933
|
+
if (!diTokens.includes(legacyToken)) diTokens.push(legacyToken)
|
|
1934
|
+
}
|
|
1038
1935
|
const searchEntities = extractSearchEntities(searchFilePath, warnings)
|
|
1039
1936
|
const notifications = extractNotifications(notificationsFilePath, warnings)
|
|
1040
1937
|
const cli = extractCli(cliFilePath, warnings)
|
|
@@ -1065,7 +1962,203 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1065
1962
|
aiAgents,
|
|
1066
1963
|
})
|
|
1067
1964
|
|
|
1068
|
-
|
|
1965
|
+
const indexSourcePath = portableOf(indexFilePath)
|
|
1966
|
+
const diSourcePath = portableOf(diFilePath)
|
|
1967
|
+
const moduleMetadataFact = extractModuleMetadataContract(indexFilePath, moduleId, indexSourcePath)
|
|
1968
|
+
const commandCandidates = extractCommandContractCandidates(moduleId, moduleRoot, sourceRoot)
|
|
1969
|
+
const commandFacts = dedupeOwnedContractFacts(
|
|
1970
|
+
commandCandidates.map((candidate) => ({
|
|
1971
|
+
kind: 'command' as const,
|
|
1972
|
+
id: candidate.id,
|
|
1973
|
+
source: { sourcePath: candidate.sourcePath },
|
|
1974
|
+
})),
|
|
1975
|
+
)
|
|
1976
|
+
const workers = extractWorkerContracts(moduleId, moduleRoot, sourceRoot)
|
|
1977
|
+
const pageMiddleware = extractPageMiddlewareContracts(moduleRoot, sourceRoot)
|
|
1978
|
+
const setupFact = extractSetupContract(moduleId, setupFilePath, portableOf(setupFilePath))
|
|
1979
|
+
const encryptionFacts = extractEncryptionContracts(encryptionFilePath, portableOf(encryptionFilePath))
|
|
1980
|
+
const customEntityFacts = extractCustomEntityContracts(ceFilePath, portableOf(ceFilePath))
|
|
1981
|
+
const aiExtensionFacts = extractAiExtensionContracts(moduleRoot, sourceRoot)
|
|
1982
|
+
const generatorPlugins = extractGeneratorPluginContracts(moduleRoot, sourceRoot)
|
|
1983
|
+
const diContractFacts = dedupeOwnedContractFacts(
|
|
1984
|
+
diRegistrations.facts.map((fact) => ({
|
|
1985
|
+
kind: 'di-registration' as const,
|
|
1986
|
+
id: fact.token,
|
|
1987
|
+
source: fact.source,
|
|
1988
|
+
metadata: {
|
|
1989
|
+
registrationKind: fact.registrationKind,
|
|
1990
|
+
...(fact.providerSymbol ? { providerSymbol: fact.providerSymbol } : {}),
|
|
1991
|
+
...(fact.lifetime ? { lifetime: fact.lifetime } : {}),
|
|
1992
|
+
...(fact.injectionMode ? { injectionMode: fact.injectionMode } : {}),
|
|
1993
|
+
},
|
|
1994
|
+
})),
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
const ownedContracts = buildOwnedContracts([
|
|
1998
|
+
['module-metadata', moduleMetadataFact ? [moduleMetadataFact] : []],
|
|
1999
|
+
['command', commandFacts],
|
|
2000
|
+
['worker', workers.facts],
|
|
2001
|
+
['page-middleware', pageMiddleware.facts],
|
|
2002
|
+
['setup', setupFact ? [setupFact] : []],
|
|
2003
|
+
['encryption', encryptionFacts],
|
|
2004
|
+
['di-registration', diContractFacts],
|
|
2005
|
+
['custom-entity', customEntityFacts],
|
|
2006
|
+
['ai-extension', aiExtensionFacts],
|
|
2007
|
+
['generator-plugin', generatorPlugins.facts],
|
|
2008
|
+
])
|
|
2009
|
+
|
|
2010
|
+
const provenanceCandidates: ProvenanceCandidate[] = []
|
|
2011
|
+
const pushSource = (kind: ModuleFactSourceKind, id: string, source: ModuleFactSourceRef): void => {
|
|
2012
|
+
provenanceCandidates.push({ kind, id, source })
|
|
2013
|
+
}
|
|
2014
|
+
const pushRef = (
|
|
2015
|
+
kind: ModuleFactSourceKind,
|
|
2016
|
+
id: string,
|
|
2017
|
+
source: ModuleFactSourceRef,
|
|
2018
|
+
factSection: string,
|
|
2019
|
+
factKey: string,
|
|
2020
|
+
): void => {
|
|
2021
|
+
provenanceCandidates.push({ kind, id, source, factRef: { factSection, factKey } })
|
|
2022
|
+
}
|
|
2023
|
+
const pushOwnedRefs = (kind: ModuleOwnedContractKind, facts: readonly ModuleOwnedContractFact[]): void => {
|
|
2024
|
+
for (const fact of facts) pushRef(kind, fact.id, fact.source, `ownedContracts.${kind}`, fact.id)
|
|
2025
|
+
}
|
|
2026
|
+
if (moduleMetadataFact) {
|
|
2027
|
+
pushRef(
|
|
2028
|
+
'module-metadata',
|
|
2029
|
+
moduleId,
|
|
2030
|
+
moduleMetadataFact.source,
|
|
2031
|
+
'ownedContracts.module-metadata',
|
|
2032
|
+
moduleMetadataFact.id,
|
|
2033
|
+
)
|
|
2034
|
+
}
|
|
2035
|
+
if (entitiesFilePath) {
|
|
2036
|
+
const entitiesSourcePath = portableOf(entitiesFilePath) as string
|
|
2037
|
+
for (const entity of entities) {
|
|
2038
|
+
pushSource('entity', entity.id, { sourcePath: entitiesSourcePath, exportName: entity.class })
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (eventsFilePath) {
|
|
2042
|
+
const eventsSourcePath = portableOf(eventsFilePath) as string
|
|
2043
|
+
for (const event of events) pushSource('event', event.id, { sourcePath: eventsSourcePath, exportName: 'events' })
|
|
2044
|
+
}
|
|
2045
|
+
if (aclFilePath) {
|
|
2046
|
+
const aclSourcePath = portableOf(aclFilePath) as string
|
|
2047
|
+
for (const feature of aclFeatures) pushSource('acl-feature', feature, { sourcePath: aclSourcePath, exportName: 'features' })
|
|
2048
|
+
}
|
|
2049
|
+
for (const route of apiRoutes) {
|
|
2050
|
+
if (route.sourcePath) {
|
|
2051
|
+
pushRef('api-route', route.path, { sourcePath: route.sourcePath }, 'apiRoutes', route.path)
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (searchFilePath) {
|
|
2055
|
+
const searchSourcePath = portableOf(searchFilePath) as string
|
|
2056
|
+
for (const entityId of searchEntities) pushSource('search', entityId, { sourcePath: searchSourcePath, exportName: 'searchConfig' })
|
|
2057
|
+
}
|
|
2058
|
+
if (notificationsFilePath) {
|
|
2059
|
+
const notificationsSourcePath = portableOf(notificationsFilePath) as string
|
|
2060
|
+
for (const id of notifications) pushSource('notification', id, { sourcePath: notificationsSourcePath })
|
|
2061
|
+
}
|
|
2062
|
+
for (const command of cliCommands) {
|
|
2063
|
+
pushRef('cli-command', command.command, { sourcePath: command.sourcePath }, 'cliCommands', command.command)
|
|
2064
|
+
}
|
|
2065
|
+
for (const page of backendPages) {
|
|
2066
|
+
pushRef('backend-page', page.path, { sourcePath: page.sourcePath }, 'backendPages', page.path)
|
|
2067
|
+
}
|
|
2068
|
+
for (const page of frontendPages) {
|
|
2069
|
+
pushRef('frontend-page', page.path, { sourcePath: page.sourcePath }, 'frontendPages', page.path)
|
|
2070
|
+
}
|
|
2071
|
+
for (const tool of aiTools) pushRef('ai-tool', tool.name, { sourcePath: tool.sourcePath }, 'aiTools', tool.name)
|
|
2072
|
+
for (const agent of aiAgents) pushRef('ai-agent', agent.id, { sourcePath: agent.sourcePath }, 'aiAgents', agent.id)
|
|
2073
|
+
for (const candidate of commandCandidates) {
|
|
2074
|
+
pushRef('command', candidate.id, { sourcePath: candidate.sourcePath }, 'ownedContracts.command', candidate.id)
|
|
2075
|
+
}
|
|
2076
|
+
pushOwnedRefs('worker', workers.facts)
|
|
2077
|
+
pushOwnedRefs('page-middleware', pageMiddleware.facts)
|
|
2078
|
+
if (setupFact) pushOwnedRefs('setup', [setupFact])
|
|
2079
|
+
pushOwnedRefs('encryption', encryptionFacts)
|
|
2080
|
+
for (const fact of diRegistrations.facts) {
|
|
2081
|
+
pushRef('di-registration', fact.token, fact.source, 'ownedContracts.di-registration', fact.token)
|
|
2082
|
+
}
|
|
2083
|
+
pushOwnedRefs('custom-entity', customEntityFacts)
|
|
2084
|
+
pushOwnedRefs('ai-extension', aiExtensionFacts)
|
|
2085
|
+
pushOwnedRefs('generator-plugin', generatorPlugins.facts)
|
|
2086
|
+
const contributionRef = (contributionId: string): { factSection: string; factKey: string } => ({
|
|
2087
|
+
factSection: 'extensionSurfaces.contributions',
|
|
2088
|
+
factKey: contributionId,
|
|
2089
|
+
})
|
|
2090
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2091
|
+
const contributionSource: ModuleFactSourceRef = {
|
|
2092
|
+
sourcePath: contribution.source.path,
|
|
2093
|
+
...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
|
|
2094
|
+
}
|
|
2095
|
+
const ref = contributionRef(contribution.id)
|
|
2096
|
+
if (contribution.kind === 'subscriber') {
|
|
2097
|
+
pushRef('subscriber', contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2098
|
+
continue
|
|
2099
|
+
}
|
|
2100
|
+
if (contribution.kind === 'specialized-registry') {
|
|
2101
|
+
const details = contribution.details as { registry?: string; registryId?: string }
|
|
2102
|
+
const sourceKind = details.registry ? REGISTRY_TO_SOURCE_KIND[details.registry] : undefined
|
|
2103
|
+
if (sourceKind) {
|
|
2104
|
+
pushRef(
|
|
2105
|
+
sourceKind,
|
|
2106
|
+
details.registryId ?? contribution.id,
|
|
2107
|
+
contributionSource,
|
|
2108
|
+
ref.factSection,
|
|
2109
|
+
ref.factKey,
|
|
2110
|
+
)
|
|
2111
|
+
}
|
|
2112
|
+
continue
|
|
2113
|
+
}
|
|
2114
|
+
const mapped = CONTRIBUTION_KIND_TO_SOURCE_KIND[contribution.kind]
|
|
2115
|
+
if (mapped) pushRef(mapped, contribution.id, contributionSource, ref.factSection, ref.factKey)
|
|
2116
|
+
}
|
|
2117
|
+
// extension-host / extension-contribution provenance is emitted as typed `factRef` pointers
|
|
2118
|
+
// rather than duplicated source refs: those facts already carry their own `source` inside
|
|
2119
|
+
// `extensionSurfaces.{hosts,contributions}`, and copying every declared host spot (hundreds
|
|
2120
|
+
// per large module) would push the shared JSON past the determinism byte guard asserted in
|
|
2121
|
+
// `module-facts.bc-guard.test.ts`.
|
|
2122
|
+
// Hosts are indexed by their registry `key`: `id` repeats across families (an entity id is
|
|
2123
|
+
// also an api-entity id), while `key` is unique inside a module.
|
|
2124
|
+
for (const host of extensionSurfaces.hosts) {
|
|
2125
|
+
provenanceCandidates.push({
|
|
2126
|
+
kind: 'extension-host',
|
|
2127
|
+
id: host.key,
|
|
2128
|
+
...(host.source.kind === 'fact-ref' ? {} : { source: { sourcePath: host.source.path } }),
|
|
2129
|
+
factRef: { factSection: 'extensionSurfaces.hosts', factKey: host.key },
|
|
2130
|
+
})
|
|
2131
|
+
}
|
|
2132
|
+
for (const contribution of extensionSurfaces.contributions) {
|
|
2133
|
+
pushRef(
|
|
2134
|
+
'extension-contribution',
|
|
2135
|
+
contribution.id,
|
|
2136
|
+
{ sourcePath: contribution.source.path },
|
|
2137
|
+
'extensionSurfaces.contributions',
|
|
2138
|
+
contribution.id,
|
|
2139
|
+
)
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const provenance = buildProvenanceIndex(provenanceCandidates)
|
|
2143
|
+
const factDiagnostics = [
|
|
2144
|
+
...provenance.diagnostics,
|
|
2145
|
+
...workers.unresolved,
|
|
2146
|
+
...pageMiddleware.unresolved,
|
|
2147
|
+
...generatorPlugins.unresolved,
|
|
2148
|
+
...diRegistrations.unresolvedTokens.map((token): ModuleFactDiagnostic => ({
|
|
2149
|
+
code: 'unresolved-static-contract',
|
|
2150
|
+
kind: 'di-registration',
|
|
2151
|
+
id: token,
|
|
2152
|
+
...(diSourcePath ? { source: { sourcePath: diSourcePath, exportName: token } } : {}),
|
|
2153
|
+
})),
|
|
2154
|
+
].sort(
|
|
2155
|
+
(left, right) =>
|
|
2156
|
+
left.code.localeCompare(right.code) ||
|
|
2157
|
+
left.kind.localeCompare(right.kind) ||
|
|
2158
|
+
left.id.localeCompare(right.id),
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
const facts: ModuleFacts = {
|
|
1069
2162
|
module: moduleId,
|
|
1070
2163
|
title,
|
|
1071
2164
|
description,
|
|
@@ -1088,8 +2181,20 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
|
|
|
1088
2181
|
aiTools,
|
|
1089
2182
|
aiAgents,
|
|
1090
2183
|
extensionSurfaces,
|
|
2184
|
+
...(provenance.factSources.length > 0 ? { factSources: provenance.factSources } : {}),
|
|
2185
|
+
...(ownedContracts ? { ownedContracts } : {}),
|
|
2186
|
+
...(factDiagnostics.length > 0 ? { factDiagnostics } : {}),
|
|
1091
2187
|
warnings,
|
|
1092
2188
|
}
|
|
2189
|
+
|
|
2190
|
+
// Exact unified override targets (spec 2026-08-02-module-facts-exact-override-targets).
|
|
2191
|
+
// Adapters consume the fully-populated owned facts + contributions above.
|
|
2192
|
+
const overrides = collectModuleOverrideTargets(facts, buildFactSourceLookup(facts))
|
|
2193
|
+
if (overrides.overrideTargets.length > 0) facts.overrideTargets = overrides.overrideTargets
|
|
2194
|
+
if (overrides.overrideTargetDiagnostics.length > 0) {
|
|
2195
|
+
facts.overrideTargetDiagnostics = overrides.overrideTargetDiagnostics
|
|
2196
|
+
}
|
|
2197
|
+
return facts
|
|
1093
2198
|
}
|
|
1094
2199
|
|
|
1095
2200
|
export interface ModuleFactsJsonEvent {
|
|
@@ -1122,6 +2227,11 @@ export interface ModuleFactsJsonEntry {
|
|
|
1122
2227
|
aiTools: ModuleAiToolFact[]
|
|
1123
2228
|
aiAgents: ModuleAiAgentFact[]
|
|
1124
2229
|
extensionSurfaces?: ModuleExtensionSurfaceFacts
|
|
2230
|
+
factSources?: ModuleFactSourceEntry[]
|
|
2231
|
+
ownedContracts?: ModuleOwnedContracts
|
|
2232
|
+
factDiagnostics?: ModuleFactDiagnostic[]
|
|
2233
|
+
overrideTargets?: ModuleOverrideTarget[]
|
|
2234
|
+
overrideTargetDiagnostics?: ModuleOverrideTargetDiagnostic[]
|
|
1125
2235
|
}
|
|
1126
2236
|
|
|
1127
2237
|
const EMPTY_SECTION_MARKER = '_none_'
|
|
@@ -1138,26 +2248,26 @@ function renderVersionStamp(
|
|
|
1138
2248
|
return `<!-- generated from @open-mercato/core ${version} — R1 staleness stamp -->`
|
|
1139
2249
|
}
|
|
1140
2250
|
|
|
1141
|
-
function renderEntitiesSection(entities: ModuleEntityFact[]): string {
|
|
2251
|
+
function renderEntitiesSection(entities: ModuleEntityFact[], lookup: FactSourceLookup): string {
|
|
1142
2252
|
if (entities.length === 0) return `## Entities\n\n${EMPTY_SECTION_MARKER}`
|
|
1143
|
-
const header = '| Entity ID | Class | Table | Editable | CustomFields |'
|
|
1144
|
-
const divider = '
|
|
2253
|
+
const header = '| Entity ID | Class | Table | Editable | CustomFields | Source |'
|
|
2254
|
+
const divider = '|---|---|---|---|---|---|'
|
|
1145
2255
|
const rows = entities.map(
|
|
1146
2256
|
(entity) =>
|
|
1147
|
-
`| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} |`,
|
|
2257
|
+
`| ${entity.id} | ${entity.class} | ${entity.table} | ${entity.editable ? 'yes' : 'no'} | ${entity.customFields ? 'yes' : 'no'} | ${renderLookedUpSource(lookup, 'entity', entity.id)} |`,
|
|
1148
2258
|
)
|
|
1149
2259
|
return ['## Entities', '', header, divider, ...rows].join('\n')
|
|
1150
2260
|
}
|
|
1151
2261
|
|
|
1152
|
-
function renderEventsSection(events: ModuleEventFact[]): string {
|
|
2262
|
+
function renderEventsSection(events: ModuleEventFact[], lookup: FactSourceLookup): string {
|
|
1153
2263
|
const heading = `## Events (${events.length})`
|
|
1154
2264
|
if (events.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
1155
|
-
const header = '| ID | Category | Entity | Browser transport |'
|
|
1156
|
-
const divider = '
|
|
2265
|
+
const header = '| ID | Category | Entity | Browser transport | Source |'
|
|
2266
|
+
const divider = '|---|---|---|---|---|'
|
|
1157
2267
|
const rows = events.map((event) => {
|
|
1158
2268
|
const transports = [event.clientBroadcast ? 'client' : null, event.portalBroadcast ? 'portal' : null]
|
|
1159
2269
|
.filter((value): value is string => value !== null)
|
|
1160
|
-
return `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} | ${transports.join(', ') || '—'} |`
|
|
2270
|
+
return `| ${event.id} | ${event.category ?? '—'} | ${event.entity ?? '—'} | ${transports.join(', ') || '—'} | ${renderLookedUpSource(lookup, 'event', event.id)} |`
|
|
1161
2271
|
})
|
|
1162
2272
|
return [heading, '', header, divider, ...rows].join('\n')
|
|
1163
2273
|
}
|
|
@@ -1167,10 +2277,73 @@ function renderInlineListSection(heading: string, values: string[]): string {
|
|
|
1167
2277
|
return `${heading}\n\n${values.join(' · ')}`
|
|
1168
2278
|
}
|
|
1169
2279
|
|
|
2280
|
+
function renderSourceLinkedListSection(
|
|
2281
|
+
heading: string,
|
|
2282
|
+
values: readonly string[],
|
|
2283
|
+
lookup: FactSourceLookup,
|
|
2284
|
+
kind: ModuleFactSourceKind,
|
|
2285
|
+
idColumnLabel: string,
|
|
2286
|
+
): string {
|
|
2287
|
+
if (values.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
|
|
2288
|
+
const rows = values.map((value) => `| ${value} | ${renderLookedUpSource(lookup, kind, value)} |`)
|
|
2289
|
+
return [heading, '', `| ${idColumnLabel} | Source |`, '|---|---|', ...rows].join('\n')
|
|
2290
|
+
}
|
|
2291
|
+
|
|
1170
2292
|
function renderSourceLink(sourcePath: string): string {
|
|
1171
2293
|
return `[${sourcePath}](../../../${sourcePath})`
|
|
1172
2294
|
}
|
|
1173
2295
|
|
|
2296
|
+
function renderSourceRefLink(source: ModuleFactSourceRef): string {
|
|
2297
|
+
const anchor = source.line ? `#L${source.line}` : ''
|
|
2298
|
+
const label = source.line ? `${source.sourcePath}:${source.line}` : source.sourcePath
|
|
2299
|
+
return `[${label}](../../../${source.sourcePath}${anchor})`
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
function renderLookedUpSource(lookup: FactSourceLookup, kind: ModuleFactSourceKind, id: string): string {
|
|
2304
|
+
const source = lookup(kind, id)
|
|
2305
|
+
return source ? renderSourceRefLink(source) : '—'
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function renderSafeScalar(value: ModuleFactSafeScalar | ModuleFactSafeScalar[] | undefined): string {
|
|
2309
|
+
if (value === undefined || value === null) return '—'
|
|
2310
|
+
if (Array.isArray(value)) return value.length > 0 ? value.map((entry) => String(entry)).join(', ') : '—'
|
|
2311
|
+
return String(value)
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function renderOwnedContractMetadata(metadata: ModuleOwnedContractFact['metadata']): string {
|
|
2315
|
+
if (!metadata) return '—'
|
|
2316
|
+
const keys = Object.keys(metadata).sort((left, right) => left.localeCompare(right))
|
|
2317
|
+
if (keys.length === 0) return '—'
|
|
2318
|
+
return keys.map((key) => `${key}=${renderSafeScalar(metadata[key])}`).join('; ')
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
function renderOwnedContractSection(
|
|
2322
|
+
heading: string,
|
|
2323
|
+
facts: readonly ModuleOwnedContractFact[] | undefined,
|
|
2324
|
+
): string {
|
|
2325
|
+
if (!facts || facts.length === 0) return ''
|
|
2326
|
+
const rows = facts.map(
|
|
2327
|
+
(fact) => `| ${fact.id} | ${renderOwnedContractMetadata(fact.metadata)} | ${renderSourceRefLink(fact.source)} |`,
|
|
2328
|
+
)
|
|
2329
|
+
return [heading, '', '| ID | Metadata | Source |', '|---|---|---|', ...rows].join('\n')
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
function renderDiRegistrationsSection(facts: readonly ModuleOwnedContractFact[] | undefined): string {
|
|
2333
|
+
if (!facts || facts.length === 0) return ''
|
|
2334
|
+
const rows = facts.map((fact) => {
|
|
2335
|
+
const metadata = fact.metadata ?? {}
|
|
2336
|
+
return `| ${fact.id} | ${renderSafeScalar(metadata.registrationKind)} | ${renderSafeScalar(metadata.lifetime)} | ${renderSafeScalar(metadata.injectionMode)} | ${renderSafeScalar(metadata.providerSymbol)} | ${renderSourceRefLink(fact.source)} |`
|
|
2337
|
+
})
|
|
2338
|
+
return [
|
|
2339
|
+
'## DI registrations (rich)',
|
|
2340
|
+
'',
|
|
2341
|
+
'| Token | Kind | Lifetime | Injection | Provider | Source |',
|
|
2342
|
+
'|---|---|---|---|---|---|',
|
|
2343
|
+
...rows,
|
|
2344
|
+
].join('\n')
|
|
2345
|
+
}
|
|
2346
|
+
|
|
1174
2347
|
function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
|
|
1175
2348
|
if (!rule) return 'public'
|
|
1176
2349
|
if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
|
|
@@ -1220,17 +2393,22 @@ function renderExtensionHostContext(host: ModuleExtensionSurfaceFacts['hosts'][n
|
|
|
1220
2393
|
return host.contextContract ?? host.runtimeContract ?? host.scopeContract ?? '—'
|
|
1221
2394
|
}
|
|
1222
2395
|
|
|
2396
|
+
function renderExtensionHostSource(host: ModuleExtensionHostFact): string {
|
|
2397
|
+
if (host.source.kind === 'fact-ref') return `${host.source.factSection}:${host.source.factKey}`
|
|
2398
|
+
return renderSourceRefLink({ sourcePath: host.source.path })
|
|
2399
|
+
}
|
|
2400
|
+
|
|
1223
2401
|
function renderExtensionHostsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
1224
2402
|
const boundHosts = extensionSurfaces.hosts.filter((host) => host.bound)
|
|
1225
2403
|
if (boundHosts.length === 0) return `## UMES hosts\n\n${EMPTY_SECTION_MARKER}`
|
|
1226
2404
|
const rows = boundHosts.map((host) =>
|
|
1227
|
-
`| ${host.id} | ${host.family} | ${host.capabilities.join(', ') || '—'} | ${renderExtensionHostContext(host)} | ${host.stability.toUpperCase()} |`,
|
|
2405
|
+
`| ${host.id} | ${host.family} | ${host.capabilities.join(', ') || '—'} | ${renderExtensionHostContext(host)} | ${host.stability.toUpperCase()} | ${renderExtensionHostSource(host)} |`,
|
|
1228
2406
|
)
|
|
1229
2407
|
return [
|
|
1230
2408
|
'## UMES hosts',
|
|
1231
2409
|
'',
|
|
1232
|
-
'| ID / pattern | Family | Supports | Context | Stability |',
|
|
1233
|
-
'
|
|
2410
|
+
'| ID / pattern | Family | Supports | Context | Stability | Source |',
|
|
2411
|
+
'|---|---|---|---|---|---|',
|
|
1234
2412
|
...rows,
|
|
1235
2413
|
].join('\n')
|
|
1236
2414
|
}
|
|
@@ -1258,13 +2436,14 @@ function renderExtensionContributionsSection(extensionSurfaces: ModuleExtensionS
|
|
|
1258
2436
|
const targets = contribution.targets.map((entry) => entry.id).join(', ')
|
|
1259
2437
|
const resolution = contribution.targets.map((entry) => entry.resolution).join(', ')
|
|
1260
2438
|
const phases = [...(contribution.phases ?? []), ...(contribution.operations ?? [])].join(', ') || '—'
|
|
1261
|
-
|
|
2439
|
+
const source = renderSourceRefLink({ sourcePath: contribution.source.path })
|
|
2440
|
+
return `| ${contribution.id} | ${contribution.kind} | ${targets || '—'} | ${phases} | ${compactContributionDetails(contribution)} | ${resolution || '—'} | ${source} |`
|
|
1262
2441
|
})
|
|
1263
2442
|
return [
|
|
1264
2443
|
'## UMES contributions',
|
|
1265
2444
|
'',
|
|
1266
|
-
'| ID | Kind | Target | Phase / operations | Contract | Resolution |',
|
|
1267
|
-
'
|
|
2445
|
+
'| ID | Kind | Target | Phase / operations | Contract | Resolution | Source |',
|
|
2446
|
+
'|---|---|---|---|---|---|---|',
|
|
1268
2447
|
...rows,
|
|
1269
2448
|
].join('\n')
|
|
1270
2449
|
}
|
|
@@ -1279,18 +2458,109 @@ function renderExtensionDiagnosticsSection(extensionSurfaces: ModuleExtensionSur
|
|
|
1279
2458
|
return ['## UMES diagnostics', '', ...diagnostics].join('\n')
|
|
1280
2459
|
}
|
|
1281
2460
|
|
|
2461
|
+
function renderExtensionTargetRef(target: ModuleExtensionTargetRef): string {
|
|
2462
|
+
const method = target.method ? ` ${target.method}` : ''
|
|
2463
|
+
const owner = target.moduleId ? ` @${target.moduleId}` : ''
|
|
2464
|
+
return `${target.kind}:${target.id}${method}${owner}`
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function renderActivationBindingsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2468
|
+
const activations = extensionSurfaces.activations ?? []
|
|
2469
|
+
if (activations.length === 0) return `## Active extension bindings\n\n${EMPTY_SECTION_MARKER}`
|
|
2470
|
+
const rows = activations.map((activation) => {
|
|
2471
|
+
const phases = activation.phases && activation.phases.length > 0 ? activation.phases.join(', ') : '—'
|
|
2472
|
+
const source = activation.source ? renderSourceRefLink(activation.source) : '—'
|
|
2473
|
+
const bridge = activation.bridge ? `${activation.bridge.factSection}:${activation.bridge.factKey}` : '—'
|
|
2474
|
+
return `| ${activation.id} | ${activation.kind} | ${renderExtensionTargetRef(activation.host)} | ${activation.contributionKinds.join(', ') || '—'} | ${phases} | ${bridge} | ${source} |`
|
|
2475
|
+
})
|
|
2476
|
+
return [
|
|
2477
|
+
'## Active extension bindings',
|
|
2478
|
+
'',
|
|
2479
|
+
'| Activation | Kind | Host | Contribution kinds | Phases | Bridge | Source |',
|
|
2480
|
+
'|---|---|---|---|---|---|---|',
|
|
2481
|
+
...rows,
|
|
2482
|
+
].join('\n')
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function renderIncomingContributionsSection(extensionSurfaces: ModuleExtensionSurfaceFacts): string {
|
|
2486
|
+
const incoming = extensionSurfaces.incoming ?? []
|
|
2487
|
+
if (incoming.length === 0) return `## Incoming installed contributions\n\n${EMPTY_SECTION_MARKER}`
|
|
2488
|
+
const rows = incoming.map((entry) => {
|
|
2489
|
+
const activation = entry.activationId ?? '—'
|
|
2490
|
+
const source = entry.source ? renderSourceRefLink(entry.source) : '—'
|
|
2491
|
+
return `| ${entry.contributorModuleId} | ${entry.contributionKind} | ${renderExtensionTargetRef(entry.target)} | ${entry.resolution} | ${activation} | ${entry.contributionId} · ${source} |`
|
|
2492
|
+
})
|
|
2493
|
+
return [
|
|
2494
|
+
'## Incoming installed contributions',
|
|
2495
|
+
'',
|
|
2496
|
+
'| Contributor | Kind | Target | Resolution | Activation | Contribution · Source |',
|
|
2497
|
+
'|---|---|---|---|---|---|',
|
|
2498
|
+
...rows,
|
|
2499
|
+
].join('\n')
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function renderContributionResolutionsSection(
|
|
2503
|
+
extensionSurfaces: ModuleExtensionSurfaceFacts,
|
|
2504
|
+
lookup: FactSourceLookup,
|
|
2505
|
+
): string {
|
|
2506
|
+
const resolutions = extensionSurfaces.contributionResolutions ?? []
|
|
2507
|
+
if (resolutions.length === 0) return `## Contribution resolutions\n\n${EMPTY_SECTION_MARKER}`
|
|
2508
|
+
const rows = resolutions.map((resolution) => {
|
|
2509
|
+
const activations = resolution.activationIds.length > 0 ? resolution.activationIds.join(', ') : '—'
|
|
2510
|
+
const source = renderLookedUpSource(lookup, 'extension-contribution', resolution.contributionId)
|
|
2511
|
+
return `| ${resolution.contributionId} | ${renderExtensionTargetRef(resolution.target)} | ${resolution.resolution} | ${activations} | ${source} |`
|
|
2512
|
+
})
|
|
2513
|
+
return [
|
|
2514
|
+
'## Contribution resolutions',
|
|
2515
|
+
'',
|
|
2516
|
+
'| Contribution | Target | Resolution | Activations | Source |',
|
|
2517
|
+
'|---|---|---|---|---|',
|
|
2518
|
+
...rows,
|
|
2519
|
+
].join('\n')
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function renderOverrideTargetPath(target: ModuleOverrideTarget): string {
|
|
2523
|
+
let display = 'overrides'
|
|
2524
|
+
for (let index = 0; index < target.path.length; index += 1) {
|
|
2525
|
+
const segment = target.path[index]
|
|
2526
|
+
const isKeyedTerminal = target.key !== undefined && index === target.path.length - 1
|
|
2527
|
+
display += isKeyedTerminal ? `[${JSON.stringify(segment)}]` : `.${segment}`
|
|
2528
|
+
}
|
|
2529
|
+
return display
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
function renderOverrideTargetsSection(targets: readonly ModuleOverrideTarget[] | undefined): string {
|
|
2533
|
+
if (!targets || targets.length === 0) return `## Exact override targets\n\n${EMPTY_SECTION_MARKER}`
|
|
2534
|
+
const header = '| Domain | Path / key | Supported modes | Referenced fact | Source |'
|
|
2535
|
+
const divider = '|---|---|---|---|---|'
|
|
2536
|
+
const rows = targets.map((target) => {
|
|
2537
|
+
const path = `\`${renderOverrideTargetPath(target)}\``
|
|
2538
|
+
const modes = target.modes.join(', ') || '—'
|
|
2539
|
+
const fact = `${target.factRef.factSection}:${target.factRef.factKey}`
|
|
2540
|
+
return `| ${target.domain} | ${path} | ${modes} | ${fact} | ${renderSourceRefLink(target.source)} |`
|
|
2541
|
+
})
|
|
2542
|
+
return ['## Exact override targets', '', header, divider, ...rows].join('\n')
|
|
2543
|
+
}
|
|
2544
|
+
|
|
1282
2545
|
export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
1283
2546
|
const extensionSurfaces = facts.extensionSurfaces ?? { hosts: [], contributions: [], unresolved: [] }
|
|
2547
|
+
const lookup = buildFactSourceLookup(facts)
|
|
1284
2548
|
const sections = [
|
|
1285
2549
|
`# ${facts.module} — module facts (generated, do not edit)`,
|
|
1286
2550
|
renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
|
|
1287
2551
|
`Source root: ${renderSourceLink(facts.sourceRoot)}`,
|
|
1288
2552
|
'',
|
|
1289
|
-
renderEntitiesSection(facts.entities),
|
|
2553
|
+
renderEntitiesSection(facts.entities, lookup),
|
|
1290
2554
|
'',
|
|
1291
|
-
renderEventsSection(facts.events),
|
|
2555
|
+
renderEventsSection(facts.events, lookup),
|
|
1292
2556
|
'',
|
|
1293
|
-
|
|
2557
|
+
renderSourceLinkedListSection(
|
|
2558
|
+
`## ACL features (${facts.aclFeatures.length})`,
|
|
2559
|
+
facts.aclFeatures,
|
|
2560
|
+
lookup,
|
|
2561
|
+
'acl-feature',
|
|
2562
|
+
'Feature',
|
|
2563
|
+
),
|
|
1294
2564
|
'',
|
|
1295
2565
|
renderApiRoutesSection(facts.apiRoutes),
|
|
1296
2566
|
'',
|
|
@@ -1298,9 +2568,9 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
1298
2568
|
'',
|
|
1299
2569
|
renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
|
|
1300
2570
|
'',
|
|
1301
|
-
|
|
2571
|
+
renderSourceLinkedListSection('## DI service tokens', facts.diTokens, lookup, 'di-registration', 'Token'),
|
|
1302
2572
|
'',
|
|
1303
|
-
|
|
2573
|
+
renderSourceLinkedListSection('## Search entities', facts.searchEntities, lookup, 'search', 'Entity ID'),
|
|
1304
2574
|
'',
|
|
1305
2575
|
renderHostTokensSection(facts.hostTokens),
|
|
1306
2576
|
'',
|
|
@@ -1308,9 +2578,15 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
1308
2578
|
'',
|
|
1309
2579
|
renderExtensionContributionsSection(extensionSurfaces),
|
|
1310
2580
|
'',
|
|
2581
|
+
renderActivationBindingsSection(extensionSurfaces),
|
|
2582
|
+
'',
|
|
2583
|
+
renderIncomingContributionsSection(extensionSurfaces),
|
|
2584
|
+
'',
|
|
2585
|
+
renderContributionResolutionsSection(extensionSurfaces, lookup),
|
|
2586
|
+
'',
|
|
1311
2587
|
renderExtensionDiagnosticsSection(extensionSurfaces),
|
|
1312
2588
|
'',
|
|
1313
|
-
|
|
2589
|
+
renderSourceLinkedListSection('## Notifications', facts.notifications, lookup, 'notification', 'Notification ID'),
|
|
1314
2590
|
'',
|
|
1315
2591
|
renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
|
|
1316
2592
|
'',
|
|
@@ -1318,7 +2594,25 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
|
|
|
1318
2594
|
'',
|
|
1319
2595
|
renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
|
|
1320
2596
|
'',
|
|
2597
|
+
renderOverrideTargetsSection(facts.overrideTargets),
|
|
2598
|
+
'',
|
|
1321
2599
|
]
|
|
2600
|
+
const owned = facts.ownedContracts ?? {}
|
|
2601
|
+
const ownedSections = [
|
|
2602
|
+
renderOwnedContractSection('## Owned contract — module metadata', owned['module-metadata']),
|
|
2603
|
+
renderOwnedContractSection('## Domain commands', owned.command),
|
|
2604
|
+
renderOwnedContractSection('## Workers', owned.worker),
|
|
2605
|
+
renderOwnedContractSection('## Page middleware', owned['page-middleware']),
|
|
2606
|
+
renderOwnedContractSection('## Setup', owned.setup),
|
|
2607
|
+
renderOwnedContractSection('## Encryption', owned.encryption),
|
|
2608
|
+
renderDiRegistrationsSection(owned['di-registration']),
|
|
2609
|
+
renderOwnedContractSection('## Custom entities', owned['custom-entity']),
|
|
2610
|
+
renderOwnedContractSection('## AI extensions', owned['ai-extension']),
|
|
2611
|
+
renderOwnedContractSection('## Generator plugins', owned['generator-plugin']),
|
|
2612
|
+
].filter((section) => section.length > 0)
|
|
2613
|
+
for (const section of ownedSections) {
|
|
2614
|
+
sections.push(section, '')
|
|
2615
|
+
}
|
|
1322
2616
|
return sections.join('\n')
|
|
1323
2617
|
}
|
|
1324
2618
|
|
|
@@ -1351,6 +2645,19 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
|
|
|
1351
2645
|
aiTools: facts.aiTools,
|
|
1352
2646
|
aiAgents: facts.aiAgents,
|
|
1353
2647
|
...(facts.extensionSurfaces ? { extensionSurfaces: facts.extensionSurfaces } : {}),
|
|
2648
|
+
...(facts.factSources && facts.factSources.length > 0 ? { factSources: facts.factSources } : {}),
|
|
2649
|
+
...(facts.ownedContracts && Object.keys(facts.ownedContracts).length > 0
|
|
2650
|
+
? { ownedContracts: facts.ownedContracts }
|
|
2651
|
+
: {}),
|
|
2652
|
+
...(facts.factDiagnostics && facts.factDiagnostics.length > 0
|
|
2653
|
+
? { factDiagnostics: facts.factDiagnostics }
|
|
2654
|
+
: {}),
|
|
2655
|
+
...(facts.overrideTargets && facts.overrideTargets.length > 0
|
|
2656
|
+
? { overrideTargets: facts.overrideTargets }
|
|
2657
|
+
: {}),
|
|
2658
|
+
...(facts.overrideTargetDiagnostics && facts.overrideTargetDiagnostics.length > 0
|
|
2659
|
+
? { overrideTargetDiagnostics: facts.overrideTargetDiagnostics }
|
|
2660
|
+
: {}),
|
|
1354
2661
|
}
|
|
1355
2662
|
}
|
|
1356
2663
|
|
|
@@ -1437,8 +2744,36 @@ function extractAllModuleFactsWithCache(options: ExtractAllModuleFactsOptions):
|
|
|
1437
2744
|
commandIds: new Set(sources.flatMap((source) => extractKnownCommandIds(source.moduleId, source.moduleRoot))),
|
|
1438
2745
|
})
|
|
1439
2746
|
assertNoUnresolvedExtensionTargets(correlated)
|
|
2747
|
+
const apiRouteOwners = new Map<string, {
|
|
2748
|
+
moduleId: string
|
|
2749
|
+
source: ModuleFactSourceRef
|
|
2750
|
+
bridges: ApiRouteInterceptorBridge[]
|
|
2751
|
+
}>()
|
|
2752
|
+
const commandOwners = new Map<string, { moduleId: string; source: ModuleFactSourceRef }>()
|
|
2753
|
+
for (const source of [...sources].sort((left, right) => left.moduleId.localeCompare(right.moduleId))) {
|
|
2754
|
+
const facts = factsByModule[source.moduleId]
|
|
2755
|
+
const sourceRoot = facts ? facts.sourceRoot : source.moduleId
|
|
2756
|
+
const owners = extractActivationTargetOwners({
|
|
2757
|
+
moduleId: source.moduleId,
|
|
2758
|
+
moduleRoot: source.moduleRoot,
|
|
2759
|
+
sourceRoot,
|
|
2760
|
+
})
|
|
2761
|
+
for (const route of owners.apiRoutes) {
|
|
2762
|
+
if (!apiRouteOwners.has(route.id)) {
|
|
2763
|
+
apiRouteOwners.set(route.id, { moduleId: source.moduleId, source: route.source, bridges: route.bridges })
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
for (const command of owners.commands) {
|
|
2767
|
+
if (!commandOwners.has(command.id)) commandOwners.set(command.id, { moduleId: source.moduleId, source: command.source })
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const withIncoming = correlateIncomingExtensions({
|
|
2771
|
+
surfacesByModule: correlated,
|
|
2772
|
+
apiRouteOwners,
|
|
2773
|
+
commandOwners,
|
|
2774
|
+
})
|
|
1440
2775
|
for (const moduleId of Object.keys(factsByModule).sort((left, right) => left.localeCompare(right))) {
|
|
1441
|
-
factsByModule[moduleId].extensionSurfaces =
|
|
2776
|
+
factsByModule[moduleId].extensionSurfaces = withIncoming[moduleId]
|
|
1442
2777
|
markdownByModule[moduleId] = renderModuleFactsMarkdown(factsByModule[moduleId])
|
|
1443
2778
|
warnings.push(...correlated[moduleId].unresolved.map((entry) =>
|
|
1444
2779
|
`[module-facts] ${moduleId} ${entry.reason}: ${entry.key} (${entry.source.path})`,
|