@open-mercato/cli 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6744.1.0483b18565

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 (84) hide show
  1. package/dist/agentic/guides/module-facts.json +2247 -172
  2. package/dist/agentic/guides/modules/ai_assistant.md +47 -3
  3. package/dist/agentic/guides/modules/api_docs.md +23 -2
  4. package/dist/agentic/guides/modules/api_keys.md +25 -3
  5. package/dist/agentic/guides/modules/attachments.md +25 -3
  6. package/dist/agentic/guides/modules/audit_logs.md +24 -3
  7. package/dist/agentic/guides/modules/auth.md +47 -3
  8. package/dist/agentic/guides/modules/business_rules.md +31 -3
  9. package/dist/agentic/guides/modules/catalog.md +69 -4
  10. package/dist/agentic/guides/modules/channel_gmail.md +19 -2
  11. package/dist/agentic/guides/modules/channel_imap.md +19 -2
  12. package/dist/agentic/guides/modules/checkout.md +34 -2
  13. package/dist/agentic/guides/modules/communication_channels.md +23 -2
  14. package/dist/agentic/guides/modules/configs.md +28 -3
  15. package/dist/agentic/guides/modules/content.md +22 -2
  16. package/dist/agentic/guides/modules/currencies.md +32 -3
  17. package/dist/agentic/guides/modules/customer_accounts.md +27 -2
  18. package/dist/agentic/guides/modules/customers.md +70 -4
  19. package/dist/agentic/guides/modules/dashboards.md +25 -3
  20. package/dist/agentic/guides/modules/data_sync.md +22 -2
  21. package/dist/agentic/guides/modules/dictionaries.md +21 -2
  22. package/dist/agentic/guides/modules/directory.md +27 -2
  23. package/dist/agentic/guides/modules/entities.md +36 -3
  24. package/dist/agentic/guides/modules/events.md +19 -2
  25. package/dist/agentic/guides/modules/feature_toggles.md +32 -3
  26. package/dist/agentic/guides/modules/gateway_stripe.md +23 -3
  27. package/dist/agentic/guides/modules/generators.md +19 -2
  28. package/dist/agentic/guides/modules/inbox_ops.md +29 -2
  29. package/dist/agentic/guides/modules/integrations.md +23 -2
  30. package/dist/agentic/guides/modules/messages.md +25 -2
  31. package/dist/agentic/guides/modules/notifications.md +24 -3
  32. package/dist/agentic/guides/modules/onboarding.md +22 -2
  33. package/dist/agentic/guides/modules/payment_gateways.md +21 -2
  34. package/dist/agentic/guides/modules/perspectives.md +19 -2
  35. package/dist/agentic/guides/modules/planner.md +27 -3
  36. package/dist/agentic/guides/modules/portal.md +27 -2
  37. package/dist/agentic/guides/modules/progress.md +19 -2
  38. package/dist/agentic/guides/modules/query_index.md +27 -3
  39. package/dist/agentic/guides/modules/record_locks.md +21 -2
  40. package/dist/agentic/guides/modules/resources.md +32 -3
  41. package/dist/agentic/guides/modules/sales.md +44 -3
  42. package/dist/agentic/guides/modules/scheduler.md +30 -3
  43. package/dist/agentic/guides/modules/search.md +38 -3
  44. package/dist/agentic/guides/modules/security.md +32 -2
  45. package/dist/agentic/guides/modules/shipping_carriers.md +21 -2
  46. package/dist/agentic/guides/modules/sso.md +23 -2
  47. package/dist/agentic/guides/modules/staff.md +48 -3
  48. package/dist/agentic/guides/modules/storage_s3.md +23 -3
  49. package/dist/agentic/guides/modules/sync_akeneo.md +23 -3
  50. package/dist/agentic/guides/modules/sync_excel.md +19 -2
  51. package/dist/agentic/guides/modules/system_status_overlays.md +19 -2
  52. package/dist/agentic/guides/modules/translations.md +21 -2
  53. package/dist/agentic/guides/modules/webhooks.md +29 -4
  54. package/dist/agentic/guides/modules/wms.md +35 -3
  55. package/dist/agentic/guides/modules/workflows.md +42 -3
  56. package/dist/agentic/guides/upstream/manifest.json +1 -1
  57. package/dist/agentic/shared/ai/harness/README.md +4 -4
  58. package/dist/agentic/shared/ai/harness/RELEASE.md +2 -2
  59. package/dist/agentic/shared/ai/harness/cases.json +334 -467
  60. package/dist/agentic/shared/ai/harness/cases.schema.json +1 -0
  61. package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +4 -4
  62. package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +53 -0
  63. package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +62 -0
  64. package/dist/agentic/shared/ai/harness/release-matrix.json +24 -0
  65. package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +192 -0
  66. package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +1 -1
  67. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +4 -4
  68. package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +1 -1
  69. package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
  70. package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
  71. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
  72. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
  73. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
  74. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
  75. package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
  76. package/dist/agentic/shared/ai/skills/tiers.json +1 -0
  77. package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +19 -6
  78. package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +163 -31
  79. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +26 -13
  80. package/dist/lib/generators/module-facts.js +197 -9
  81. package/dist/lib/generators/module-facts.js.map +2 -2
  82. package/package.json +5 -5
  83. package/src/lib/generators/__tests__/module-facts.source-links.test.ts +119 -0
  84. package/src/lib/generators/module-facts.ts +271 -7
