ctxman 3.0.0 → 3.1.1

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 (224) 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 +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -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 +687 -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 +772 -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 +116 -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 +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -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/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -0,0 +1,574 @@
1
+ /**
2
+ * Import resolution for the repository map
3
+ *
4
+ * Turns the raw references from imports.js into files of the repository. Only files the
5
+ * scanner returned can be targets, so anything outside the project (packages, the
6
+ * standard library, ignored files) stays unresolved and is ignored. All paths are
7
+ * project-relative with forward slashes.
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ const posix = path.posix;
14
+
15
+ // TypeScript sources first: they win over compiled JavaScript next to them
16
+ const JS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '.d.ts'];
17
+ // An ESM import names the compiled file (`./a.js`) even when the source is `a.ts`
18
+ const JS_TO_TS = { '.js': ['.ts', '.tsx'], '.jsx': ['.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };
19
+ const RUST_ROOT_DIRS = new Set(['bin', 'examples', 'tests', 'benches']);
20
+
21
+ /** Directory of a project-relative path ('' for the root) */
22
+ function dirOf(filePath) {
23
+ const dir = posix.dirname(filePath);
24
+ return dir === '.' ? '' : dir;
25
+ }
26
+
27
+ /** Join and normalise; null when the result leaves the project */
28
+ function joinInside(...parts) {
29
+ const joined = posix.normalize(posix.join(...parts.filter((part) => part !== '')));
30
+ if (joined === '..' || joined.startsWith('../') || posix.isAbsolute(joined)) return null;
31
+ return joined === '.' ? '' : joined;
32
+ }
33
+
34
+ /** Number of leading directory names two paths share */
35
+ function sharedDepth(a, b) {
36
+ const left = dirOf(a).split('/');
37
+ const right = dirOf(b).split('/');
38
+ let depth = 0;
39
+ while (depth < left.length && depth < right.length && left[depth] === right[depth]) depth++;
40
+ return depth;
41
+ }
42
+
43
+ /**
44
+ * Remove comments and trailing commas from JSON with comments (tsconfig.json)
45
+ * @param {string} text
46
+ * @returns {string}
47
+ */
48
+ export function stripJsonComments(text) {
49
+ let out = '';
50
+ let inString = false;
51
+ for (let i = 0; i < text.length; i++) {
52
+ const ch = text[i];
53
+ if (inString) {
54
+ out += ch;
55
+ if (ch === '\\') out += text[++i] ?? '';
56
+ else if (ch === '"') inString = false;
57
+ } else if (ch === '"') {
58
+ inString = true;
59
+ out += ch;
60
+ } else if (ch === '/' && text[i + 1] === '/') {
61
+ while (i < text.length && text[i] !== '\n') i++;
62
+ out += '\n';
63
+ } else if (ch === '/' && text[i + 1] === '*') {
64
+ i = text.indexOf('*/', i + 2);
65
+ if (i === -1) break;
66
+ i++;
67
+ } else {
68
+ out += ch;
69
+ }
70
+ }
71
+ return out.replace(/,(\s*[}\]])/g, '$1');
72
+ }
73
+
74
+ export class ImportResolver {
75
+ /**
76
+ * @param {string} root - Absolute project root
77
+ * @param {Array<{path: string, language: string, declarations: string[],
78
+ * methods: Array<{name: string}>}>} nodes - Files of the map
79
+ */
80
+ constructor(root, nodes) {
81
+ this.root = root;
82
+ this.nodes = new Map(nodes.map((node) => [node.path, node]));
83
+ this.dirFiles = new Map();
84
+ this.byBasename = new Map();
85
+ this.typeIndex = new Map(); // fully qualified type name -> file (Java, Kotlin, Scala, C#)
86
+ this.packageIndex = new Map(); // package or namespace -> files
87
+ this.phpIndex = new Map(); // lower-case PHP class name -> file
88
+ this.phpNamespaces = new Map(); // lower-case PHP namespace -> files
89
+ this.existsCache = new Map();
90
+ this.goModules = null;
91
+ this.cargoCache = new Map();
92
+ this.cratesByName = null;
93
+ this.jsAliases = undefined;
94
+
95
+ for (const node of nodes) {
96
+ const dir = dirOf(node.path);
97
+ if (!this.dirFiles.has(dir)) this.dirFiles.set(dir, []);
98
+ this.dirFiles.get(dir).push(node.path);
99
+ const base = posix.basename(node.path);
100
+ if (!this.byBasename.has(base)) this.byBasename.set(base, []);
101
+ this.byBasename.get(base).push(node.path);
102
+
103
+ const typeName = posix.basename(node.path).replace(/\.[^.]+$/, '');
104
+ for (const declared of node.declarations || []) {
105
+ if (node.language === 'php') {
106
+ addTo(this.phpNamespaces, declared.toLowerCase(), node.path);
107
+ } else {
108
+ addTo(this.packageIndex, declared, node.path);
109
+ if (!this.typeIndex.has(`${declared}.${typeName}`)) {
110
+ this.typeIndex.set(`${declared}.${typeName}`, node.path);
111
+ }
112
+ }
113
+ }
114
+ if (node.language === 'php') {
115
+ const namespace = node.declarations?.[0];
116
+ const name = (namespace ? `${namespace}\\${typeName}` : typeName).toLowerCase();
117
+ if (!this.phpIndex.has(name)) this.phpIndex.set(name, node.path);
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Files a node depends on
124
+ * @param {Object} node - Node with `path`, `language` and `imports` (from imports.js)
125
+ * @returns {{edges: Array<{to: string, weight: number}>, unresolved: number}}
126
+ */
127
+ resolve(node) {
128
+ const edges = new Map();
129
+ let unresolved = 0;
130
+ for (const ref of node.imports || []) {
131
+ const targets = this.resolveOne(node, ref).filter((target) => target.to !== node.path);
132
+ if (targets.length === 0) unresolved++;
133
+ for (const { to, weight } of targets) {
134
+ edges.set(to, Math.max(edges.get(to) || 0, weight));
135
+ }
136
+ }
137
+ return {
138
+ edges: [...edges].map(([to, weight]) => ({ to, weight })),
139
+ unresolved,
140
+ };
141
+ }
142
+
143
+ /** @returns {Array<{to: string, weight: number}>} */
144
+ resolveOne(node, ref) {
145
+ switch (node.language) {
146
+ case 'javascript':
147
+ case 'typescript':
148
+ return single(this.resolveJs(node.path, ref.spec));
149
+ case 'python':
150
+ return this.resolvePython(node.path, ref).map((to) => ({ to, weight: 1 }));
151
+ case 'go':
152
+ return spread(this.resolveGo(node.path, ref.spec));
153
+ case 'java':
154
+ case 'kotlin':
155
+ case 'scala':
156
+ return this.resolveJvm(ref);
157
+ case 'ruby':
158
+ return single(this.resolveRuby(node.path, ref));
159
+ case 'php':
160
+ return this.resolvePhp(node, ref);
161
+ case 'c':
162
+ case 'cpp':
163
+ return single(this.resolveInclude(node.path, ref.include));
164
+ case 'rust':
165
+ return single(this.resolveRust(node.path, ref));
166
+ case 'csharp':
167
+ return this.resolveCSharp(ref);
168
+ default:
169
+ return [];
170
+ }
171
+ }
172
+
173
+ has(filePath) {
174
+ return filePath !== null && this.nodes.has(filePath);
175
+ }
176
+
177
+ firstExisting(candidates) {
178
+ return candidates.find((candidate) => this.has(candidate)) ?? null;
179
+ }
180
+
181
+ existsOnDisk(relativePath) {
182
+ if (!this.existsCache.has(relativePath)) {
183
+ this.existsCache.set(relativePath, fs.existsSync(path.join(this.root, relativePath)));
184
+ }
185
+ return this.existsCache.get(relativePath);
186
+ }
187
+
188
+ readText(relativePath) {
189
+ try {
190
+ return fs.readFileSync(path.join(this.root, relativePath), 'utf-8');
191
+ } catch (_error) {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ // ---------------------------------------------------------------- JavaScript/TypeScript
197
+
198
+ resolveJs(fromPath, rawSpec) {
199
+ // `./worker.js?url` (bundler query), but `#internal` (subpath import) stays whole
200
+ const cut = rawSpec.slice(1).search(/[?#]/);
201
+ const spec = cut === -1 ? rawSpec : rawSpec.slice(0, cut + 1);
202
+ let bases;
203
+ if (spec === '.' || spec === '..' || spec.startsWith('./') || spec.startsWith('../')) {
204
+ bases = [joinInside(dirOf(fromPath), spec)];
205
+ } else {
206
+ bases = this.aliasTargets(spec);
207
+ }
208
+ for (const base of bases) {
209
+ if (base === null) continue;
210
+ const hit = this.firstExisting(this.jsCandidates(base));
211
+ if (hit) return hit;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ jsCandidates(base) {
217
+ const candidates = [base];
218
+ const ext = posix.extname(base);
219
+ for (const tsExt of JS_TO_TS[ext] || []) candidates.push(base.slice(0, -ext.length) + tsExt);
220
+ for (const extension of JS_EXTENSIONS) candidates.push(base + extension);
221
+ const index = base === '' ? 'index' : `${base}/index`;
222
+ for (const extension of JS_EXTENSIONS) candidates.push(index + extension);
223
+ return candidates;
224
+ }
225
+
226
+ /** Targets of a bare specifier under the `paths`/`baseUrl` of tsconfig.json/jsconfig.json */
227
+ aliasTargets(spec) {
228
+ if (this.jsAliases === undefined) this.jsAliases = this.loadJsAliases();
229
+ if (!this.jsAliases) return [];
230
+ const { baseUrl, paths, explicitBaseUrl } = this.jsAliases;
231
+ const targets = [];
232
+ for (const [pattern, replacements] of paths) {
233
+ const star = pattern.indexOf('*');
234
+ let captured = null;
235
+ if (star === -1) {
236
+ if (spec === pattern) captured = '';
237
+ } else {
238
+ const prefix = pattern.slice(0, star);
239
+ const suffix = pattern.slice(star + 1);
240
+ if (
241
+ spec.startsWith(prefix) &&
242
+ spec.endsWith(suffix) &&
243
+ spec.length >= prefix.length + suffix.length
244
+ ) {
245
+ captured = spec.slice(prefix.length, spec.length - suffix.length);
246
+ }
247
+ }
248
+ if (captured === null) continue;
249
+ for (const replacement of replacements) {
250
+ targets.push(joinInside(baseUrl, String(replacement).split('*').join(captured)));
251
+ }
252
+ }
253
+ if (explicitBaseUrl) targets.push(joinInside(baseUrl, spec));
254
+ return targets;
255
+ }
256
+
257
+ loadJsAliases() {
258
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
259
+ const text = this.readText(name);
260
+ if (text === null) continue;
261
+ try {
262
+ const options = JSON.parse(stripJsonComments(text)).compilerOptions || {};
263
+ const baseUrl = joinInside(options.baseUrl || '.');
264
+ if (baseUrl === null) return null;
265
+ const paths = Object.entries(options.paths || {}).filter(([, value]) =>
266
+ Array.isArray(value)
267
+ );
268
+ return { baseUrl, paths, explicitBaseUrl: Boolean(options.baseUrl) };
269
+ } catch (_error) {
270
+ return null;
271
+ }
272
+ }
273
+ return null;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------- Python
277
+
278
+ resolvePython(fromPath, ref) {
279
+ const names = ref.names.filter((name) => name !== '*');
280
+ const dots = ref.module.match(/^\.*/)[0].length;
281
+ const dotted = ref.module.slice(dots);
282
+
283
+ if (dots > 0) {
284
+ let base = dirOf(fromPath);
285
+ for (let level = 1; level < dots; level++) {
286
+ if (base === '') return [];
287
+ base = dirOf(base);
288
+ }
289
+ return this.pythonTargets([base], dotted, names);
290
+ }
291
+ return this.pythonTargets(this.pythonRoots(fromPath), dotted, names);
292
+ }
293
+
294
+ /** Module (and imported submodules) under the first source root that has it */
295
+ pythonTargets(roots, dotted, names) {
296
+ for (const root of roots) {
297
+ const found = [];
298
+ const module = dotted ? this.pythonModule(root, dotted) : null;
299
+ if (module) found.push(module);
300
+ for (const name of names) {
301
+ const submodule = this.pythonModule(root, dotted ? `${dotted}.${name}` : name);
302
+ if (submodule) found.push(submodule);
303
+ }
304
+ if (found.length === 0 && !dotted) {
305
+ const init = this.firstExisting([joinInside(root, '__init__.py')]);
306
+ if (init) found.push(init);
307
+ }
308
+ if (found.length > 0) return [...new Set(found)];
309
+ }
310
+ return [];
311
+ }
312
+
313
+ pythonModule(root, dotted) {
314
+ const base = joinInside(root, dotted.split('.').join('/'));
315
+ if (base === null) return null;
316
+ return this.firstExisting([`${base}.py`, joinInside(base, '__init__.py')]);
317
+ }
318
+
319
+ /** Project root, src/, the folder above the importer's top-level package, its own folder */
320
+ pythonRoots(fromPath) {
321
+ let dir = dirOf(fromPath);
322
+ while (dir !== '' && this.has(joinInside(dir, '__init__.py'))) dir = dirOf(dir);
323
+ return [...new Set(['', 'src', dir, dirOf(fromPath)])];
324
+ }
325
+
326
+ // -------------------------------------------------------------------------------- Go
327
+
328
+ resolveGo(fromPath, spec) {
329
+ if (this.goModules === null) this.goModules = this.findGoModules();
330
+ let best = null;
331
+ for (const module of this.goModules) {
332
+ if (spec !== module.path && !spec.startsWith(`${module.path}/`)) continue;
333
+ if (!best || module.path.length > best.path.length) best = module;
334
+ }
335
+ if (!best) return [];
336
+ const dir = joinInside(best.dir, spec.slice(best.path.length).replace(/^\//, ''));
337
+ if (dir === null) return [];
338
+ return (this.dirFiles.get(dir) || []).filter(
339
+ (file) => file.endsWith('.go') && !file.endsWith('_test.go') && file !== fromPath
340
+ );
341
+ }
342
+
343
+ /** Every go.mod at or above a folder holding Go files */
344
+ findGoModules() {
345
+ const modules = new Map();
346
+ for (const [dir, files] of this.dirFiles) {
347
+ if (!files.some((file) => file.endsWith('.go'))) continue;
348
+ let current = dir;
349
+ for (;;) {
350
+ const goMod = current === '' ? 'go.mod' : `${current}/go.mod`;
351
+ if (!modules.has(current) && this.existsOnDisk(goMod)) {
352
+ const match = (this.readText(goMod) || '').match(/^\s*module\s+(\S+)/m);
353
+ modules.set(current, match ? { dir: current, path: match[1].replace(/"/g, '') } : null);
354
+ }
355
+ if (current === '') break;
356
+ current = dirOf(current);
357
+ }
358
+ }
359
+ return [...modules.values()].filter(Boolean);
360
+ }
361
+
362
+ // -------------------------------------------------------------- Java / Kotlin / Scala
363
+
364
+ resolveJvm(ref) {
365
+ if (ref.members.length > 0) {
366
+ return ref.members.flatMap((member) => this.resolveTypeName(`${ref.name}.${member}`));
367
+ }
368
+ if (ref.wildcard) {
369
+ if (this.packageIndex.has(ref.name)) return spread(this.packageIndex.get(ref.name));
370
+ return single(this.typeIndex.get(ref.name) ?? null);
371
+ }
372
+ return this.resolveTypeName(ref.name);
373
+ }
374
+
375
+ /** `a.b.Outer.Inner` → the file of a.b.Outer; `a.b.topLevelFun` → files defining it */
376
+ resolveTypeName(name) {
377
+ const segments = name.split('.');
378
+ for (let length = segments.length; length >= 2; length--) {
379
+ const hit = this.typeIndex.get(segments.slice(0, length).join('.'));
380
+ if (hit) return single(hit);
381
+ }
382
+ return this.membersOf(this.packageIndex.get(segments.slice(0, -1).join('.')), segments.at(-1));
383
+ }
384
+
385
+ /** Files of a package that define a function named `member` */
386
+ membersOf(files, member) {
387
+ if (!files) return [];
388
+ return spread(
389
+ files.filter((file) => this.nodes.get(file).methods?.some((m) => m.name === member))
390
+ );
391
+ }
392
+
393
+ // ------------------------------------------------------------------------------ Ruby
394
+
395
+ resolveRuby(fromPath, ref) {
396
+ const spec = ref.spec.replace(/\.rb$/, '');
397
+ const bases = ref.relative
398
+ ? [joinInside(dirOf(fromPath), spec)]
399
+ : [joinInside('lib', spec), joinInside(spec), joinInside(dirOf(fromPath), spec)];
400
+ return this.firstExisting(bases.filter((base) => base !== null).map((base) => `${base}.rb`));
401
+ }
402
+
403
+ // ------------------------------------------------------------------------------- PHP
404
+
405
+ resolvePhp(node, ref) {
406
+ if (ref.include !== undefined) {
407
+ const spec = ref.include;
408
+ const candidates = ref.fromDir
409
+ ? [joinInside(dirOf(node.path), spec.replace(/^\/+/, ''))]
410
+ : spec.startsWith('/')
411
+ ? []
412
+ : [joinInside(dirOf(node.path), spec), joinInside(spec)];
413
+ return single(this.firstExisting(candidates));
414
+ }
415
+
416
+ const name = ref.name.replace(/^\\/, '');
417
+ const namespace = node.declarations?.[0];
418
+ const names = [name];
419
+ if (namespace && !name.includes('\\')) names.push(`${namespace}\\${name}`);
420
+ for (const candidate of names) {
421
+ const hit = this.phpIndex.get(candidate.toLowerCase());
422
+ if (hit) return single(hit);
423
+ }
424
+ // `use function App\Support\helper;`: the namespace's files defining it
425
+ const parts = name.split('\\');
426
+ return this.membersOf(
427
+ this.phpNamespaces.get(parts.slice(0, -1).join('\\').toLowerCase()),
428
+ parts.at(-1)
429
+ );
430
+ }
431
+
432
+ // ----------------------------------------------------------------------------- C/C++
433
+
434
+ resolveInclude(fromPath, include) {
435
+ const direct = this.firstExisting([
436
+ joinInside(dirOf(fromPath), include),
437
+ joinInside(include),
438
+ joinInside('include', include),
439
+ joinInside('src', include),
440
+ ]);
441
+ if (direct) return direct;
442
+
443
+ // Include directories we do not know: any file whose path ends with the include,
444
+ // the one closest to the includer
445
+ const normalized = posix.normalize(include).replace(/^(\.\.\/)+/, '');
446
+ const matches = (this.byBasename.get(posix.basename(normalized)) || []).filter(
447
+ (file) => file === normalized || file.endsWith(`/${normalized}`)
448
+ );
449
+ let best = null;
450
+ for (const file of matches) {
451
+ if (!best || sharedDepth(file, fromPath) > sharedDepth(best, fromPath)) best = file;
452
+ }
453
+ return best;
454
+ }
455
+
456
+ // ------------------------------------------------------------------------------ Rust
457
+
458
+ resolveRust(fromPath, ref) {
459
+ const moduleDir = this.rustModuleDir(fromPath);
460
+ if (ref.mod !== undefined) {
461
+ return this.firstExisting([
462
+ joinInside(moduleDir, `${ref.mod}.rs`),
463
+ joinInside(moduleDir, ref.mod, 'mod.rs'),
464
+ ]);
465
+ }
466
+
467
+ let startDir;
468
+ const segments = [...ref.segments];
469
+ if (ref.crate !== undefined) {
470
+ startDir = this.crateSources().get(ref.crate);
471
+ if (startDir === undefined) return null;
472
+ } else if (ref.anchor === 'crate') {
473
+ startDir = this.rustSourceDir(fromPath);
474
+ } else {
475
+ startDir = moduleDir;
476
+ if (ref.anchor === 'super') startDir = dirOf(moduleDir);
477
+ while (segments[0] === 'super') {
478
+ segments.shift();
479
+ startDir = dirOf(startDir);
480
+ }
481
+ }
482
+ if (startDir === null) return null;
483
+
484
+ let dir = startDir;
485
+ let found = null;
486
+ for (const segment of segments) {
487
+ const hit = this.firstExisting([
488
+ joinInside(dir, `${segment}.rs`),
489
+ joinInside(dir, segment, 'mod.rs'),
490
+ ]);
491
+ if (!hit) break;
492
+ found = hit;
493
+ dir = joinInside(dir, segment);
494
+ }
495
+ return found ?? this.rustModuleFile(startDir);
496
+ }
497
+
498
+ /** Folder holding the submodules of a file: its own folder for crate roots and mod.rs */
499
+ rustModuleDir(fromPath) {
500
+ const base = posix.basename(fromPath);
501
+ const dir = dirOf(fromPath);
502
+ if (['lib.rs', 'main.rs', 'mod.rs', 'build.rs'].includes(base)) return dir;
503
+ if (RUST_ROOT_DIRS.has(posix.basename(dir))) return dir;
504
+ return fromPath.slice(0, -'.rs'.length);
505
+ }
506
+
507
+ /** File of the module whose submodules live in `dir` */
508
+ rustModuleFile(dir) {
509
+ return this.firstExisting([
510
+ `${dir}.rs`,
511
+ joinInside(dir, 'mod.rs'),
512
+ joinInside(dir, 'lib.rs'),
513
+ joinInside(dir, 'main.rs'),
514
+ ]);
515
+ }
516
+
517
+ /** `src/` of the crate (nearest Cargo.toml) a file belongs to */
518
+ rustSourceDir(fromPath) {
519
+ const crateDir = this.cargoDir(dirOf(fromPath));
520
+ return crateDir === null ? null : joinInside(crateDir, 'src');
521
+ }
522
+
523
+ cargoDir(dir) {
524
+ if (this.cargoCache.has(dir)) return this.cargoCache.get(dir);
525
+ let result = null;
526
+ if (this.existsOnDisk(dir === '' ? 'Cargo.toml' : `${dir}/Cargo.toml`)) result = dir;
527
+ else if (dir !== '') result = this.cargoDir(dirOf(dir));
528
+ this.cargoCache.set(dir, result);
529
+ return result;
530
+ }
531
+
532
+ /** Workspace crates by name (`my-crate` is used as `my_crate`) → their src/ folder */
533
+ crateSources() {
534
+ if (this.cratesByName) return this.cratesByName;
535
+ this.cratesByName = new Map();
536
+ const crateDirs = new Set();
537
+ for (const file of this.nodes.keys()) {
538
+ if (!file.endsWith('.rs')) continue;
539
+ const crateDir = this.cargoDir(dirOf(file));
540
+ if (crateDir !== null) crateDirs.add(crateDir);
541
+ }
542
+ for (const crateDir of crateDirs) {
543
+ const manifest = this.readText(crateDir === '' ? 'Cargo.toml' : `${crateDir}/Cargo.toml`);
544
+ const match = manifest?.match(/^\s*\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/m);
545
+ if (match) this.cratesByName.set(match[1].replace(/-/g, '_'), joinInside(crateDir, 'src'));
546
+ }
547
+ return this.cratesByName;
548
+ }
549
+
550
+ // ---------------------------------------------------------------------------- C#
551
+
552
+ resolveCSharp(ref) {
553
+ if (!ref.isStatic && this.packageIndex.has(ref.name)) {
554
+ return spread(this.packageIndex.get(ref.name));
555
+ }
556
+ return single(this.typeIndex.get(ref.name) ?? null);
557
+ }
558
+ }
559
+
560
+ function addTo(map, key, value) {
561
+ if (!map.has(key)) map.set(key, []);
562
+ map.get(key).push(value);
563
+ }
564
+
565
+ function single(target) {
566
+ return target ? [{ to: target, weight: 1 }] : [];
567
+ }
568
+
569
+ /** A dependency on a package of n files: each file gets 1/n of the edge */
570
+ function spread(targets) {
571
+ return targets.map((to) => ({ to, weight: 1 / targets.length }));
572
+ }
573
+
574
+ export default ImportResolver;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Token budget for `pack --budget`
3
+ *
4
+ * Files are ranked with the repository-map ranking of `ctxman map` (lib/map): PageRank over
5
+ * the import graph blended with git change frequency and symbol references, so the files
6
+ * the rest of the code depends on come first. Files the graph does not cover (documentation,
7
+ * configuration, data, languages without import parsing, files the map skips as minified)
8
+ * follow in path order. They are packed greedily: a file that does not fit is skipped and
9
+ * smaller, lower-ranked files can still use the room. The per-file costs are estimates, so
10
+ * the rendered document is then counted exactly and files are dropped (lowest rank first)
11
+ * until it fits: the result never exceeds the budget.
12
+ */
13
+
14
+ import { buildRepoGraph, rankFiles } from '../map/index.js';
15
+ import { PackError } from './select.js';
16
+
17
+ // Exact re-renders spent trying to use the room left after the estimate-based pass
18
+ const MAX_TOP_UP_ATTEMPTS = 8;
19
+
20
+ /**
21
+ * Rank entries most important first
22
+ * @param {string} root - Project root (the import graph and git history are read from it)
23
+ * @param {Array<{path: string}>} entries - Project-relative paths with `/` separators
24
+ * @param {object} [options]
25
+ * @param {number} [options.history] - Commits of git history to mine (map default: 300)
26
+ * @returns {{ranked: Array, scores: Map<string, number>}} The same entry objects, reordered,
27
+ * and the map score (0..1) of each ranked path
28
+ */
29
+ export function rankEntries(root, entries, options = {}) {
30
+ const graph = buildRepoGraph(root, { history: options.history, redact: false });
31
+ const rows = rankFiles(graph);
32
+ const position = new Map(rows.map((row, i) => [row.path, i]));
33
+ const scores = new Map(rows.map((row) => [row.path, row.score]));
34
+
35
+ const inGraph = entries.filter((entry) => position.has(entry.path));
36
+ const rest = entries.filter((entry) => !position.has(entry.path));
37
+ inGraph.sort((a, b) => position.get(a.path) - position.get(b.path));
38
+ rest.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
39
+ return { ranked: [...inGraph, ...rest], scores };
40
+ }
41
+
42
+ /**
43
+ * Choose the entries that fit in the budget
44
+ * @param {Array} entries - Candidates in output order
45
+ * @param {Array} ranked - The same entries, most important first
46
+ * @param {number} budget - Token limit for the whole rendered document
47
+ * @param {object} helpers
48
+ * @param {function(Array, Array): {text: string, tokens: number}} helpers.measure - Render
49
+ * and count a document with these packed entries (output order) and omitted entries (rank
50
+ * order)
51
+ * @param {function(object): number} helpers.cost - Estimated tokens an entry adds
52
+ * @returns {{packed: Array, omitted: Array, text: string, tokens: number}}
53
+ */
54
+ export function fitToBudget(entries, ranked, budget, { measure, cost }) {
55
+ const selected = new Set();
56
+ const evaluate = () =>
57
+ measure(
58
+ entries.filter((entry) => selected.has(entry)),
59
+ ranked.filter((entry) => !selected.has(entry))
60
+ );
61
+
62
+ let current = evaluate();
63
+ if (current.tokens > budget) {
64
+ throw new PackError(
65
+ 'BUDGET_TOO_SMALL',
66
+ `The document needs ${current.tokens} tokens without any file (header, tree, omitted ` +
67
+ `list, instructions); the budget is ${budget}`
68
+ );
69
+ }
70
+
71
+ let used = current.tokens;
72
+ for (const entry of ranked) {
73
+ const entryCost = cost(entry);
74
+ if (used + entryCost <= budget) {
75
+ selected.add(entry);
76
+ used += entryCost;
77
+ }
78
+ }
79
+ current = evaluate();
80
+
81
+ // Estimates can be a little low: drop the lowest-ranked files until the exact count fits
82
+ while (current.tokens > budget) {
83
+ // Without files the document was measured to fit, so this cannot loop forever
84
+ if (!selected.size) throw new PackError('BUDGET_TOO_SMALL', `Cannot fit in ${budget} tokens`);
85
+ let excess = current.tokens - budget;
86
+ for (let i = ranked.length - 1; i >= 0 && excess > 0; i--) {
87
+ if (selected.delete(ranked[i])) excess -= cost(ranked[i]);
88
+ }
89
+ current = evaluate();
90
+ }
91
+
92
+ // ... or a little high: try the best-ranked files left out while there may be room
93
+ let attempts = 0;
94
+ for (const entry of ranked) {
95
+ if (attempts >= MAX_TOP_UP_ATTEMPTS) break;
96
+ if (selected.has(entry) || current.tokens + entry.tokens > budget) continue;
97
+ attempts++;
98
+ selected.add(entry);
99
+ const next = evaluate();
100
+ if (next.tokens <= budget) current = next;
101
+ else selected.delete(entry);
102
+ }
103
+
104
+ return {
105
+ packed: entries.filter((entry) => selected.has(entry)),
106
+ omitted: ranked.filter((entry) => !selected.has(entry)),
107
+ ...current,
108
+ };
109
+ }
110
+
111
+ export default { rankEntries, fitToBudget };