@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.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build.mjs +1 -1
  3. package/dist/agentic/guides/module-facts.json +90266 -20749
  4. package/dist/agentic/guides/modules/ai_assistant.md +170 -60
  5. package/dist/agentic/guides/modules/api_docs.md +26 -1
  6. package/dist/agentic/guides/modules/api_keys.md +57 -17
  7. package/dist/agentic/guides/modules/attachments.md +72 -19
  8. package/dist/agentic/guides/modules/audit_logs.md +95 -29
  9. package/dist/agentic/guides/modules/auth.md +218 -83
  10. package/dist/agentic/guides/modules/business_rules.md +108 -34
  11. package/dist/agentic/guides/modules/catalog.md +386 -157
  12. package/dist/agentic/guides/modules/channel_gmail.md +55 -6
  13. package/dist/agentic/guides/modules/channel_imap.md +55 -6
  14. package/dist/agentic/guides/modules/checkout.md +254 -129
  15. package/dist/agentic/guides/modules/communication_channels.md +280 -109
  16. package/dist/agentic/guides/modules/configs.md +68 -11
  17. package/dist/agentic/guides/modules/content.md +26 -1
  18. package/dist/agentic/guides/modules/currencies.md +109 -44
  19. package/dist/agentic/guides/modules/customer_accounts.md +350 -156
  20. package/dist/agentic/guides/modules/customers.md +642 -294
  21. package/dist/agentic/guides/modules/dashboards.md +122 -28
  22. package/dist/agentic/guides/modules/data_sync.md +117 -37
  23. package/dist/agentic/guides/modules/design_system.md +36 -2
  24. package/dist/agentic/guides/modules/dictionaries.md +78 -20
  25. package/dist/agentic/guides/modules/directory.md +110 -45
  26. package/dist/agentic/guides/modules/entities.md +95 -38
  27. package/dist/agentic/guides/modules/events.md +31 -1
  28. package/dist/agentic/guides/modules/feature_toggles.md +79 -12
  29. package/dist/agentic/guides/modules/gateway_stripe.md +61 -6
  30. package/dist/agentic/guides/modules/generators.md +17 -1
  31. package/dist/agentic/guides/modules/inbox_ops.md +171 -60
  32. package/dist/agentic/guides/modules/integrations.md +116 -30
  33. package/dist/agentic/guides/modules/messages.md +187 -66
  34. package/dist/agentic/guides/modules/notifications.md +64 -9
  35. package/dist/agentic/guides/modules/onboarding.md +47 -8
  36. package/dist/agentic/guides/modules/payment_gateways.md +133 -48
  37. package/dist/agentic/guides/modules/perspectives.md +50 -12
  38. package/dist/agentic/guides/modules/planner.md +102 -32
  39. package/dist/agentic/guides/modules/portal.md +58 -15
  40. package/dist/agentic/guides/modules/progress.md +78 -30
  41. package/dist/agentic/guides/modules/query_index.md +115 -53
  42. package/dist/agentic/guides/modules/record_locks.md +178 -59
  43. package/dist/agentic/guides/modules/resources.md +155 -78
  44. package/dist/agentic/guides/modules/sales.md +620 -257
  45. package/dist/agentic/guides/modules/scheduler.md +95 -19
  46. package/dist/agentic/guides/modules/search.md +124 -20
  47. package/dist/agentic/guides/modules/security.md +251 -105
  48. package/dist/agentic/guides/modules/shipping_carriers.md +106 -37
  49. package/dist/agentic/guides/modules/sso.md +121 -49
  50. package/dist/agentic/guides/modules/staff.md +314 -126
  51. package/dist/agentic/guides/modules/storage_s3.md +55 -6
  52. package/dist/agentic/guides/modules/sync_akeneo.md +54 -4
  53. package/dist/agentic/guides/modules/sync_excel.md +49 -11
  54. package/dist/agentic/guides/modules/system_status_overlays.md +33 -5
  55. package/dist/agentic/guides/modules/translations.md +76 -21
  56. package/dist/agentic/guides/modules/webhooks.md +166 -82
  57. package/dist/agentic/guides/modules/wms.md +383 -223
  58. package/dist/agentic/guides/modules/workflows.md +248 -112
  59. package/dist/agentic/guides/upstream/AGENTS.md +1 -1
  60. package/dist/agentic/guides/upstream/manifest.json +2 -2
  61. package/dist/agentic/shared/AGENTS.md.template +6 -5
  62. package/dist/agentic/shared/ai/harness/cases.json +3 -0
  63. package/dist/agentic/shared/ai/lessons/_template.md +16 -0
  64. package/dist/agentic/shared/ai/lessons.md +26 -3
  65. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +7 -6
  66. package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +4 -2
  67. package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
  68. package/dist/lib/agentic-setup.js +2 -2
  69. package/dist/lib/agentic-setup.js.map +2 -2
  70. package/dist/lib/generators/module-extension-facts.js +652 -24
  71. package/dist/lib/generators/module-extension-facts.js.map +3 -3
  72. package/dist/lib/generators/module-fact-sources.js +70 -0
  73. package/dist/lib/generators/module-fact-sources.js.map +7 -0
  74. package/dist/lib/generators/module-facts.js +1001 -26
  75. package/dist/lib/generators/module-facts.js.map +3 -3
  76. package/dist/lib/generators/module-override-targets.js +688 -0
  77. package/dist/lib/generators/module-override-targets.js.map +7 -0
  78. package/dist/lib/testing/integration.js +5 -0
  79. package/dist/lib/testing/integration.js.map +2 -2
  80. package/package.json +5 -5
  81. package/src/lib/__tests__/agentic-setup.ownership.test.ts +6 -0
  82. package/src/lib/agentic-setup.ts +6 -2
  83. package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
  84. package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
  85. package/src/lib/generators/__tests__/module-extension-facts.test.ts +4 -2
  86. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +37 -3
  87. package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
  88. package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
  89. package/src/lib/generators/__tests__/module-override-targets.test.ts +304 -0
  90. package/src/lib/generators/module-extension-facts.ts +923 -27
  91. package/src/lib/generators/module-fact-sources.ts +108 -0
  92. package/src/lib/generators/module-facts.ts +1360 -25
  93. package/src/lib/generators/module-override-targets.ts +863 -0
  94. package/src/lib/testing/__tests__/integration.test.ts +4 -0
  95. 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 { ModuleExtensionContributionFact, ModuleExtensionSurfaceFacts } from '@open-mercato/shared/modules/widgets/extension-points'
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 diTokens = extractDiTokens(diFilePath)
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
- return {
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
- return `| ${contribution.id} | ${contribution.kind} | ${targets || '—'} | ${phases} | ${compactContributionDetails(contribution)} | ${resolution || '—'} |`
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
- renderInlineListSection(`## ACL features (${facts.aclFeatures.length})`, facts.aclFeatures),
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
- renderInlineListSection('## DI service tokens', facts.diTokens),
2571
+ renderSourceLinkedListSection('## DI service tokens', facts.diTokens, lookup, 'di-registration', 'Token'),
1302
2572
  '',
1303
- renderInlineListSection('## Search entities', facts.searchEntities),
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
- renderInlineListSection('## Notifications', facts.notifications),
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 = correlated[moduleId]
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})`,