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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import extraction for the repository map
|
|
3
|
+
*
|
|
4
|
+
* Regex based, one pass per file: the module references a source file makes (imports,
|
|
5
|
+
* requires, includes, `mod` declarations) and the package or namespace it declares.
|
|
6
|
+
* Resolving the references to files of the repository is the job of resolver.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { ImportTracker } from '../analyzers/import-tracker.js';
|
|
11
|
+
|
|
12
|
+
/** Language of each source extension the map covers */
|
|
13
|
+
export const LANGUAGE_BY_EXTENSION = {
|
|
14
|
+
'.js': 'javascript',
|
|
15
|
+
'.jsx': 'javascript',
|
|
16
|
+
'.mjs': 'javascript',
|
|
17
|
+
'.cjs': 'javascript',
|
|
18
|
+
'.ts': 'typescript',
|
|
19
|
+
'.tsx': 'typescript',
|
|
20
|
+
'.mts': 'typescript',
|
|
21
|
+
'.cts': 'typescript',
|
|
22
|
+
'.py': 'python',
|
|
23
|
+
'.go': 'go',
|
|
24
|
+
'.java': 'java',
|
|
25
|
+
'.kt': 'kotlin',
|
|
26
|
+
'.kts': 'kotlin',
|
|
27
|
+
'.scala': 'scala',
|
|
28
|
+
'.rb': 'ruby',
|
|
29
|
+
'.php': 'php',
|
|
30
|
+
'.c': 'c',
|
|
31
|
+
'.h': 'c',
|
|
32
|
+
'.cc': 'cpp',
|
|
33
|
+
'.cpp': 'cpp',
|
|
34
|
+
'.hpp': 'cpp',
|
|
35
|
+
'.rs': 'rust',
|
|
36
|
+
'.cs': 'csharp',
|
|
37
|
+
'.swift': 'swift',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Language of a file, or null when the map does not cover it
|
|
42
|
+
* @param {string} filePath
|
|
43
|
+
* @returns {string|null}
|
|
44
|
+
*/
|
|
45
|
+
export function languageOf(filePath) {
|
|
46
|
+
return LANGUAGE_BY_EXTENSION[path.extname(filePath).toLowerCase()] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Specifier parsing for JavaScript/TypeScript lives in the import tracker
|
|
50
|
+
const jsTracker = new ImportTracker('.');
|
|
51
|
+
|
|
52
|
+
/** @param {RegExp} pattern @param {string} content @returns {RegExpExecArray[]} */
|
|
53
|
+
function allMatches(pattern, content) {
|
|
54
|
+
const matches = [];
|
|
55
|
+
pattern.lastIndex = 0;
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
58
|
+
matches.push(match);
|
|
59
|
+
if (match[0] === '') pattern.lastIndex++;
|
|
60
|
+
}
|
|
61
|
+
return matches;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Split `a, b as c, d` (or `(a, b)`) into names, dropping aliases */
|
|
65
|
+
function splitNames(list) {
|
|
66
|
+
return list
|
|
67
|
+
.replace(/[()\\]/g, ' ')
|
|
68
|
+
.replace(/#.*$/gm, '')
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((name) => name.trim().split(/\s+/)[0])
|
|
71
|
+
.filter((name) => /^[\w.*]+$/.test(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const PY_IMPORT =
|
|
75
|
+
/^[ \t]*import[ \t]+([\w.]+(?:[ \t]+as[ \t]+\w+)?(?:[ \t]*,[ \t]*[\w.]+(?:[ \t]+as[ \t]+\w+)?)*)/gm;
|
|
76
|
+
const PY_FROM = /^[ \t]*from[ \t]+(\.+[\w.]*|[\w.]+)[ \t]+import[ \t]+(\([^)]*\)|[^\n#;]+)/gm;
|
|
77
|
+
const GO_IMPORT_LINE = /^[ \t]*import[ \t]+(?:[\w.]+[ \t]+)?"([^"\n]+)"/gm;
|
|
78
|
+
const GO_IMPORT_BLOCK = /^[ \t]*import[ \t]*\(([\s\S]*?)\)/gm;
|
|
79
|
+
const GO_BLOCK_ENTRY = /"([^"\n]+)"/g;
|
|
80
|
+
const JVM_PACKAGE = /^[ \t]*package[ \t]+([\w.]+)/m;
|
|
81
|
+
const JVM_IMPORT =
|
|
82
|
+
/^[ \t]*import[ \t]+(?:static[ \t]+)?([\w.]+?)(?:\.(\*|_|\{[^}\n]*\}))?(?=[ \t]*(?:;|\/[/*]|as\b|$))/gm;
|
|
83
|
+
const RUBY_REQUIRE = /^[ \t]*(require_relative|require)[ \t(]+['"]([^'"\n]+)['"]/gm;
|
|
84
|
+
const PHP_NAMESPACE = /^[ \t]*namespace[ \t]+([\w\\]+)[ \t]*[;{]/m;
|
|
85
|
+
const PHP_USE = /^[ \t]*use[ \t]+(?!\()([^;{]+(?:\{[^}]*\})?)[ \t]*;/gm;
|
|
86
|
+
const PHP_INCLUDE =
|
|
87
|
+
/\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*((?:__DIR__|dirname[ \t]*\([ \t]*__FILE__[ \t]*\))[ \t]*\.[ \t]*)?['"]([^'"\n]+)['"]/g;
|
|
88
|
+
const C_INCLUDE = /^[ \t]*#[ \t]*include[ \t]*"([^"\n]+)"/gm;
|
|
89
|
+
const RUST_MOD = /^[ \t]*(?:pub(?:\([^)\n]*\))?[ \t]+)?mod[ \t]+(\w+)[ \t]*;/gm;
|
|
90
|
+
const RUST_PATH = /\b(crate|super|self)((?:::\w+)*)(?:::\{([^{}]*)\})?/g;
|
|
91
|
+
const RUST_USE_EXTERNAL = /^[ \t]*(?:pub(?:\([^)\n]*\))?[ \t]+)?use[ \t]+(\w+)((?:::\w+)*)/gm;
|
|
92
|
+
const CS_NAMESPACE = /^[ \t]*namespace[ \t]+([\w.]+)/gm;
|
|
93
|
+
const CS_USING =
|
|
94
|
+
/^[ \t]*(?:global[ \t]+)?using[ \t]+(static[ \t]+)?(?:\w+[ \t]*=[ \t]*)?([\w.]+)[ \t]*;/gm;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Module references of one file
|
|
98
|
+
* @param {string} language - From languageOf()
|
|
99
|
+
* @param {string} content - Source text
|
|
100
|
+
* @returns {Array<Object>} Raw references; their shape depends on the language
|
|
101
|
+
*/
|
|
102
|
+
export function extractImports(language, content) {
|
|
103
|
+
switch (language) {
|
|
104
|
+
case 'javascript':
|
|
105
|
+
case 'typescript':
|
|
106
|
+
return jsTracker.parseModuleSpecifiers(content).map((spec) => ({ spec }));
|
|
107
|
+
case 'python': {
|
|
108
|
+
const refs = [];
|
|
109
|
+
for (const match of allMatches(PY_IMPORT, content)) {
|
|
110
|
+
for (const name of splitNames(match[1])) refs.push({ module: name, names: [] });
|
|
111
|
+
}
|
|
112
|
+
for (const match of allMatches(PY_FROM, content)) {
|
|
113
|
+
refs.push({ module: match[1], names: splitNames(match[2]) });
|
|
114
|
+
}
|
|
115
|
+
return refs;
|
|
116
|
+
}
|
|
117
|
+
case 'go': {
|
|
118
|
+
const specs = allMatches(GO_IMPORT_LINE, content).map((match) => match[1]);
|
|
119
|
+
for (const block of allMatches(GO_IMPORT_BLOCK, content)) {
|
|
120
|
+
specs.push(...allMatches(GO_BLOCK_ENTRY, block[1]).map((match) => match[1]));
|
|
121
|
+
}
|
|
122
|
+
return [...new Set(specs)].map((spec) => ({ spec }));
|
|
123
|
+
}
|
|
124
|
+
case 'java':
|
|
125
|
+
case 'kotlin':
|
|
126
|
+
case 'scala':
|
|
127
|
+
return allMatches(JVM_IMPORT, content).map((match) => ({
|
|
128
|
+
name: match[1],
|
|
129
|
+
wildcard: match[2] === '*' || match[2] === '_',
|
|
130
|
+
members: match[2]?.startsWith('{')
|
|
131
|
+
? match[2]
|
|
132
|
+
.slice(1, -1)
|
|
133
|
+
.split(',')
|
|
134
|
+
.map((member) => member.split(/=>|\bas\b/)[0].trim())
|
|
135
|
+
.filter((member) => /^\w+$/.test(member))
|
|
136
|
+
: [],
|
|
137
|
+
}));
|
|
138
|
+
case 'ruby':
|
|
139
|
+
return allMatches(RUBY_REQUIRE, content).map((match) => ({
|
|
140
|
+
spec: match[2],
|
|
141
|
+
relative: match[1] === 'require_relative',
|
|
142
|
+
}));
|
|
143
|
+
case 'php': {
|
|
144
|
+
const refs = [];
|
|
145
|
+
for (const match of allMatches(PHP_USE, content)) {
|
|
146
|
+
for (const name of expandPhpUse(match[1])) refs.push({ name });
|
|
147
|
+
}
|
|
148
|
+
for (const match of allMatches(PHP_INCLUDE, content)) {
|
|
149
|
+
refs.push({ include: match[2], fromDir: Boolean(match[1]) });
|
|
150
|
+
}
|
|
151
|
+
return refs;
|
|
152
|
+
}
|
|
153
|
+
case 'c':
|
|
154
|
+
case 'cpp':
|
|
155
|
+
return allMatches(C_INCLUDE, content).map((match) => ({ include: match[1] }));
|
|
156
|
+
case 'rust': {
|
|
157
|
+
const refs = allMatches(RUST_MOD, content).map((match) => ({ mod: match[1] }));
|
|
158
|
+
for (const match of allMatches(RUST_PATH, content)) {
|
|
159
|
+
const segments = match[2].split('::').filter(Boolean);
|
|
160
|
+
if (match[3] === undefined) {
|
|
161
|
+
if (segments.length === 0) continue;
|
|
162
|
+
refs.push({ anchor: match[1], segments });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (const member of match[3].split(',')) {
|
|
166
|
+
const tail = member.trim().split(/\s+as\s+/)[0];
|
|
167
|
+
const extra = /^\w+(?:::\w+)*$/.test(tail) && tail !== 'self' ? tail.split('::') : [];
|
|
168
|
+
refs.push({ anchor: match[1], segments: [...segments, ...extra] });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const match of allMatches(RUST_USE_EXTERNAL, content)) {
|
|
172
|
+
if (['crate', 'super', 'self', 'std', 'core', 'alloc'].includes(match[1])) continue;
|
|
173
|
+
refs.push({ crate: match[1], segments: match[2].split('::').filter(Boolean) });
|
|
174
|
+
}
|
|
175
|
+
return refs;
|
|
176
|
+
}
|
|
177
|
+
case 'csharp':
|
|
178
|
+
return allMatches(CS_USING, content).map((match) => ({
|
|
179
|
+
name: match[2],
|
|
180
|
+
isStatic: Boolean(match[1]),
|
|
181
|
+
}));
|
|
182
|
+
default:
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `A\B\{C, D as E}` → ['A\B\C', 'A\B\D']; `function A\f` → ['A\f'] */
|
|
188
|
+
function expandPhpUse(clause) {
|
|
189
|
+
const names = [];
|
|
190
|
+
const body = clause.replace(/^\s*(?:function|const)\s+/, '');
|
|
191
|
+
const group = body.match(/^\s*\\?([\w\\]*?)\\?\s*\{([^}]*)\}/);
|
|
192
|
+
if (group) {
|
|
193
|
+
for (const member of group[2].split(',')) {
|
|
194
|
+
const name = member.trim().split(/\s+as\s+/)[0];
|
|
195
|
+
if (/^[\w\\]+$/.test(name)) names.push(`${group[1]}\\${name}`);
|
|
196
|
+
}
|
|
197
|
+
return names;
|
|
198
|
+
}
|
|
199
|
+
for (const part of body.split(',')) {
|
|
200
|
+
const name = part
|
|
201
|
+
.trim()
|
|
202
|
+
.split(/\s+as\s+/)[0]
|
|
203
|
+
.replace(/^\\/, '');
|
|
204
|
+
if (/^[\w\\]+$/.test(name)) names.push(name);
|
|
205
|
+
}
|
|
206
|
+
return names;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Packages or namespaces a file declares (Java/Kotlin/Scala `package`, PHP and C#
|
|
211
|
+
* `namespace`), used to resolve imports by fully qualified name
|
|
212
|
+
* @param {string} language
|
|
213
|
+
* @param {string} content
|
|
214
|
+
* @returns {string[]}
|
|
215
|
+
*/
|
|
216
|
+
export function extractDeclarations(language, content) {
|
|
217
|
+
switch (language) {
|
|
218
|
+
case 'java':
|
|
219
|
+
case 'kotlin':
|
|
220
|
+
case 'scala': {
|
|
221
|
+
const match = content.match(JVM_PACKAGE);
|
|
222
|
+
return match ? [match[1]] : [];
|
|
223
|
+
}
|
|
224
|
+
case 'php': {
|
|
225
|
+
const match = content.match(PHP_NAMESPACE);
|
|
226
|
+
return match ? [match[1].replace(/^\\/, '')] : [];
|
|
227
|
+
}
|
|
228
|
+
case 'csharp':
|
|
229
|
+
return [...new Set(allMatches(CS_NAMESPACE, content).map((match) => match[1]))];
|
|
230
|
+
default:
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
package/lib/map/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository map (`ctxman map`)
|
|
3
|
+
*
|
|
4
|
+
* A ranked overview of a codebase for an LLM, after Aider's repo map: a file dependency
|
|
5
|
+
* graph built from imports, PageRank over it blended with git change frequency and
|
|
6
|
+
* symbol references, and the function signatures of the best files trimmed to a token
|
|
7
|
+
* budget.
|
|
8
|
+
*
|
|
9
|
+
* buildRepoGraph(root, opts) scan + parse + resolve imports (+ git history)
|
|
10
|
+
* rankFiles(graph, opts) scores, best first
|
|
11
|
+
* renderRepoMap(root, opts) all of the above, rendered within the budget
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { buildRepoGraph, mineHistory, signatureAt, DEFAULT_HISTORY } from './graph.js';
|
|
15
|
+
export {
|
|
16
|
+
rankFiles,
|
|
17
|
+
pageRank,
|
|
18
|
+
resolveFocus,
|
|
19
|
+
toProjectPath,
|
|
20
|
+
DEFAULT_WEIGHTS,
|
|
21
|
+
DEFAULT_DAMPING,
|
|
22
|
+
} from './rank.js';
|
|
23
|
+
export {
|
|
24
|
+
renderRepoMap,
|
|
25
|
+
renderGraph,
|
|
26
|
+
renderBlock,
|
|
27
|
+
normalizeMapOptions,
|
|
28
|
+
DEFAULT_TOKENS,
|
|
29
|
+
STYLES,
|
|
30
|
+
ORDERS,
|
|
31
|
+
} from './render.js';
|
|
32
|
+
export { extractImports, languageOf, LANGUAGE_BY_EXTENSION } from './imports.js';
|
|
33
|
+
export { ImportResolver } from './resolver.js';
|
package/lib/map/rank.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File ranking for `ctxman map`
|
|
3
|
+
*
|
|
4
|
+
* score(f) = (wP·P(f) + wH·H(f) + wS·S(f)) / (wP + wH + wS)
|
|
5
|
+
*
|
|
6
|
+
* P(f) = PR(f) / max PR PageRank over the import graph (importer → imported,
|
|
7
|
+
* damping 0.85), optionally personalised toward focus files
|
|
8
|
+
* H(f) = ln(1 + c(f)) / ln(1 + max c)
|
|
9
|
+
* c = commits among the last N (merges excluded) touching f
|
|
10
|
+
* S(f) = ln(1 + s(f)) / ln(1 + max s)
|
|
11
|
+
* s = mentions of f's function names in other files
|
|
12
|
+
*
|
|
13
|
+
* Default weights wP = 0.70, wH = 0.25, wS = 0.05. A component that is 0 for every file
|
|
14
|
+
* (no git history, no symbol references) drops out of the sum and of the divisor, so the
|
|
15
|
+
* others are renormalised. Equal scores are ordered by path.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
/** Default blend of the three components */
|
|
21
|
+
export const DEFAULT_WEIGHTS = Object.freeze({ pagerank: 0.7, history: 0.25, symbols: 0.05 });
|
|
22
|
+
export const DEFAULT_DAMPING = 0.85;
|
|
23
|
+
/** Share of the random jump that goes to focus files and their neighbours */
|
|
24
|
+
export const FOCUS_SHARE = 0.9;
|
|
25
|
+
/** Relative jump weight of a focus file and of a file one import away from one */
|
|
26
|
+
export const FOCUS_WEIGHT = 1;
|
|
27
|
+
export const NEIGHBOUR_WEIGHT = 0.5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Weighted PageRank by power iteration. The rank of nodes without outgoing edges
|
|
31
|
+
* (dangling nodes) is spread like the random jump, so the scores always sum to 1.
|
|
32
|
+
* @param {string[]} ids - Node ids; the order is the iteration order
|
|
33
|
+
* @param {Array<{from: string, to: string, weight?: number}>} edges
|
|
34
|
+
* @param {Object} [options]
|
|
35
|
+
* @param {number} [options.damping=0.85]
|
|
36
|
+
* @param {Map<string, number>|Object<string, number>} [options.personalization] - Random
|
|
37
|
+
* jump weights (normalised here); uniform when missing or all zero
|
|
38
|
+
* @param {number} [options.maxIterations=200]
|
|
39
|
+
* @param {number} [options.tolerance=1e-10] - Stop when the L1 change is below n × tolerance
|
|
40
|
+
* @returns {{scores: Map<string, number>, iterations: number, converged: boolean}}
|
|
41
|
+
*/
|
|
42
|
+
export function pageRank(ids, edges, options = {}) {
|
|
43
|
+
const damping = options.damping ?? DEFAULT_DAMPING;
|
|
44
|
+
const maxIterations = options.maxIterations ?? 200;
|
|
45
|
+
const tolerance = options.tolerance ?? 1e-10;
|
|
46
|
+
if (!(damping >= 0 && damping < 1)) throw new Error('damping must be in [0, 1)');
|
|
47
|
+
|
|
48
|
+
const n = ids.length;
|
|
49
|
+
const scores = new Map();
|
|
50
|
+
if (n === 0) return { scores, iterations: 0, converged: true };
|
|
51
|
+
|
|
52
|
+
const index = new Map(ids.map((id, i) => [id, i]));
|
|
53
|
+
const merged = new Map();
|
|
54
|
+
for (const edge of edges) {
|
|
55
|
+
const from = index.get(edge.from);
|
|
56
|
+
const to = index.get(edge.to);
|
|
57
|
+
const weight = edge.weight ?? 1;
|
|
58
|
+
if (from === undefined || to === undefined || from === to || !(weight > 0)) continue;
|
|
59
|
+
const key = from * n + to;
|
|
60
|
+
merged.set(key, (merged.get(key) || 0) + weight);
|
|
61
|
+
}
|
|
62
|
+
const sources = new Int32Array(merged.size);
|
|
63
|
+
const targets = new Int32Array(merged.size);
|
|
64
|
+
const weights = new Float64Array(merged.size);
|
|
65
|
+
const outWeight = new Float64Array(n);
|
|
66
|
+
[...merged.keys()]
|
|
67
|
+
.sort((a, b) => a - b)
|
|
68
|
+
.forEach((key, i) => {
|
|
69
|
+
sources[i] = Math.floor(key / n);
|
|
70
|
+
targets[i] = key % n;
|
|
71
|
+
weights[i] = merged.get(key);
|
|
72
|
+
outWeight[sources[i]] += weights[i];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const jump = new Float64Array(n);
|
|
76
|
+
const custom = options.personalization;
|
|
77
|
+
let total = 0;
|
|
78
|
+
if (custom) {
|
|
79
|
+
const entries = custom instanceof Map ? custom : new Map(Object.entries(custom));
|
|
80
|
+
for (const [id, weight] of entries) {
|
|
81
|
+
const i = index.get(id);
|
|
82
|
+
if (i !== undefined && weight > 0) {
|
|
83
|
+
jump[i] += weight;
|
|
84
|
+
total += weight;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (total > 0) for (let i = 0; i < n; i++) jump[i] /= total;
|
|
89
|
+
else jump.fill(1 / n);
|
|
90
|
+
|
|
91
|
+
let rank = new Float64Array(n).fill(1 / n);
|
|
92
|
+
let iterations = 0;
|
|
93
|
+
let converged = false;
|
|
94
|
+
while (iterations < maxIterations) {
|
|
95
|
+
iterations++;
|
|
96
|
+
const next = new Float64Array(n);
|
|
97
|
+
let dangling = 0;
|
|
98
|
+
for (let i = 0; i < n; i++) if (outWeight[i] === 0) dangling += rank[i];
|
|
99
|
+
for (let e = 0; e < sources.length; e++) {
|
|
100
|
+
next[targets[e]] += (damping * rank[sources[e]] * weights[e]) / outWeight[sources[e]];
|
|
101
|
+
}
|
|
102
|
+
const teleport = 1 - damping + damping * dangling;
|
|
103
|
+
let change = 0;
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
next[i] += teleport * jump[i];
|
|
106
|
+
change += Math.abs(next[i] - rank[i]);
|
|
107
|
+
}
|
|
108
|
+
rank = next;
|
|
109
|
+
if (change < n * tolerance) {
|
|
110
|
+
converged = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ids.forEach((id, i) => scores.set(id, rank[i]));
|
|
116
|
+
return { scores, iterations, converged };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Files of the graph matched by focus paths (a file, or a folder for every file in it)
|
|
121
|
+
* @param {import('./graph.js').RepoGraph} graph
|
|
122
|
+
* @param {string[]} focus - Absolute or project-relative paths
|
|
123
|
+
* @returns {{files: string[], unmatched: string[]}}
|
|
124
|
+
* @throws {Error} When a path lies outside the project
|
|
125
|
+
*/
|
|
126
|
+
export function resolveFocus(graph, focus = []) {
|
|
127
|
+
const files = new Set();
|
|
128
|
+
const unmatched = [];
|
|
129
|
+
const nodePaths = graph.nodes.map((node) => node.path);
|
|
130
|
+
for (const entry of focus) {
|
|
131
|
+
const relative = toProjectPath(graph.root, entry);
|
|
132
|
+
const matches = nodePaths.filter(
|
|
133
|
+
(file) => relative === '' || file === relative || file.startsWith(`${relative}/`)
|
|
134
|
+
);
|
|
135
|
+
if (matches.length === 0) unmatched.push(entry);
|
|
136
|
+
for (const file of matches) files.add(file);
|
|
137
|
+
}
|
|
138
|
+
return { files: [...files].sort(), unmatched };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Project-relative form of a path (forward slashes); throws when it escapes the project
|
|
143
|
+
* @param {string} root - Absolute project root
|
|
144
|
+
* @param {string} entry
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
export function toProjectPath(root, entry) {
|
|
148
|
+
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
149
|
+
throw new Error(`Invalid focus path: ${JSON.stringify(entry)}`);
|
|
150
|
+
}
|
|
151
|
+
const absolute = path.resolve(root, entry);
|
|
152
|
+
const relative = path.relative(root, absolute);
|
|
153
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
154
|
+
throw new Error(`Focus path is outside the project: ${entry}`);
|
|
155
|
+
}
|
|
156
|
+
return relative.split(path.sep).join('/');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Rank the files of a graph, best first
|
|
161
|
+
* @param {import('./graph.js').RepoGraph} graph
|
|
162
|
+
* @param {Object} [options]
|
|
163
|
+
* @param {string[]} [options.focus] - Files or folders to personalise PageRank toward
|
|
164
|
+
* @param {{pagerank?: number, history?: number, symbols?: number}} [options.weights]
|
|
165
|
+
* @param {number} [options.damping=0.85]
|
|
166
|
+
* @returns {Array<{path: string, rank: number, score: number, pagerank: number,
|
|
167
|
+
* changes: number, symbolRefs: number, focus: boolean,
|
|
168
|
+
* components: {pagerank: number, history: number, symbols: number}}>}
|
|
169
|
+
*/
|
|
170
|
+
export function rankFiles(graph, options = {}) {
|
|
171
|
+
const weights = { ...DEFAULT_WEIGHTS, ...options.weights };
|
|
172
|
+
const ids = graph.nodes.map((node) => node.path);
|
|
173
|
+
const focus = new Set(resolveFocus(graph, options.focus || []).files);
|
|
174
|
+
|
|
175
|
+
let personalization = null;
|
|
176
|
+
if (focus.size > 0) {
|
|
177
|
+
const preference = new Map();
|
|
178
|
+
for (const file of focus) preference.set(file, FOCUS_WEIGHT);
|
|
179
|
+
for (const { from, to } of graph.edges) {
|
|
180
|
+
for (const [a, b] of [
|
|
181
|
+
[from, to],
|
|
182
|
+
[to, from],
|
|
183
|
+
]) {
|
|
184
|
+
if (focus.has(a) && !focus.has(b)) {
|
|
185
|
+
preference.set(b, Math.max(preference.get(b) || 0, NEIGHBOUR_WEIGHT));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const preferenceTotal = [...preference.values()].reduce((sum, value) => sum + value, 0);
|
|
190
|
+
personalization = new Map(
|
|
191
|
+
ids.map((id) => [
|
|
192
|
+
id,
|
|
193
|
+
(1 - FOCUS_SHARE) / ids.length +
|
|
194
|
+
(FOCUS_SHARE * (preference.get(id) || 0)) / preferenceTotal,
|
|
195
|
+
])
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { scores } = pageRank(ids, graph.edges, {
|
|
200
|
+
damping: options.damping ?? DEFAULT_DAMPING,
|
|
201
|
+
personalization,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const history = graph.history?.available ? graph.history.counts : null;
|
|
205
|
+
const maxRank = maxOf(scores.values());
|
|
206
|
+
const maxChanges = history ? maxOf(ids.map((id) => history[id] || 0)) : 0;
|
|
207
|
+
const maxSymbols = maxOf(graph.nodes.map((node) => node.symbolRefs || 0));
|
|
208
|
+
// Components without any signal drop out instead of capping every score below 1
|
|
209
|
+
const active = {
|
|
210
|
+
pagerank: weights.pagerank,
|
|
211
|
+
history: maxChanges > 0 ? weights.history : 0,
|
|
212
|
+
symbols: maxSymbols > 0 ? weights.symbols : 0,
|
|
213
|
+
};
|
|
214
|
+
const totalWeight = active.pagerank + active.history + active.symbols;
|
|
215
|
+
|
|
216
|
+
const rows = graph.nodes.map((node) => {
|
|
217
|
+
const pagerank = scores.get(node.path);
|
|
218
|
+
const changes = history ? history[node.path] || 0 : 0;
|
|
219
|
+
const symbolRefs = node.symbolRefs || 0;
|
|
220
|
+
const components = {
|
|
221
|
+
pagerank: maxRank > 0 ? pagerank / maxRank : 0,
|
|
222
|
+
history: maxChanges > 0 ? Math.log1p(changes) / Math.log1p(maxChanges) : 0,
|
|
223
|
+
symbols: maxSymbols > 0 ? Math.log1p(symbolRefs) / Math.log1p(maxSymbols) : 0,
|
|
224
|
+
};
|
|
225
|
+
const weighted =
|
|
226
|
+
active.pagerank * components.pagerank +
|
|
227
|
+
active.history * components.history +
|
|
228
|
+
active.symbols * components.symbols;
|
|
229
|
+
return {
|
|
230
|
+
path: node.path,
|
|
231
|
+
rank: 0,
|
|
232
|
+
score: totalWeight > 0 ? weighted / totalWeight : 0,
|
|
233
|
+
pagerank,
|
|
234
|
+
changes,
|
|
235
|
+
symbolRefs,
|
|
236
|
+
focus: focus.has(node.path),
|
|
237
|
+
components,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
rows.sort((a, b) => b.score - a.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
242
|
+
rows.forEach((row, i) => {
|
|
243
|
+
row.rank = i + 1;
|
|
244
|
+
});
|
|
245
|
+
return rows;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Largest value, 0 for none (no argument spreading, so any size works) */
|
|
249
|
+
function maxOf(values) {
|
|
250
|
+
let max = 0;
|
|
251
|
+
for (const value of values) if (value > max) max = value;
|
|
252
|
+
return max;
|
|
253
|
+
}
|