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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- 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
|
+
}
|