@@ -21,6 +21,27 @@ export interface ModuleApiRouteFact {
21
21
  path: string
22
22
  methods: string[]
23
23
  auth: Record<string, ApiRouteAuthRule>
24
+ sourcePath: string | null
25
+ }
26
+
27
+ export interface ModulePageFact {
28
+ path: string
29
+ sourcePath: string
30
+ }
31
+
32
+ export interface ModuleCliCommandFact {
33
+ command: string
34
+ sourcePath: string
35
+ }
36
+
37
+ export interface ModuleAiToolFact {
38
+ name: string
39
+ sourcePath: string
40
+ }
41
+
42
+ export interface ModuleAiAgentFact {
43
+ id: string
44
+ sourcePath: string
24
45
  }
25
46
 
26
47
  export interface ModuleEventFact {
@@ -42,6 +63,7 @@ export interface ModuleFacts {
42
63
  coreVersion: string | null
43
64
  sourcePackage: string | null
44
65
  sourceVersion: string | null
66
+ sourceRoot: string
45
67
  entities: ModuleEntityFact[]
46
68
  events: ModuleEventFact[]
47
69
  aclFeatures: string[]
@@ -51,6 +73,11 @@ export interface ModuleFacts {
51
73
  hostTokens: ModuleHostTokens
52
74
  notifications: string[]
53
75
  cli: string[]
76
+ backendPages: ModulePageFact[]
77
+ frontendPages: ModulePageFact[]
78
+ cliCommands: ModuleCliCommandFact[]
79
+ aiTools: ModuleAiToolFact[]
80
+ aiAgents: ModuleAiAgentFact[]
54
81
  warnings: string[]
55
82
  }
56
83
 
@@ -495,13 +522,14 @@ function parseApiRouteEntry(entryLiteral: ts.ObjectLiteralExpression): ModuleApi
495
522
  }
496
523
  }
497
524
 
498
- return { path: routePath, methods, auth }
525
+ return { path: routePath, methods, auth, sourcePath: null }
499
526
  }
500
527
 
