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,346 @@
1
+ /**
2
+ * Rendering for `ctxman map`: the ranked files with their function signatures, trimmed
3
+ * to a token budget, or the dependency graph itself
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import MethodAnalyzer from '../analyzers/method-analyzer.js';
9
+ import TokenUtils from '../utils/token-utils.js';
10
+ import { buildRepoGraph, DEFAULT_HISTORY } from './graph.js';
11
+ import { rankFiles, resolveFocus, DEFAULT_WEIGHTS, DEFAULT_DAMPING } from './rank.js';
12
+
13
+ export const DEFAULT_TOKENS = 2048;
14
+ export const STYLES = ['text', 'markdown', 'json', 'dot'];
15
+ export const ORDERS = ['rank', 'path'];
16
+ const MARKDOWN_TITLE = '# Repository map';
17
+ // No file block is smaller than this, so a smaller remainder ends the selection
18
+ const MIN_BLOCK_TOKENS = 4;
19
+ // Fewer than one token per this many characters never happens in practice: a block
20
+ // whose characters / 10 exceed the remaining budget is skipped without counting it
21
+ const CHARS_PER_TOKEN_CEILING = 10;
22
+
23
+ const countTokens = (text) => (text ? TokenUtils.calculate(text, 'map.txt') : 0);
24
+ const round = (value, digits) => Number(value.toFixed(digits));
25
+
26
+ /**
27
+ * Check and complete the options of renderRepoMap()
28
+ * @param {Object} options
29
+ * @returns {Object}
30
+ */
31
+ export function normalizeMapOptions(options = {}) {
32
+ const normalized = {
33
+ tokens: options.tokens ?? DEFAULT_TOKENS,
34
+ focus: options.focus ?? [],
35
+ history: options.history ?? DEFAULT_HISTORY,
36
+ order: options.order ?? 'rank',
37
+ importantLast: Boolean(options.importantLast),
38
+ style: options.style ?? 'text',
39
+ graph: Boolean(options.graph),
40
+ redact: options.redact !== false,
41
+ weights: { ...DEFAULT_WEIGHTS, ...options.weights },
42
+ damping: options.damping ?? DEFAULT_DAMPING,
43
+ maxFileBytes: options.maxFileBytes,
44
+ };
45
+ if (!Number.isInteger(normalized.tokens) || normalized.tokens < 1) {
46
+ throw new Error(`tokens must be a positive integer, got ${JSON.stringify(options.tokens)}`);
47
+ }
48
+ if (!Number.isInteger(normalized.history) || normalized.history < 0) {
49
+ throw new Error(
50
+ `history must be a non-negative integer, got ${JSON.stringify(options.history)}`
51
+ );
52
+ }
53
+ if (!Array.isArray(normalized.focus)) normalized.focus = [normalized.focus];
54
+ if (!STYLES.includes(normalized.style)) {
55
+ throw new Error(`style must be one of ${STYLES.join(', ')}, got ${normalized.style}`);
56
+ }
57
+ if (!ORDERS.includes(normalized.order)) {
58
+ throw new Error(`order must be one of ${ORDERS.join(', ')}, got ${normalized.order}`);
59
+ }
60
+ if (normalized.importantLast && normalized.order !== 'rank') {
61
+ throw new Error('importantLast applies to rank order only');
62
+ }
63
+ for (const [name, weight] of Object.entries(normalized.weights)) {
64
+ if (!(weight >= 0)) throw new Error(`weight ${name} must be a non-negative number`);
65
+ }
66
+ return normalized;
67
+ }
68
+
69
+ /**
70
+ * Build, rank and render the repository map of a project
71
+ * @param {string} root - Project directory
72
+ * @param {Object} [options]
73
+ * @param {number} [options.tokens=2048] - Budget for the rendered map (TokenUtils count)
74
+ * @param {string[]} [options.focus] - Files or folders to bias the ranking toward
75
+ * @param {number} [options.history=300] - Commits of git history to mine (0 disables)
76
+ * @param {'rank'|'path'} [options.order='rank'] - Order of the files in the output
77
+ * @param {boolean} [options.importantLast=false] - Highest-ranked file at the end
78
+ * @param {'text'|'markdown'|'json'|'dot'} [options.style='text']
79
+ * @param {boolean} [options.graph=false] - Render the dependency edges instead of the map
80
+ * @param {boolean} [options.redact=true] - Mask secrets in signatures
81
+ * @param {{pagerank?: number, history?: number, symbols?: number}} [options.weights]
82
+ * @returns {{output: string, style: string, budget: number, tokens: number,
83
+ * files: Array<Object>, ranked: Array<Object>, graph: import('./graph.js').RepoGraph,
84
+ * skipped: number, warnings: string[], stats: Object}}
85
+ */
86
+ export function renderRepoMap(root, options = {}) {
87
+ const opts = normalizeMapOptions(options);
88
+ const started = performance.now();
89
+ const graph = buildRepoGraph(root, {
90
+ history: opts.history,
91
+ redact: opts.redact,
92
+ maxFileBytes: opts.maxFileBytes,
93
+ });
94
+
95
+ const warnings = [];
96
+ const focus = resolveFocus(graph, opts.focus);
97
+ for (const entry of focus.unmatched) warnings.push(`focus path matches no mapped file: ${entry}`);
98
+ if (opts.history > 0 && !graph.history.available) {
99
+ warnings.push(`git history unavailable (${graph.history.reason}); ranking without it`);
100
+ }
101
+
102
+ let timer = performance.now();
103
+ const ranked = rankFiles(graph, {
104
+ focus: focus.files,
105
+ weights: opts.weights,
106
+ damping: opts.damping,
107
+ });
108
+ graph.stats.timings.rank = performance.now() - timer;
109
+
110
+ timer = performance.now();
111
+ const result = {
112
+ style: opts.style,
113
+ budget: opts.tokens,
114
+ graph,
115
+ ranked,
116
+ warnings,
117
+ };
118
+ if (opts.graph || opts.style === 'dot') {
119
+ result.output = renderGraph(graph, ranked, opts.style);
120
+ result.tokens = countTokens(result.output);
121
+ result.files = [];
122
+ result.skipped = 0;
123
+ } else {
124
+ const selection = selectFiles(graph, ranked, opts);
125
+ result.files = selection.files;
126
+ result.skipped = selection.skipped;
127
+ result.tokens = selection.tokens;
128
+ result.output =
129
+ opts.style === 'json' ? renderJson(graph, ranked, selection, opts) : selection.output;
130
+ if (selection.files.length === 0 && ranked.length > 0) {
131
+ warnings.push(`no file fits in ${opts.tokens} tokens`);
132
+ }
133
+ }
134
+ graph.stats.timings.render = performance.now() - timer;
135
+ graph.stats.timings.total = performance.now() - started;
136
+ result.stats = graph.stats;
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Greedy selection, best rank first: every whole file block that still fits the budget.
142
+ * The sum of block counts can differ from the count of the joined text by a token here
143
+ * and there, so the joined text is counted again and the lowest-ranked files are dropped
144
+ * until it fits.
145
+ */
146
+ function selectFiles(graph, ranked, opts) {
147
+ const style = opts.style === 'markdown' ? 'markdown' : 'text';
148
+ const nodes = new Map(graph.nodes.map((node) => [node.path, node]));
149
+ const analyzer = new MethodAnalyzer();
150
+ const separator = style === 'markdown' ? '\n\n' : '\n';
151
+ const chosen = [];
152
+ let used = style === 'markdown' ? countTokens(`${MARKDOWN_TITLE}${separator}`) : 0;
153
+ let skipped = 0;
154
+
155
+ for (const row of ranked) {
156
+ const remaining = opts.tokens - used;
157
+ if (remaining < MIN_BLOCK_TOKENS) {
158
+ skipped += ranked.length - row.rank + 1;
159
+ break;
160
+ }
161
+ const node = nodes.get(row.path);
162
+ const characters = node.methods.reduce(
163
+ (sum, method) => sum + method.signature.length,
164
+ row.path.length
165
+ );
166
+ if (characters / CHARS_PER_TOKEN_CEILING > remaining) {
167
+ skipped++;
168
+ continue;
169
+ }
170
+ // Token counts need the whole file and every method body counted; the block with
171
+ // zeros in their place costs about as much and is cheap to count, so files that
172
+ // cannot fit are skipped before that work
173
+ const skeleton = renderBlock(style, {
174
+ path: row.path,
175
+ score: row.score,
176
+ tokens: 0,
177
+ methods: node.methods.map((method) => ({ ...method, tokens: 0 })),
178
+ });
179
+ if (countTokens(skeleton + separator) > remaining) {
180
+ skipped++;
181
+ continue;
182
+ }
183
+ const entry = describeFile(graph, node, row, analyzer);
184
+ const block = renderBlock(style, entry);
185
+ const cost = countTokens(block + separator);
186
+ if (cost > remaining) {
187
+ skipped++;
188
+ continue;
189
+ }
190
+ chosen.push({ entry, block });
191
+ used += cost;
192
+ }
193
+
194
+ for (;;) {
195
+ const ordered = displayOrder(chosen, opts);
196
+ const blocks = ordered.map((item) => item.block);
197
+ if (style === 'markdown' && blocks.length > 0) blocks.unshift(MARKDOWN_TITLE);
198
+ const output = blocks.length > 0 ? `${blocks.join(separator)}\n` : '';
199
+ const tokens = countTokens(output);
200
+ if (tokens <= opts.tokens || chosen.length === 0) {
201
+ return { files: ordered.map((item) => item.entry), output, tokens, skipped };
202
+ }
203
+ chosen.pop();
204
+ skipped++;
205
+ }
206
+ }
207
+
208
+ /** rank (best first), path, or rank with the best last */
209
+ function displayOrder(chosen, opts) {
210
+ if (opts.order === 'path') {
211
+ return [...chosen].sort((a, b) =>
212
+ a.entry.path < b.entry.path ? -1 : a.entry.path > b.entry.path ? 1 : 0
213
+ );
214
+ }
215
+ return opts.importantLast ? [...chosen].reverse() : [...chosen];
216
+ }
217
+
218
+ /** Map entry of a file: its tokens and the tokens of each of its methods */
219
+ function describeFile(graph, node, row, analyzer) {
220
+ const absolute = path.join(graph.root, node.path);
221
+ let content = '';
222
+ try {
223
+ content = fs.readFileSync(absolute, 'utf-8');
224
+ } catch (_error) {
225
+ // Deleted since the scan: keep the signatures, count nothing
226
+ }
227
+ const sources = content ? analyzer.extractMethodSources(content, node.methods, absolute) : [];
228
+ return {
229
+ path: node.path,
230
+ rank: row.rank,
231
+ score: row.score,
232
+ tokens: content ? TokenUtils.calculate(content, absolute) : 0,
233
+ methods: node.methods.map((method, i) => ({
234
+ line: method.line,
235
+ name: method.name,
236
+ signature: method.signature,
237
+ tokens: sources[i] ? TokenUtils.calculate(sources[i], absolute) : 0,
238
+ })),
239
+ };
240
+ }
241
+
242
+ /** One file of the map */
243
+ export function renderBlock(style, entry) {
244
+ const score = entry.score.toFixed(3);
245
+ if (style === 'markdown') {
246
+ const lines = [`## ${entry.path}`, `score ${score} · ${entry.tokens} tokens`];
247
+ if (entry.methods.length > 0) lines.push('');
248
+ for (const method of entry.methods) {
249
+ lines.push(`- L${method.line} ${inlineCode(method.signature)} (${method.tokens} tokens)`);
250
+ }
251
+ return lines.join('\n');
252
+ }
253
+ const lines = [`${entry.path} (score ${score}, ${entry.tokens} tokens)`];
254
+ for (const method of entry.methods) {
255
+ lines.push(` ${method.line}: ${method.signature} [${method.tokens} tok]`);
256
+ }
257
+ return lines.join('\n');
258
+ }
259
+
260
+ /** Markdown inline code that survives backticks in the text */
261
+ function inlineCode(text) {
262
+ if (!text.includes('`')) return `\`${text}\``;
263
+ return `\`\` ${text} \`\``;
264
+ }
265
+
266
+ function renderJson(graph, ranked, selection, opts) {
267
+ const included = new Set(selection.files.map((file) => file.path));
268
+ const document = {
269
+ root: graph.root,
270
+ budget: opts.tokens,
271
+ tokens: selection.tokens,
272
+ order: opts.order,
273
+ importantLast: opts.importantLast,
274
+ weights: opts.weights,
275
+ damping: opts.damping,
276
+ history: {
277
+ requested: graph.history.requested,
278
+ commits: graph.history.commits,
279
+ available: graph.history.available,
280
+ },
281
+ files: selection.files.map((file) => ({ ...file, score: round(file.score, 6) })),
282
+ nodes: ranked.map((row) => ({
283
+ path: row.path,
284
+ rank: row.rank,
285
+ score: round(row.score, 6),
286
+ pagerank: round(row.pagerank, 8),
287
+ changes: row.changes,
288
+ symbolRefs: round(row.symbolRefs, 4),
289
+ focus: row.focus,
290
+ included: included.has(row.path),
291
+ })),
292
+ edges: graph.edges.map((edge) => ({ ...edge, weight: round(edge.weight, 6) })),
293
+ stats: {
294
+ files: graph.stats.files,
295
+ edges: graph.stats.edges,
296
+ unresolvedImports: graph.stats.unresolvedImports,
297
+ skippedFiles: graph.stats.skipped,
298
+ },
299
+ };
300
+ return `${JSON.stringify(document, null, 2)}\n`;
301
+ }
302
+
303
+ /**
304
+ * The dependency edges: `a -> b` lines (text), a fenced block (markdown), nodes and edges
305
+ * (json) or a Graphviz digraph (dot)
306
+ */
307
+ export function renderGraph(graph, ranked, style) {
308
+ const weightNote = (edge) => (edge.weight === 1 ? '' : ` (${round(edge.weight, 3)})`);
309
+ if (style === 'json') {
310
+ return `${JSON.stringify(
311
+ {
312
+ root: graph.root,
313
+ nodes: ranked.map((row) => ({
314
+ path: row.path,
315
+ rank: row.rank,
316
+ score: round(row.score, 6),
317
+ })),
318
+ edges: graph.edges.map((edge) => ({ ...edge, weight: round(edge.weight, 6) })),
319
+ },
320
+ null,
321
+ 2
322
+ )}\n`;
323
+ }
324
+ if (style === 'dot') {
325
+ const escape = (value) => value.replace(/["\\]/g, '\\$&');
326
+ const quote = (value) => `"${escape(value)}"`;
327
+ const lines = ['digraph repo {', ' rankdir=LR;', ' node [shape=box, fontsize=10];'];
328
+ const byPath = [...ranked].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
329
+ for (const row of byPath) {
330
+ // \n in a DOT string is a line break in the label
331
+ const label = `"${escape(row.path)}\\n${row.score.toFixed(3)}"`;
332
+ lines.push(` ${quote(row.path)} [label=${label}];`);
333
+ }
334
+ for (const edge of graph.edges) {
335
+ const attributes = edge.weight === 1 ? '' : ` [weight=${round(edge.weight, 3)}]`;
336
+ lines.push(` ${quote(edge.from)} -> ${quote(edge.to)}${attributes};`);
337
+ }
338
+ lines.push('}');
339
+ return `${lines.join('\n')}\n`;
340
+ }
341
+ const lines = graph.edges.map((edge) => `${edge.from} -> ${edge.to}${weightNote(edge)}`);
342
+ if (style === 'markdown') {
343
+ return `# Dependency graph\n\n\`\`\`text\n${lines.join('\n')}${lines.length ? '\n' : ''}\`\`\`\n`;
344
+ }
345
+ return lines.length > 0 ? `${lines.join('\n')}\n` : '';
346
+ }