ctxman 3.0.0 → 3.1.0

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 (223) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +683 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +675 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +113 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Import extraction for the repository map
3
+ *
4
+ * Regex based, one pass per file: the module references a source file makes (imports,
5
+ * requires, includes, `mod` declarations) and the package or namespace it declares.
6
+ * Resolving the references to files of the repository is the job of resolver.js.
7
+ */
8
+
9
+ import path from 'path';
10
+ import { ImportTracker } from '../analyzers/import-tracker.js';
11
+
12
+ /** Language of each source extension the map covers */
13
+ export const LANGUAGE_BY_EXTENSION = {
14
+ '.js': 'javascript',
15
+ '.jsx': 'javascript',
16
+ '.mjs': 'javascript',
17
+ '.cjs': 'javascript',
18
+ '.ts': 'typescript',
19
+ '.tsx': 'typescript',
20
+ '.mts': 'typescript',
21
+ '.cts': 'typescript',
22
+ '.py': 'python',
23
+ '.go': 'go',
24
+ '.java': 'java',
25
+ '.kt': 'kotlin',
26
+ '.kts': 'kotlin',
27
+ '.scala': 'scala',
28
+ '.rb': 'ruby',
29
+ '.php': 'php',
30
+ '.c': 'c',
31
+ '.h': 'c',
32
+ '.cc': 'cpp',
33
+ '.cpp': 'cpp',
34
+ '.hpp': 'cpp',
35
+ '.rs': 'rust',
36
+ '.cs': 'csharp',
37
+ '.swift': 'swift',
38
+ };
39
+
40
+ /**
41
+ * Language of a file, or null when the map does not cover it
42
+ * @param {string} filePath
43
+ * @returns {string|null}
44
+ */
45
+ export function languageOf(filePath) {
46
+ return LANGUAGE_BY_EXTENSION[path.extname(filePath).toLowerCase()] || null;
47
+ }
48
+
49
+ // Specifier parsing for JavaScript/TypeScript lives in the import tracker
50
+ const jsTracker = new ImportTracker('.');
51
+
52
+ /** @param {RegExp} pattern @param {string} content @returns {RegExpExecArray[]} */
53
+ function allMatches(pattern, content) {
54
+ const matches = [];
55
+ pattern.lastIndex = 0;
56
+ let match;
57
+ while ((match = pattern.exec(content)) !== null) {
58
+ matches.push(match);
59
+ if (match[0] === '') pattern.lastIndex++;
60
+ }
61
+ return matches;
62
+ }
63
+
64
+ /** Split `a, b as c, d` (or `(a, b)`) into names, dropping aliases */
65
+ function splitNames(list) {
66
+ return list
67
+ .replace(/[()\\]/g, ' ')
68
+ .replace(/#.*$/gm, '')
69
+ .split(',')
70
+ .map((name) => name.trim().split(/\s+/)[0])
71
+ .filter((name) => /^[\w.*]+$/.test(name));
72
+ }
73
+
74
+ const PY_IMPORT =
75
+ /^[ \t]*import[ \t]+([\w.]+(?:[ \t]+as[ \t]+\w+)?(?:[ \t]*,[ \t]*[\w.]+(?:[ \t]+as[ \t]+\w+)?)*)/gm;
76
+ const PY_FROM = /^[ \t]*from[ \t]+(\.+[\w.]*|[\w.]+)[ \t]+import[ \t]+(\([^)]*\)|[^\n#;]+)/gm;
77
+ const GO_IMPORT_LINE = /^[ \t]*import[ \t]+(?:[\w.]+[ \t]+)?"([^"\n]+)"/gm;
78
+ const GO_IMPORT_BLOCK = /^[ \t]*import[ \t]*\(([\s\S]*?)\)/gm;
79
+ const GO_BLOCK_ENTRY = /"([^"\n]+)"/g;
80
+ const JVM_PACKAGE = /^[ \t]*package[ \t]+([\w.]+)/m;
81
+ const JVM_IMPORT =
82
+ /^[ \t]*import[ \t]+(?:static[ \t]+)?([\w.]+?)(?:\.(\*|_|\{[^}\n]*\}))?(?=[ \t]*(?:;|\/[/*]|as\b|$))/gm;
83
+ const RUBY_REQUIRE = /^[ \t]*(require_relative|require)[ \t(]+['"]([^'"\n]+)['"]/gm;
84
+ const PHP_NAMESPACE = /^[ \t]*namespace[ \t]+([\w\\]+)[ \t]*[;{]/m;
85
+ const PHP_USE = /^[ \t]*use[ \t]+(?!\()([^;{]+(?:\{[^}]*\})?)[ \t]*;/gm;
86
+ const PHP_INCLUDE =
87
+ /\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*((?:__DIR__|dirname[ \t]*\([ \t]*__FILE__[ \t]*\))[ \t]*\.[ \t]*)?['"]([^'"\n]+)['"]/g;
88
+ const C_INCLUDE = /^[ \t]*#[ \t]*include[ \t]*"([^"\n]+)"/gm;
89
+ const RUST_MOD = /^[ \t]*(?:pub(?:\([^)\n]*\))?[ \t]+)?mod[ \t]+(\w+)[ \t]*;/gm;
90
+ const RUST_PATH = /\b(crate|super|self)((?:::\w+)*)(?:::\{([^{}]*)\})?/g;
91
+ const RUST_USE_EXTERNAL = /^[ \t]*(?:pub(?:\([^)\n]*\))?[ \t]+)?use[ \t]+(\w+)((?:::\w+)*)/gm;
92
+ const CS_NAMESPACE = /^[ \t]*namespace[ \t]+([\w.]+)/gm;
93
+ const CS_USING =
94
+ /^[ \t]*(?:global[ \t]+)?using[ \t]+(static[ \t]+)?(?:\w+[ \t]*=[ \t]*)?([\w.]+)[ \t]*;/gm;
95
+
96
+ /**
97
+ * Module references of one file
98
+ * @param {string} language - From languageOf()
99
+ * @param {string} content - Source text
100
+ * @returns {Array<Object>} Raw references; their shape depends on the language
101
+ */
102
+ export function extractImports(language, content) {
103
+ switch (language) {
104
+ case 'javascript':
105
+ case 'typescript':
106
+ return jsTracker.parseModuleSpecifiers(content).map((spec) => ({ spec }));
107
+ case 'python': {
108
+ const refs = [];
109
+ for (const match of allMatches(PY_IMPORT, content)) {
110
+ for (const name of splitNames(match[1])) refs.push({ module: name, names: [] });
111
+ }
112
+ for (const match of allMatches(PY_FROM, content)) {
113
+ refs.push({ module: match[1], names: splitNames(match[2]) });
114
+ }
115
+ return refs;
116
+ }
117
+ case 'go': {
118
+ const specs = allMatches(GO_IMPORT_LINE, content).map((match) => match[1]);
119
+ for (const block of allMatches(GO_IMPORT_BLOCK, content)) {
120
+ specs.push(...allMatches(GO_BLOCK_ENTRY, block[1]).map((match) => match[1]));
121
+ }
122
+ return [...new Set(specs)].map((spec) => ({ spec }));
123
+ }
124
+ case 'java':
125
+ case 'kotlin':
126
+ case 'scala':
127
+ return allMatches(JVM_IMPORT, content).map((match) => ({
128
+ name: match[1],
129
+ wildcard: match[2] === '*' || match[2] === '_',
130
+ members: match[2]?.startsWith('{')
131
+ ? match[2]
132
+ .slice(1, -1)
133
+ .split(',')
134
+ .map((member) => member.split(/=>|\bas\b/)[0].trim())
135
+ .filter((member) => /^\w+$/.test(member))
136
+ : [],
137
+ }));
138
+ case 'ruby':
139
+ return allMatches(RUBY_REQUIRE, content).map((match) => ({
140
+ spec: match[2],
141
+ relative: match[1] === 'require_relative',
142
+ }));
143
+ case 'php': {
144
+ const refs = [];
145
+ for (const match of allMatches(PHP_USE, content)) {
146
+ for (const name of expandPhpUse(match[1])) refs.push({ name });
147
+ }
148
+ for (const match of allMatches(PHP_INCLUDE, content)) {
149
+ refs.push({ include: match[2], fromDir: Boolean(match[1]) });
150
+ }
151
+ return refs;
152
+ }
153
+ case 'c':
154
+ case 'cpp':
155
+ return allMatches(C_INCLUDE, content).map((match) => ({ include: match[1] }));
156
+ case 'rust': {
157
+ const refs = allMatches(RUST_MOD, content).map((match) => ({ mod: match[1] }));
158
+ for (const match of allMatches(RUST_PATH, content)) {
159
+ const segments = match[2].split('::').filter(Boolean);
160
+ if (match[3] === undefined) {
161
+ if (segments.length === 0) continue;
162
+ refs.push({ anchor: match[1], segments });
163
+ continue;
164
+ }
165
+ for (const member of match[3].split(',')) {
166
+ const tail = member.trim().split(/\s+as\s+/)[0];
167
+ const extra = /^\w+(?:::\w+)*$/.test(tail) && tail !== 'self' ? tail.split('::') : [];
168
+ refs.push({ anchor: match[1], segments: [...segments, ...extra] });
169
+ }
170
+ }
171
+ for (const match of allMatches(RUST_USE_EXTERNAL, content)) {
172
+ if (['crate', 'super', 'self', 'std', 'core', 'alloc'].includes(match[1])) continue;
173
+ refs.push({ crate: match[1], segments: match[2].split('::').filter(Boolean) });
174
+ }
175
+ return refs;
176
+ }
177
+ case 'csharp':
178
+ return allMatches(CS_USING, content).map((match) => ({
179
+ name: match[2],
180
+ isStatic: Boolean(match[1]),
181
+ }));
182
+ default:
183
+ return [];
184
+ }
185
+ }
186
+
187
+ /** `A\B\{C, D as E}` → ['A\B\C', 'A\B\D']; `function A\f` → ['A\f'] */
188
+ function expandPhpUse(clause) {
189
+ const names = [];
190
+ const body = clause.replace(/^\s*(?:function|const)\s+/, '');
191
+ const group = body.match(/^\s*\\?([\w\\]*?)\\?\s*\{([^}]*)\}/);
192
+ if (group) {
193
+ for (const member of group[2].split(',')) {
194
+ const name = member.trim().split(/\s+as\s+/)[0];
195
+ if (/^[\w\\]+$/.test(name)) names.push(`${group[1]}\\${name}`);
196
+ }
197
+ return names;
198
+ }
199
+ for (const part of body.split(',')) {
200
+ const name = part
201
+ .trim()
202
+ .split(/\s+as\s+/)[0]
203
+ .replace(/^\\/, '');
204
+ if (/^[\w\\]+$/.test(name)) names.push(name);
205
+ }
206
+ return names;
207
+ }
208
+
209
+ /**
210
+ * Packages or namespaces a file declares (Java/Kotlin/Scala `package`, PHP and C#
211
+ * `namespace`), used to resolve imports by fully qualified name
212
+ * @param {string} language
213
+ * @param {string} content
214
+ * @returns {string[]}
215
+ */
216
+ export function extractDeclarations(language, content) {
217
+ switch (language) {
218
+ case 'java':
219
+ case 'kotlin':
220
+ case 'scala': {
221
+ const match = content.match(JVM_PACKAGE);
222
+ return match ? [match[1]] : [];
223
+ }
224
+ case 'php': {
225
+ const match = content.match(PHP_NAMESPACE);
226
+ return match ? [match[1].replace(/^\\/, '')] : [];
227
+ }
228
+ case 'csharp':
229
+ return [...new Set(allMatches(CS_NAMESPACE, content).map((match) => match[1]))];
230
+ default:
231
+ return [];
232
+ }
233
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Repository map (`ctxman map`)
3
+ *
4
+ * A ranked overview of a codebase for an LLM, after Aider's repo map: a file dependency
5
+ * graph built from imports, PageRank over it blended with git change frequency and
6
+ * symbol references, and the function signatures of the best files trimmed to a token
7
+ * budget.
8
+ *
9
+ * buildRepoGraph(root, opts) scan + parse + resolve imports (+ git history)
10
+ * rankFiles(graph, opts) scores, best first
11
+ * renderRepoMap(root, opts) all of the above, rendered within the budget
12
+ */
13
+
14
+ export { buildRepoGraph, mineHistory, signatureAt, DEFAULT_HISTORY } from './graph.js';
15
+ export {
16
+ rankFiles,
17
+ pageRank,
18
+ resolveFocus,
19
+ toProjectPath,
20
+ DEFAULT_WEIGHTS,
21
+ DEFAULT_DAMPING,
22
+ } from './rank.js';
23
+ export {
24
+ renderRepoMap,
25
+ renderGraph,
26
+ renderBlock,
27
+ normalizeMapOptions,
28
+ DEFAULT_TOKENS,
29
+ STYLES,
30
+ ORDERS,
31
+ } from './render.js';
32
+ export { extractImports, languageOf, LANGUAGE_BY_EXTENSION } from './imports.js';
33
+ export { ImportResolver } from './resolver.js';
@@ -0,0 +1,253 @@
1
+ /**
2
+ * File ranking for `ctxman map`
3
+ *
4
+ * score(f) = (wP·P(f) + wH·H(f) + wS·S(f)) / (wP + wH + wS)
5
+ *
6
+ * P(f) = PR(f) / max PR PageRank over the import graph (importer → imported,
7
+ * damping 0.85), optionally personalised toward focus files
8
+ * H(f) = ln(1 + c(f)) / ln(1 + max c)
9
+ * c = commits among the last N (merges excluded) touching f
10
+ * S(f) = ln(1 + s(f)) / ln(1 + max s)
11
+ * s = mentions of f's function names in other files
12
+ *
13
+ * Default weights wP = 0.70, wH = 0.25, wS = 0.05. A component that is 0 for every file
14
+ * (no git history, no symbol references) drops out of the sum and of the divisor, so the
15
+ * others are renormalised. Equal scores are ordered by path.
16
+ */
17
+
18
+ import path from 'path';
19
+
20
+ /** Default blend of the three components */
21
+ export const DEFAULT_WEIGHTS = Object.freeze({ pagerank: 0.7, history: 0.25, symbols: 0.05 });
22
+ export const DEFAULT_DAMPING = 0.85;
23
+ /** Share of the random jump that goes to focus files and their neighbours */
24
+ export const FOCUS_SHARE = 0.9;
25
+ /** Relative jump weight of a focus file and of a file one import away from one */
26
+ export const FOCUS_WEIGHT = 1;
27
+ export const NEIGHBOUR_WEIGHT = 0.5;
28
+
29
+ /**
30
+ * Weighted PageRank by power iteration. The rank of nodes without outgoing edges
31
+ * (dangling nodes) is spread like the random jump, so the scores always sum to 1.
32
+ * @param {string[]} ids - Node ids; the order is the iteration order
33
+ * @param {Array<{from: string, to: string, weight?: number}>} edges
34
+ * @param {Object} [options]
35
+ * @param {number} [options.damping=0.85]
36
+ * @param {Map<string, number>|Object<string, number>} [options.personalization] - Random
37
+ * jump weights (normalised here); uniform when missing or all zero
38
+ * @param {number} [options.maxIterations=200]
39
+ * @param {number} [options.tolerance=1e-10] - Stop when the L1 change is below n × tolerance
40
+ * @returns {{scores: Map<string, number>, iterations: number, converged: boolean}}
41
+ */
42
+ export function pageRank(ids, edges, options = {}) {
43
+ const damping = options.damping ?? DEFAULT_DAMPING;
44
+ const maxIterations = options.maxIterations ?? 200;
45
+ const tolerance = options.tolerance ?? 1e-10;
46
+ if (!(damping >= 0 && damping < 1)) throw new Error('damping must be in [0, 1)');
47
+
48
+ const n = ids.length;
49
+ const scores = new Map();
50
+ if (n === 0) return { scores, iterations: 0, converged: true };
51
+
52
+ const index = new Map(ids.map((id, i) => [id, i]));
53
+ const merged = new Map();
54
+ for (const edge of edges) {
55
+ const from = index.get(edge.from);
56
+ const to = index.get(edge.to);
57
+ const weight = edge.weight ?? 1;
58
+ if (from === undefined || to === undefined || from === to || !(weight > 0)) continue;
59
+ const key = from * n + to;
60
+ merged.set(key, (merged.get(key) || 0) + weight);
61
+ }
62
+ const sources = new Int32Array(merged.size);
63
+ const targets = new Int32Array(merged.size);
64
+ const weights = new Float64Array(merged.size);
65
+ const outWeight = new Float64Array(n);
66
+ [...merged.keys()]
67
+ .sort((a, b) => a - b)
68
+ .forEach((key, i) => {
69
+ sources[i] = Math.floor(key / n);
70
+ targets[i] = key % n;
71
+ weights[i] = merged.get(key);
72
+ outWeight[sources[i]] += weights[i];
73
+ });
74
+
75
+ const jump = new Float64Array(n);
76
+ const custom = options.personalization;
77
+ let total = 0;
78
+ if (custom) {
79
+ const entries = custom instanceof Map ? custom : new Map(Object.entries(custom));
80
+ for (const [id, weight] of entries) {
81
+ const i = index.get(id);
82
+ if (i !== undefined && weight > 0) {
83
+ jump[i] += weight;
84
+ total += weight;
85
+ }
86
+ }
87
+ }
88
+ if (total > 0) for (let i = 0; i < n; i++) jump[i] /= total;
89
+ else jump.fill(1 / n);
90
+
91
+ let rank = new Float64Array(n).fill(1 / n);
92
+ let iterations = 0;
93
+ let converged = false;
94
+ while (iterations < maxIterations) {
95
+ iterations++;
96
+ const next = new Float64Array(n);
97
+ let dangling = 0;
98
+ for (let i = 0; i < n; i++) if (outWeight[i] === 0) dangling += rank[i];
99
+ for (let e = 0; e < sources.length; e++) {
100
+ next[targets[e]] += (damping * rank[sources[e]] * weights[e]) / outWeight[sources[e]];
101
+ }
102
+ const teleport = 1 - damping + damping * dangling;
103
+ let change = 0;
104
+ for (let i = 0; i < n; i++) {
105
+ next[i] += teleport * jump[i];
106
+ change += Math.abs(next[i] - rank[i]);
107
+ }
108
+ rank = next;
109
+ if (change < n * tolerance) {
110
+ converged = true;
111
+ break;
112
+ }
113
+ }
114
+
115
+ ids.forEach((id, i) => scores.set(id, rank[i]));
116
+ return { scores, iterations, converged };
117
+ }
118
+
119
+ /**
120
+ * Files of the graph matched by focus paths (a file, or a folder for every file in it)
121
+ * @param {import('./graph.js').RepoGraph} graph
122
+ * @param {string[]} focus - Absolute or project-relative paths
123
+ * @returns {{files: string[], unmatched: string[]}}
124
+ * @throws {Error} When a path lies outside the project
125
+ */
126
+ export function resolveFocus(graph, focus = []) {
127
+ const files = new Set();
128
+ const unmatched = [];
129
+ const nodePaths = graph.nodes.map((node) => node.path);
130
+ for (const entry of focus) {
131
+ const relative = toProjectPath(graph.root, entry);
132
+ const matches = nodePaths.filter(
133
+ (file) => relative === '' || file === relative || file.startsWith(`${relative}/`)
134
+ );
135
+ if (matches.length === 0) unmatched.push(entry);
136
+ for (const file of matches) files.add(file);
137
+ }
138
+ return { files: [...files].sort(), unmatched };
139
+ }
140
+
141
+ /**
142
+ * Project-relative form of a path (forward slashes); throws when it escapes the project
143
+ * @param {string} root - Absolute project root
144
+ * @param {string} entry
145
+ * @returns {string}
146
+ */
147
+ export function toProjectPath(root, entry) {
148
+ if (typeof entry !== 'string' || entry.trim() === '') {
149
+ throw new Error(`Invalid focus path: ${JSON.stringify(entry)}`);
150
+ }
151
+ const absolute = path.resolve(root, entry);
152
+ const relative = path.relative(root, absolute);
153
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
154
+ throw new Error(`Focus path is outside the project: ${entry}`);
155
+ }
156
+ return relative.split(path.sep).join('/');
157
+ }
158
+
159
+ /**
160
+ * Rank the files of a graph, best first
161
+ * @param {import('./graph.js').RepoGraph} graph
162
+ * @param {Object} [options]
163
+ * @param {string[]} [options.focus] - Files or folders to personalise PageRank toward
164
+ * @param {{pagerank?: number, history?: number, symbols?: number}} [options.weights]
165
+ * @param {number} [options.damping=0.85]
166
+ * @returns {Array<{path: string, rank: number, score: number, pagerank: number,
167
+ * changes: number, symbolRefs: number, focus: boolean,
168
+ * components: {pagerank: number, history: number, symbols: number}}>}
169
+ */
170
+ export function rankFiles(graph, options = {}) {
171
+ const weights = { ...DEFAULT_WEIGHTS, ...options.weights };
172
+ const ids = graph.nodes.map((node) => node.path);
173
+ const focus = new Set(resolveFocus(graph, options.focus || []).files);
174
+
175
+ let personalization = null;
176
+ if (focus.size > 0) {
177
+ const preference = new Map();
178
+ for (const file of focus) preference.set(file, FOCUS_WEIGHT);
179
+ for (const { from, to } of graph.edges) {
180
+ for (const [a, b] of [
181
+ [from, to],
182
+ [to, from],
183
+ ]) {
184
+ if (focus.has(a) && !focus.has(b)) {
185
+ preference.set(b, Math.max(preference.get(b) || 0, NEIGHBOUR_WEIGHT));
186
+ }
187
+ }
188
+ }
189
+ const preferenceTotal = [...preference.values()].reduce((sum, value) => sum + value, 0);
190
+ personalization = new Map(
191
+ ids.map((id) => [
192
+ id,
193
+ (1 - FOCUS_SHARE) / ids.length +
194
+ (FOCUS_SHARE * (preference.get(id) || 0)) / preferenceTotal,
195
+ ])
196
+ );
197
+ }
198
+
199
+ const { scores } = pageRank(ids, graph.edges, {
200
+ damping: options.damping ?? DEFAULT_DAMPING,
201
+ personalization,
202
+ });
203
+
204
+ const history = graph.history?.available ? graph.history.counts : null;
205
+ const maxRank = maxOf(scores.values());
206
+ const maxChanges = history ? maxOf(ids.map((id) => history[id] || 0)) : 0;
207
+ const maxSymbols = maxOf(graph.nodes.map((node) => node.symbolRefs || 0));
208
+ // Components without any signal drop out instead of capping every score below 1
209
+ const active = {
210
+ pagerank: weights.pagerank,
211
+ history: maxChanges > 0 ? weights.history : 0,
212
+ symbols: maxSymbols > 0 ? weights.symbols : 0,
213
+ };
214
+ const totalWeight = active.pagerank + active.history + active.symbols;
215
+
216
+ const rows = graph.nodes.map((node) => {
217
+ const pagerank = scores.get(node.path);
218
+ const changes = history ? history[node.path] || 0 : 0;
219
+ const symbolRefs = node.symbolRefs || 0;
220
+ const components = {
221
+ pagerank: maxRank > 0 ? pagerank / maxRank : 0,
222
+ history: maxChanges > 0 ? Math.log1p(changes) / Math.log1p(maxChanges) : 0,
223
+ symbols: maxSymbols > 0 ? Math.log1p(symbolRefs) / Math.log1p(maxSymbols) : 0,
224
+ };
225
+ const weighted =
226
+ active.pagerank * components.pagerank +
227
+ active.history * components.history +
228
+ active.symbols * components.symbols;
229
+ return {
230
+ path: node.path,
231
+ rank: 0,
232
+ score: totalWeight > 0 ? weighted / totalWeight : 0,
233
+ pagerank,
234
+ changes,
235
+ symbolRefs,
236
+ focus: focus.has(node.path),
237
+ components,
238
+ };
239
+ });
240
+
241
+ rows.sort((a, b) => b.score - a.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
242
+ rows.forEach((row, i) => {
243
+ row.rank = i + 1;
244
+ });
245
+ return rows;
246
+ }
247
+
248
+ /** Largest value, 0 for none (no argument spreading, so any size works) */
249
+ function maxOf(values) {
250
+ let max = 0;
251
+ for (const value of values) if (value > max) max = value;
252
+ return max;
253
+ }