501
528
  function extractApiRoutes(
502
529
  moduleId: string,
503
530
  registrySource: string | null,
504
531
  registryDescription: string,
532
+ routeSourcePaths: ReadonlyMap<string, string>,
505
533
  warnings: string[],
506
534
  ): ModuleApiRouteFact[] {
507
535
  if (registrySource == null) {
@@ -529,7 +557,7 @@ function extractApiRoutes(
529
557
  const route = parseApiRouteEntry(element)
530
558
  if (route && !seenPaths.has(route.path)) {
531
559
  seenPaths.add(route.path)
532
- routes.push(route)
560
+ routes.push({ ...route, sourcePath: routeSourcePaths.get(route.path) ?? null })
533
561
  }
534
562
  }
535
563
  }
@@ -717,6 +745,190 @@ function listSourceFilesRecursive(directory: string): string[] {
717
745
  return files
718
746
  }
719
747
 
748
+ function toPortableSourceRoot(moduleId: string, sourcePackage: string | null): string {
749
+ return path.posix.join('node_modules', sourcePackage ?? '@open-mercato/core', 'src', 'modules', moduleId)
750
+ }
751
+
752
+ function toPortableSourcePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
753
+ const relativePath = path.relative(moduleRoot, filePath).split(path.sep).join('/')
754
+ return path.posix.join(sourceRoot, relativePath)
755
+ }
756
+
757
+ function sourceHasDefaultExport(sourceFile: ts.SourceFile): boolean {
758
+ return sourceFile.statements.some((statement) => {
759
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) return true
760
+ if (!ts.isFunctionDeclaration(statement) && !ts.isClassDeclaration(statement)) return false
761
+ return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
762
+ })
763
+ }
764
+
765
+ function readRouteMetadataPath(sourceFile: ts.SourceFile): string | null {
766
+ const metadata = findObjectLiteralDeclaration(sourceFile, 'metadata')
767
+ return metadata ? readStringPropertyInitializer(metadata, 'path') ?? null : null
768
+ }
769
+
770
+ function extractApiRouteSourcePaths(
771
+ moduleId: string,
772
+ moduleRoot: string,
773
+ sourceRoot: string,
774
+ ): Map<string, string> {
775
+ const apiRoot = path.join(moduleRoot, 'api')
776
+ const sources = new Map<string, string>()
777
+ if (!fs.existsSync(apiRoot)) return sources
778
+ const methodDirectories = new Set(['get', 'post', 'put', 'patch', 'delete'])
779
+
780
+ for (const filePath of listSourceFilesRecursive(apiRoot)) {
781
+ const relativePath = path.relative(apiRoot, filePath).split(path.sep).join('/')
782
+ const segments = relativePath.split('/')
783
+ const fileName = segments.pop() as string
784
+ const fileStem = fileName.replace(/\.tsx?$/, '')
785
+ let routeSegments: string[]
786
+ if (fileStem === 'route') {
787
+ routeSegments = segments
788
+ } else if (segments[0] && methodDirectories.has(segments[0].toLowerCase())) {
789
+ routeSegments = [...segments.slice(1), fileStem]
790
+ } else {
791
+ routeSegments = [...segments, fileStem]
792
+ }
793
+ const defaultPath = `/${[moduleId, ...routeSegments].filter(Boolean).join('/')}`
794
+ const sourceFile = readSourceFile(filePath)
795
+ const routePath = sourceFile ? readRouteMetadataPath(sourceFile) ?? defaultPath : defaultPath
796
+ sources.set(routePath, toPortableSourcePath(moduleRoot, sourceRoot, filePath))
797
+ }
798
+ return sources
799
+ }
800
+
801
+ function extractModulePages(
802
+ moduleId: string,
803
+ moduleRoot: string,
804
+ sourceRoot: string,
805
+ surface: 'backend' | 'frontend',
806
+ ): ModulePageFact[] {
807
+ const pageRoot = path.join(moduleRoot, surface)
808
+ if (!fs.existsSync(pageRoot)) return []
809
+ const pages: ModulePageFact[] = []
810
+ const seen = new Set<string>()
811
+
812
+ for (const filePath of listSourceFilesRecursive(pageRoot)) {
813
+ if (!filePath.endsWith('.tsx')) continue
814
+ const relativePath = path.relative(pageRoot, filePath).split(path.sep).join('/')
815
+ const segments = relativePath.split('/')
816
+ const fileName = segments.pop() as string
817
+ const fileStem = fileName.replace(/\.tsx$/, '')
818
+ const isModernPage = fileStem === 'page'
819
+ if (!isModernPage && (fileStem.endsWith('.meta') || /^[A-Z]/.test(fileStem))) continue
820
+ const sourceFile = readSourceFile(filePath)
821
+ if (!sourceFile || !sourceHasDefaultExport(sourceFile)) continue
822
+ const routeSegments = isModernPage ? segments : [...segments, fileStem]
823
+ const routePath = surface === 'frontend'
824
+ ? `/${routeSegments.filter(Boolean).join('/')}`
825
+ : isModernPage
826
+ ? `/backend/${routeSegments.join('/') || moduleId}`
827
+ : `/backend/${routeSegments[0] === moduleId
828
+ ? routeSegments.filter(Boolean).join('/')
829
+ : [moduleId, ...routeSegments].filter(Boolean).join('/')}`
830
+ if (seen.has(routePath)) continue
831
+ seen.add(routePath)
832
+ pages.push({
833
+ path: routePath,
834
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
835
+ })
836
+ }
837
+ return pages.sort((left, right) => left.path.localeCompare(right.path))
838
+ }
839
+
840
+ function findAncestorVariableDeclaration(node: ts.Node): ts.VariableDeclaration | null {
841
+ let current: ts.Node | undefined = node.parent
842
+ while (current) {
843
+ if (ts.isVariableDeclaration(current)) return current
844
+ current = current.parent
845
+ }
846
+ return null
847
+ }
848
+
849
+ function findAncestorCallExpression(node: ts.Node): ts.CallExpression | null {
850
+ let current: ts.Node | undefined = node.parent
851
+ while (current) {
852
+ if (ts.isCallExpression(current)) return current
853
+ if (ts.isVariableDeclaration(current) || ts.isStatement(current)) return null
854
+ current = current.parent
855
+ }
856
+ return null
857
+ }
858
+
859
+ function extractNamedAiFacts(
860
+ filePaths: readonly string[],
861
+ moduleRoot: string,
862
+ sourceRoot: string,
863
+ propertyName: 'name' | 'id',
864
+ ): Array<{ value: string; sourcePath: string }> {
865
+ const facts: Array<{ value: string; sourcePath: string }> = []
866
+ const seen = new Set<string>()
867
+ for (const filePath of filePaths) {
868
+ const sourceFile = readSourceFile(filePath)
869
+ if (!sourceFile) continue
870
+ const initializers = buildVariableInitializerMap(sourceFile)
871
+ const readDefinitionValue = (objectLiteral: ts.ObjectLiteralExpression): string | undefined => {
872
+ const initializer = getObjectPropertyInitializer(objectLiteral, propertyName)
873
+ if (!initializer) return undefined
874
+ const resolved = unwrapExpression(initializer)
875
+ if (ts.isStringLiteralLike(resolved)) return resolved.text
876
+ if (ts.isIdentifier(resolved)) {
877
+ const declarationInitializer = initializers.get(resolved.text)
878
+ if (declarationInitializer) {
879
+ const declarationValue = unwrapExpression(declarationInitializer)
880
+ if (ts.isStringLiteralLike(declarationValue)) return declarationValue.text
881
+ }
882
+ }
883
+ return undefined
884
+ }
885
+ const visit = (node: ts.Node): void => {
886
+ if (ts.isObjectLiteralExpression(node)) {
887
+ const value = readDefinitionValue(node)
888
+ if (value && !seen.has(value)) {
889
+ const declaration = findAncestorVariableDeclaration(node)
890
+ const declarationName = declaration && ts.isIdentifier(declaration.name) ? declaration.name.text : ''
891
+ const declarationType = declaration?.type?.getText(sourceFile) ?? ''
892
+ const call = findAncestorCallExpression(node)
893
+ const callee = call?.expression.getText(sourceFile) ?? ''
894
+ const isDefinition = propertyName === 'name'
895
+ ? /AiTool/.test(declarationType) || /tool$/i.test(declarationName) || /AiTool/.test(callee)
896
+ : /AiAgentDefinition/.test(declarationType) || /agent$/i.test(declarationName) || /defineAiAgent/.test(callee)
897
+ if (isDefinition) {
898
+ seen.add(value)
899
+ facts.push({
900
+ value,
901
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, filePath),
902
+ })
903
+ }
904
+ }
905
+ }
906
+ node.forEachChild(visit)
907
+ }
908
+ sourceFile.forEachChild(visit)
909
+ }
910
+ return facts.sort((left, right) => left.value.localeCompare(right.value))
911
+ }
912
+
913
+ function extractAiTools(moduleRoot: string, sourceRoot: string): ModuleAiToolFact[] {
914
+ const files = new Set<string>()
915
+ const rootFile = resolveConventionFile(moduleRoot, 'ai-tools')
916
+ if (rootFile) files.add(rootFile)
917
+ const toolsDirectory = path.join(moduleRoot, 'ai-tools')
918
+ if (fs.existsSync(toolsDirectory)) {
919
+ for (const filePath of listSourceFilesRecursive(toolsDirectory)) files.add(filePath)
920
+ }
921
+ return extractNamedAiFacts([...files], moduleRoot, sourceRoot, 'name')
922
+ .map((fact) => ({ name: fact.value, sourcePath: fact.sourcePath }))
923
+ }
924
+
925
+ function extractAiAgents(moduleRoot: string, sourceRoot: string): ModuleAiAgentFact[] {
926
+ const agentsFile = resolveConventionFile(moduleRoot, 'ai-agents')
927
+ if (!agentsFile) return []
928
+ return extractNamedAiFacts([agentsFile], moduleRoot, sourceRoot, 'id')
929
+ .map((fact) => ({ id: fact.value, sourcePath: fact.sourcePath }))
930
+ }
931
+
720
932
  function extractTableIds(moduleRoot: string): string[] {
721
933
  if (!fs.existsSync(moduleRoot)) return []
722
934
 
@@ -780,6 +992,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
780
992
  if (!moduleRoot) {
781
993
  throw new Error(`[internal] extractModuleFacts requires moduleRoot or coreSrcRoot for module "${moduleId}"`)
782
994
  }
995
+ const sourceRoot = toPortableSourceRoot(moduleId, sourcePackage)
783
996
 
784
997
  const entitiesFilePath =
785
998
  resolveConventionFile(path.join(moduleRoot, 'data'), 'entities') ??
@@ -802,11 +1015,22 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
802
1015
  const aclFeatures = extractAclFeatures(aclFilePath)
803
1016
 
804
1017
  const { source: registrySource, description: registryDescription } = resolveRegistrySource(options)
805
- const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, warnings)
1018
+ const apiRouteSourcePaths = extractApiRouteSourcePaths(moduleId, moduleRoot, sourceRoot)
1019
+ const apiRoutes = extractApiRoutes(moduleId, registrySource, registryDescription, apiRouteSourcePaths, warnings)
806
1020
  const diTokens = extractDiTokens(diFilePath)
807
1021
  const searchEntities = extractSearchEntities(searchFilePath, warnings)
808
1022
  const notifications = extractNotifications(notificationsFilePath, warnings)
809
1023
  const cli = extractCli(cliFilePath, warnings)
1024
+ const cliCommands = cliFilePath
1025
+ ? cli.map((command) => ({
1026
+ command,
1027
+ sourcePath: toPortableSourcePath(moduleRoot, sourceRoot, cliFilePath),
1028
+ }))
1029
+ : []
1030
+ const backendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'backend')
1031
+ const frontendPages = extractModulePages(moduleId, moduleRoot, sourceRoot, 'frontend')
1032
+ const aiTools = extractAiTools(moduleRoot, sourceRoot)
1033
+ const aiAgents = extractAiAgents(moduleRoot, sourceRoot)
810
1034
  const hostTokens: ModuleHostTokens = {
811
1035
  entityIds: extractHostEntityIds(entities),
812
1036
  tableIds: extractTableIds(moduleRoot),
@@ -819,6 +1043,7 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
819
1043
  coreVersion,
820
1044
  sourcePackage,
821
1045
  sourceVersion,
1046
+ sourceRoot,
822
1047
  entities,
823
1048
  events,
824
1049
  aclFeatures,
@@ -828,6 +1053,11 @@ export function extractModuleFacts(options: ExtractModuleFactsOptions): ModuleFa
828
1053
  hostTokens,
829
1054
  notifications,
830
1055
  cli,
1056
+ backendPages,
1057
+ frontendPages,
1058
+ cliCommands,
1059
+ aiTools,
1060
+ aiAgents,
831
1061
  warnings,
832
1062
  }
833
1063
  }
@@ -844,6 +1074,7 @@ export interface ModuleFactsJsonEntry {
844
1074
  coreVersion: string | null
845
1075
  sourcePackage: string | null
846
1076
  sourceVersion: string | null
1077
+ sourceRoot: string
847
1078
  entities: ModuleEntityFact[]
848
1079
  events: ModuleFactsJsonEvent[]
849
1080
  aclFeatures: string[]
@@ -853,6 +1084,11 @@ export interface ModuleFactsJsonEntry {
853
1084
  hostTokens: ModuleHostTokens
854
1085
  notifications: string[]
855
1086
  cli: string[]
1087
+ backendPages: ModulePageFact[]
1088
+ frontendPages: ModulePageFact[]
1089
+ cliCommands: ModuleCliCommandFact[]
1090
+ aiTools: ModuleAiToolFact[]
1091
+ aiAgents: ModuleAiAgentFact[]
856
1092
  }
857
1093
 
858
1094
  const EMPTY_SECTION_MARKER = '_none_'
@@ -894,6 +1130,10 @@ function renderInlineListSection(heading: string, values: string[]): string {
894
1130
  return `${heading}\n\n${values.join(' · ')}`
895
1131
  }
896
1132
 
1133
+ function renderSourceLink(sourcePath: string): string {
1134
+ return `[${sourcePath}](../../../${sourcePath})`
1135
+ }
1136
+
897
1137
  function describeAuthRule(rule: ApiRouteAuthRule | undefined): string {
898
1138
  if (!rule) return 'public'
899
1139
  if (rule.requireFeatures && rule.requireFeatures.length > 0) return rule.requireFeatures.join(', ')
@@ -916,14 +1156,23 @@ function renderApiRouteAuthCell(route: ModuleApiRouteFact): string {
916
1156
 
917
1157
  function renderApiRoutesSection(routes: ModuleApiRouteFact[]): string {
918
1158
  if (routes.length === 0) return `## API routes\n\n${EMPTY_SECTION_MARKER}`
919
- const header = '| Path | Methods | Auth (per-method requireFeatures) |'
920
- const divider = '|---|---|---|'
1159
+ const header = '| Path | Methods | Auth (per-method requireFeatures) | Source |'
1160
+ const divider = '|---|---|---|---|'
921
1161
  const rows = routes.map(
922
- (route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} |`,
1162
+ (route) => `| ${route.path} | ${route.methods.join(' ')} | ${renderApiRouteAuthCell(route)} | ${route.sourcePath ? renderSourceLink(route.sourcePath) : '—'} |`,
923
1163
  )
924
1164
  return ['## API routes', '', header, divider, ...rows].join('\n')
925
1165
  }
926
1166
 
1167
+ function renderLinkedFactsSection(
1168
+ heading: string,
1169
+ facts: ReadonlyArray<{ label: string; sourcePath: string }>,
1170
+ ): string {
1171
+ if (facts.length === 0) return `${heading}\n\n${EMPTY_SECTION_MARKER}`
1172
+ const rows = facts.map((fact) => `| ${fact.label} | ${renderSourceLink(fact.sourcePath)} |`)
1173
+ return [heading, '', '| ID / path | Source |', '|---|---|', ...rows].join('\n')
1174
+ }
1175
+
927
1176
  function renderHostTokensSection(hostTokens: ModuleHostTokens): string {
928
1177
  const entityIdsLine = hostTokens.entityIds.length > 0 ? hostTokens.entityIds.join(' · ') : EMPTY_SECTION_MARKER
929
1178
  const tableIdsLine = hostTokens.tableIds.length > 0 ? hostTokens.tableIds.join(' · ') : EMPTY_SECTION_MARKER
@@ -934,6 +1183,7 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
934
1183
  const sections = [
935
1184
  `# ${facts.module} — module facts (generated, do not edit)`,
936
1185
  renderVersionStamp(facts.coreVersion, facts.sourcePackage, facts.sourceVersion),
1186
+ `Source root: ${renderSourceLink(facts.sourceRoot)}`,
937
1187
  '',
938
1188
  renderEntitiesSection(facts.entities),
939
1189
  '',
@@ -943,6 +1193,10 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
943
1193
  '',
944
1194
  renderApiRoutesSection(facts.apiRoutes),
945
1195
  '',
1196
+ renderLinkedFactsSection('## Backend pages', facts.backendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
1197
+ '',
1198
+ renderLinkedFactsSection('## Frontend pages', facts.frontendPages.map((page) => ({ label: page.path, sourcePath: page.sourcePath }))),
1199
+ '',
946
1200
  renderInlineListSection('## DI service tokens', facts.diTokens),
947
1201
  '',
948
1202
  renderInlineListSection('## Search entities', facts.searchEntities),
@@ -951,7 +1205,11 @@ export function renderModuleFactsMarkdown(facts: ModuleFacts): string {
951
1205
  '',
952
1206
  renderInlineListSection('## Notifications', facts.notifications),
953
1207
  '',
954
- renderInlineListSection('## CLI', facts.cli),
1208
+ renderLinkedFactsSection('## CLI commands', facts.cliCommands.map((command) => ({ label: command.command, sourcePath: command.sourcePath }))),
1209
+ '',
1210
+ renderLinkedFactsSection('## AI tools / MCP capabilities', facts.aiTools.map((tool) => ({ label: tool.name, sourcePath: tool.sourcePath }))),
1211
+ '',
1212
+ renderLinkedFactsSection('## AI agents', facts.aiAgents.map((agent) => ({ label: agent.id, sourcePath: agent.sourcePath }))),
955
1213
  '',
956
1214
  ]
957
1215
  return sections.join('\n')
@@ -964,6 +1222,7 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
964
1222
  coreVersion: facts.coreVersion,
965
1223
  sourcePackage: facts.sourcePackage,
966
1224
  sourceVersion: facts.sourceVersion,
1225
+ sourceRoot: facts.sourceRoot,
967
1226
  entities: facts.entities,
968
1227
  events: facts.events.map((event) => ({ id: event.id, category: event.category, entity: event.entity })),
969
1228
  aclFeatures: facts.aclFeatures,
@@ -973,6 +1232,11 @@ export function toModuleFactsJsonEntry(facts: ModuleFacts): ModuleFactsJsonEntry
973
1232
  hostTokens: facts.hostTokens,
974
1233
  notifications: facts.notifications,
975
1234
  cli: facts.cli,
1235
+ backendPages: facts.backendPages,
1236
+ frontendPages: facts.frontendPages,
1237
+ cliCommands: facts.cliCommands,
1238
+ aiTools: facts.aiTools,
1239
+ aiAgents: facts.aiAgents,
976
1240
  }
977
1241
  }
978
1242