ctxman 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
- 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 +243 -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 +255 -0
- package/lib/wizards/project-detector.js +571 -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,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output of `ctxman why`: Markdown (default, for pasting into an LLM), XML and JSON, and
|
|
3
|
+
* fitting the document into a token budget by dropping the least useful material first
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import TokenUtils from '../utils/token-utils.js';
|
|
8
|
+
import { WhyError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
export const STYLES = ['markdown', 'xml', 'json'];
|
|
11
|
+
|
|
12
|
+
/** Sections a budget can shorten */
|
|
13
|
+
export const DROP_ORDER = [
|
|
14
|
+
'patches',
|
|
15
|
+
'related',
|
|
16
|
+
'tests',
|
|
17
|
+
'coupling',
|
|
18
|
+
'commits',
|
|
19
|
+
'references',
|
|
20
|
+
'notes',
|
|
21
|
+
'ownership',
|
|
22
|
+
'source',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What a budget drops, first to last, as [section, items kept at least]. The first five
|
|
27
|
+
* steps follow the documented priority (patches, related text, tests, coupling details,
|
|
28
|
+
* older commits, keeping the newest one); the rest only run when the budget is smaller
|
|
29
|
+
* than the core of the document
|
|
30
|
+
*/
|
|
31
|
+
const DROP_STEPS = [
|
|
32
|
+
['patches', 0],
|
|
33
|
+
['related', 0],
|
|
34
|
+
['tests', 0],
|
|
35
|
+
['coupling', 0],
|
|
36
|
+
['commits', 1],
|
|
37
|
+
['references', 0],
|
|
38
|
+
['notes', 0],
|
|
39
|
+
['ownership', 0],
|
|
40
|
+
['source', 0],
|
|
41
|
+
['commits', 0],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const OMITTED_LABELS = {
|
|
45
|
+
patches: 'patches',
|
|
46
|
+
related: 'related text snippets',
|
|
47
|
+
tests: 'test files',
|
|
48
|
+
coupling: 'co-changed files',
|
|
49
|
+
commits: 'commits',
|
|
50
|
+
references: 'references',
|
|
51
|
+
notes: 'notes',
|
|
52
|
+
ownership: 'blame authors',
|
|
53
|
+
source: 'source lines',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const LANGUAGES = {
|
|
57
|
+
'.js': 'javascript',
|
|
58
|
+
'.mjs': 'javascript',
|
|
59
|
+
'.cjs': 'javascript',
|
|
60
|
+
'.jsx': 'jsx',
|
|
61
|
+
'.ts': 'typescript',
|
|
62
|
+
'.tsx': 'tsx',
|
|
63
|
+
'.py': 'python',
|
|
64
|
+
'.rb': 'ruby',
|
|
65
|
+
'.rs': 'rust',
|
|
66
|
+
'.go': 'go',
|
|
67
|
+
'.java': 'java',
|
|
68
|
+
'.kt': 'kotlin',
|
|
69
|
+
'.kts': 'kotlin',
|
|
70
|
+
'.cs': 'csharp',
|
|
71
|
+
'.c': 'c',
|
|
72
|
+
'.h': 'c',
|
|
73
|
+
'.cc': 'cpp',
|
|
74
|
+
'.cpp': 'cpp',
|
|
75
|
+
'.hpp': 'cpp',
|
|
76
|
+
'.swift': 'swift',
|
|
77
|
+
'.scala': 'scala',
|
|
78
|
+
'.php': 'php',
|
|
79
|
+
'.md': 'markdown',
|
|
80
|
+
'.yml': 'yaml',
|
|
81
|
+
'.yaml': 'yaml',
|
|
82
|
+
'.json': 'json',
|
|
83
|
+
'.sh': 'bash',
|
|
84
|
+
'.sql': 'sql',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flattened reference list, highest value first
|
|
89
|
+
*/
|
|
90
|
+
function flattenReferences(references) {
|
|
91
|
+
return [
|
|
92
|
+
...references.issues.map((entry) => ({ type: 'issue', ...entry })),
|
|
93
|
+
...references.tickets.map((entry) => ({ type: 'ticket', ...entry })),
|
|
94
|
+
...references.cves.map((entry) => ({ type: 'cve', ...entry })),
|
|
95
|
+
...references.urls.map((entry) => ({ type: 'url', ...entry })),
|
|
96
|
+
...references.coAuthors.map((entry) => ({ type: 'co-author', ...entry })),
|
|
97
|
+
...references.reviewers.map((entry) => ({ type: 'reviewer', ...entry })),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Number of droppable items per DROP_ORDER key
|
|
103
|
+
* @param {object} data
|
|
104
|
+
* @returns {Record<string, number>}
|
|
105
|
+
*/
|
|
106
|
+
export function countItems(data) {
|
|
107
|
+
return {
|
|
108
|
+
patches: data.history.commits.filter((commit) => commit.patch).length,
|
|
109
|
+
related: data.related.length,
|
|
110
|
+
tests: data.tests.length,
|
|
111
|
+
coupling: data.coupling?.files.length ?? 0,
|
|
112
|
+
commits: data.history.commits.length,
|
|
113
|
+
references: flattenReferences(data.references).length,
|
|
114
|
+
notes: data.notes.length,
|
|
115
|
+
ownership: data.ownership?.authors.length ?? 0,
|
|
116
|
+
source: data.target.source === null ? 0 : data.target.source.split('\n').length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Copy of `data` with only the first `view[key]` items of each section
|
|
122
|
+
* @param {object} data
|
|
123
|
+
* @param {Record<string, number>} view
|
|
124
|
+
* @returns {object}
|
|
125
|
+
*/
|
|
126
|
+
export function applyView(data, view) {
|
|
127
|
+
const totals = countItems(data);
|
|
128
|
+
const omitted = {};
|
|
129
|
+
for (const key of DROP_ORDER) {
|
|
130
|
+
if (view[key] < totals[key]) omitted[key] = totals[key] - view[key];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let patchesKept = 0;
|
|
134
|
+
const commits = data.history.commits.slice(0, view.commits).map((commit) => {
|
|
135
|
+
if (!commit.patch) return commit;
|
|
136
|
+
const { patch, ...rest } = commit;
|
|
137
|
+
return patchesKept++ < view.patches ? { ...rest, patch } : rest;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const kept = flattenReferences(data.references).slice(0, view.references);
|
|
141
|
+
const pick = (type) =>
|
|
142
|
+
kept.filter((entry) => entry.type === type).map(({ type: _type, ...entry }) => entry);
|
|
143
|
+
const references = {
|
|
144
|
+
issues: pick('issue'),
|
|
145
|
+
tickets: pick('ticket'),
|
|
146
|
+
cves: pick('cve'),
|
|
147
|
+
urls: pick('url'),
|
|
148
|
+
coAuthors: pick('co-author'),
|
|
149
|
+
reviewers: pick('reviewer'),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let target = data.target;
|
|
153
|
+
if (target.source !== null && view.source < totals.source) {
|
|
154
|
+
target =
|
|
155
|
+
view.source === 0
|
|
156
|
+
? { ...target, source: null, contentOmitted: 'omitted to fit the token budget' }
|
|
157
|
+
: { ...target, source: target.source.split('\n').slice(0, view.source).join('\n') };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...data,
|
|
162
|
+
target,
|
|
163
|
+
history: { ...data.history, commits },
|
|
164
|
+
references,
|
|
165
|
+
ownership: data.ownership && {
|
|
166
|
+
...data.ownership,
|
|
167
|
+
authors: data.ownership.authors.slice(0, view.ownership),
|
|
168
|
+
},
|
|
169
|
+
coupling: data.coupling && {
|
|
170
|
+
...data.coupling,
|
|
171
|
+
files: data.coupling.files.slice(0, view.coupling),
|
|
172
|
+
},
|
|
173
|
+
related: data.related.slice(0, view.related),
|
|
174
|
+
notes: data.notes.slice(0, view.notes),
|
|
175
|
+
tests: data.tests.slice(0, view.tests),
|
|
176
|
+
omitted,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Render a (trimmed) document
|
|
182
|
+
* @param {object} data - Output of applyView
|
|
183
|
+
* @param {string} style - markdown | xml | json
|
|
184
|
+
* @param {number|null} budget
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function render(data, style, budget = null) {
|
|
188
|
+
if (style === 'json') return renderJson(data, budget);
|
|
189
|
+
if (style === 'xml') return renderXml(data, budget);
|
|
190
|
+
return renderMarkdown(data, budget);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Render `data`, dropping material in DROP_ORDER until the text fits `budget` tokens
|
|
195
|
+
* @param {object} data
|
|
196
|
+
* @param {object} options
|
|
197
|
+
* @param {string} options.style
|
|
198
|
+
* @param {number|null} options.budget
|
|
199
|
+
* @returns {{text: string, tokens: number, omitted: Record<string, number>}}
|
|
200
|
+
*/
|
|
201
|
+
export function renderWithinBudget(data, { style, budget }) {
|
|
202
|
+
const fileName = `why.${style === 'markdown' ? 'md' : style}`;
|
|
203
|
+
const count = (text) => TokenUtils.calculate(text, fileName);
|
|
204
|
+
const totals = countItems(data);
|
|
205
|
+
const attempt = (view) => {
|
|
206
|
+
const trimmed = applyView(data, view);
|
|
207
|
+
const text = render(trimmed, style, budget);
|
|
208
|
+
return { text, tokens: count(text), omitted: trimmed.omitted };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
let view = { ...totals };
|
|
212
|
+
let result = attempt(view);
|
|
213
|
+
if (!budget || result.tokens <= budget) return result;
|
|
214
|
+
|
|
215
|
+
for (const [key, floor] of DROP_STEPS) {
|
|
216
|
+
if (view[key] <= floor) continue;
|
|
217
|
+
const without = attempt({ ...view, [key]: floor });
|
|
218
|
+
if (without.tokens > budget) {
|
|
219
|
+
view = { ...view, [key]: floor };
|
|
220
|
+
result = without;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Largest number of items of this kind that still fits
|
|
224
|
+
let low = floor;
|
|
225
|
+
let high = view[key] - 1;
|
|
226
|
+
let best = without;
|
|
227
|
+
while (low < high) {
|
|
228
|
+
const middle = Math.ceil((low + high) / 2);
|
|
229
|
+
const candidate = attempt({ ...view, [key]: middle });
|
|
230
|
+
if (candidate.tokens <= budget) {
|
|
231
|
+
low = middle;
|
|
232
|
+
best = candidate;
|
|
233
|
+
} else {
|
|
234
|
+
high = middle - 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return best;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new WhyError(
|
|
241
|
+
'BUDGET_TOO_SMALL',
|
|
242
|
+
`--budget ${budget} is too small: the target summary alone needs ${result.tokens} tokens`,
|
|
243
|
+
{ minimum: result.tokens }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Markdown
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
function fence(text, language = '') {
|
|
252
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((match) => match[0].length));
|
|
253
|
+
const marks = '`'.repeat(Math.max(3, longest + 1));
|
|
254
|
+
return `${marks}${language}\n${text}\n${marks}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function code(text) {
|
|
258
|
+
const value = String(text);
|
|
259
|
+
const longest = Math.max(0, ...[...value.matchAll(/`+/g)].map((match) => match[0].length));
|
|
260
|
+
const marks = '`'.repeat(longest + 1);
|
|
261
|
+
const pad = value.startsWith('`') || value.endsWith('`') ? ' ' : '';
|
|
262
|
+
return `${marks}${pad}${value}${pad}${marks}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const lineSpan = (start, end) => (start === end ? `line ${start}` : `lines ${start}-${end}`);
|
|
266
|
+
const day = (iso) => (iso ? iso.slice(0, 10) : 'unknown');
|
|
267
|
+
const percent = (share) => `${Math.round(share * 100)}%`;
|
|
268
|
+
|
|
269
|
+
function title(target) {
|
|
270
|
+
if (target.symbol) return `${code(target.symbol)} in ${code(target.path)}`;
|
|
271
|
+
if (target.wholeFile) return code(target.path);
|
|
272
|
+
return `${code(target.path)} ${lineSpan(target.startLine, target.endLine)}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function omittedSummary(omitted) {
|
|
276
|
+
return Object.entries(omitted)
|
|
277
|
+
.map(([key, value]) => `${value} ${OMITTED_LABELS[key]}`)
|
|
278
|
+
.join(', ');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function historyDescription(history) {
|
|
282
|
+
switch (history.mode) {
|
|
283
|
+
case 'range':
|
|
284
|
+
return `that changed ${lineSpan(history.startLine, history.endLine)} (\`git log -L\`)`;
|
|
285
|
+
case 'funcname':
|
|
286
|
+
return `that changed the function (\`git log -L :${history.funcname}\`)`;
|
|
287
|
+
case 'file':
|
|
288
|
+
return 'that changed the file, following renames (`git log --follow`)';
|
|
289
|
+
default:
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function renderMarkdown(data, budget) {
|
|
295
|
+
const out = [`# Why: ${title(data.target)}`, ''];
|
|
296
|
+
out.push(
|
|
297
|
+
'Context for explaining why this code looks the way it does, collected by `ctxman why` from the local git history and the repository (no network).',
|
|
298
|
+
''
|
|
299
|
+
);
|
|
300
|
+
for (const warning of data.warnings) out.push(`> **Warning:** ${warning}`);
|
|
301
|
+
if (data.warnings.length) out.push('');
|
|
302
|
+
if (Object.keys(data.omitted).length) {
|
|
303
|
+
out.push(`> Trimmed to fit ${budget} tokens: omitted ${omittedSummary(data.omitted)}.`, '');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const section of [
|
|
307
|
+
markdownTarget,
|
|
308
|
+
markdownHistory,
|
|
309
|
+
markdownReferences,
|
|
310
|
+
markdownOwnership,
|
|
311
|
+
markdownCoupling,
|
|
312
|
+
markdownRelated,
|
|
313
|
+
markdownNotes,
|
|
314
|
+
markdownTests,
|
|
315
|
+
markdownQuestion,
|
|
316
|
+
]) {
|
|
317
|
+
const lines = section(data);
|
|
318
|
+
if (lines.length) out.push(...lines, '');
|
|
319
|
+
}
|
|
320
|
+
return out.join('\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function markdownTarget({ target, repository, omitted }) {
|
|
324
|
+
const out = ['## Target', '', `- File: ${code(target.path)}`];
|
|
325
|
+
if (target.symbol) {
|
|
326
|
+
const line = target.line ? `, declared on line ${target.line}` : '';
|
|
327
|
+
out.push(`- Symbol: ${code(target.symbol)} (${target.kind}${line})`);
|
|
328
|
+
}
|
|
329
|
+
if (target.binary) out.push('- Content: binary file');
|
|
330
|
+
else if (target.wholeFile) {
|
|
331
|
+
out.push(
|
|
332
|
+
`- Lines: whole file (${target.totalLines} line${target.totalLines === 1 ? '' : 's'})`
|
|
333
|
+
);
|
|
334
|
+
} else if (target.startLine === target.endLine && target.startLine !== null) {
|
|
335
|
+
out.push(`- Line: ${target.startLine} of ${target.totalLines}`);
|
|
336
|
+
} else if (target.startLine !== null) {
|
|
337
|
+
out.push(`- Lines: ${target.startLine}-${target.endLine} of ${target.totalLines}`);
|
|
338
|
+
}
|
|
339
|
+
if (target.symbolsInRange.length) {
|
|
340
|
+
out.push(`- Definitions in range: ${target.symbolsInRange.map(code).join(', ')}`);
|
|
341
|
+
}
|
|
342
|
+
out.push(`- Revision: ${target.revision}`);
|
|
343
|
+
if (repository.remote) out.push(`- Repository: ${repository.remote}`);
|
|
344
|
+
if (target.contentOmitted) out.push(`- Source: not shown (${target.contentOmitted})`);
|
|
345
|
+
if (target.source !== null) {
|
|
346
|
+
out.push('', fence(target.source, LANGUAGES[path.extname(target.path).toLowerCase()] ?? ''));
|
|
347
|
+
if (omitted.source)
|
|
348
|
+
out.push(`_${omitted.source} more lines not shown to fit the token budget._`);
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function markdownHistory({ history, omitted }) {
|
|
354
|
+
const out = ['## History', ''];
|
|
355
|
+
if (history.mode === 'none') return [...out, `No committed history: ${history.reason}.`];
|
|
356
|
+
|
|
357
|
+
const total = history.commits.length + (omitted.commits ?? 0);
|
|
358
|
+
const since = history.since ? ` since ${history.since}` : '';
|
|
359
|
+
const more = history.limitReached
|
|
360
|
+
? ` Showing the newest ${history.limit}; older commits exist (raise --limit).`
|
|
361
|
+
: '';
|
|
362
|
+
out.push(
|
|
363
|
+
`${total} commit${total === 1 ? '' : 's'}${since} ${historyDescription(history)}, newest first.${more}`
|
|
364
|
+
);
|
|
365
|
+
history.commits.forEach((commit, index) => {
|
|
366
|
+
out.push('', `### ${index + 1}. ${commit.subject || '(no subject)'}`, '');
|
|
367
|
+
const meta = [code(commit.shortSha), day(commit.date), commit.author];
|
|
368
|
+
if (commit.merge) meta.push('merge');
|
|
369
|
+
if (commit.shallowBoundary) {
|
|
370
|
+
meta.push(
|
|
371
|
+
'shallow clone boundary: older history is missing, so it looks like it added the code'
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (commit.refs?.length) meta.push(`refs: ${commit.refs.join(', ')}`);
|
|
375
|
+
out.push(meta.join(' · '));
|
|
376
|
+
if (commit.body) {
|
|
377
|
+
out.push('', commit.body.replace(/^/gm, '> ').replace(/^> $/gm, '>'));
|
|
378
|
+
}
|
|
379
|
+
if (commit.patch) out.push('', fence(commit.patch, 'diff'));
|
|
380
|
+
});
|
|
381
|
+
if (omitted.commits) {
|
|
382
|
+
out.push('', `_${omitted.commits} older commits not shown to fit the token budget._`);
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function markdownReferences({ references, history, omitted }) {
|
|
388
|
+
const entries = flattenReferences(references);
|
|
389
|
+
// Nothing to say without commits, or when the budget dropped every reference
|
|
390
|
+
if (!entries.length && (omitted.references || !history.commits.length)) return [];
|
|
391
|
+
const out = ['## References', ''];
|
|
392
|
+
if (!entries.length) {
|
|
393
|
+
return [...out, 'No issue, pull request, ticket or URL references in these commits.'];
|
|
394
|
+
}
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
const where = `(${entry.commits.join(', ')})`;
|
|
397
|
+
if (entry.type === 'issue') {
|
|
398
|
+
const label = { pr: 'Pull request', mr: 'Merge request' }[entry.kind] ?? 'Issue';
|
|
399
|
+
const closes = entry.closes ? ' (fixes / closes)' : '';
|
|
400
|
+
out.push(`- ${label} ${entry.id}${closes}${entry.url ? `: ${entry.url}` : ''} ${where}`);
|
|
401
|
+
} else if (entry.type === 'ticket') out.push(`- Ticket ${entry.id} ${where}`);
|
|
402
|
+
else if (entry.type === 'cve') out.push(`- ${entry.id} ${where}`);
|
|
403
|
+
else if (entry.type === 'url') out.push(`- URL ${entry.url} ${where}`);
|
|
404
|
+
else if (entry.type === 'co-author') out.push(`- Co-authored-by: ${entry.value} ${where}`);
|
|
405
|
+
else out.push(`- Reviewed-by: ${entry.value} ${where}`);
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function markdownOwnership({ ownership, omitted }) {
|
|
411
|
+
if (!ownership || !ownership.authors.length) return [];
|
|
412
|
+
const scope =
|
|
413
|
+
ownership.startLine === null
|
|
414
|
+
? 'the whole file'
|
|
415
|
+
: lineSpan(ownership.startLine, ownership.endLine);
|
|
416
|
+
const out = [
|
|
417
|
+
'## Ownership',
|
|
418
|
+
'',
|
|
419
|
+
`\`git blame\` of ${scope} (${ownership.totalLines} lines), last changed ${day(ownership.lastChanged)}:`,
|
|
420
|
+
'',
|
|
421
|
+
'| Author | Lines | Share | Commits | Last touched |',
|
|
422
|
+
'|---|---:|---:|---:|---|',
|
|
423
|
+
];
|
|
424
|
+
for (const author of ownership.authors) {
|
|
425
|
+
const name = author.name.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
426
|
+
out.push(
|
|
427
|
+
`| ${name} | ${author.lines} | ${percent(author.share)} | ${author.commits} | ${day(author.lastTouched)} |`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (omitted.ownership) out.push(`| … ${omitted.ownership} more | | | | |`);
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function markdownCoupling({ coupling, target, omitted }) {
|
|
435
|
+
if (!coupling || (!coupling.files.length && omitted.coupling)) return [];
|
|
436
|
+
const skipped = coupling.skippedLargeCommits
|
|
437
|
+
? ` (${coupling.skippedLargeCommits} commit${coupling.skippedLargeCommits === 1 ? '' : 's'} with more than ${coupling.maxFilesPerCommit} files skipped)`
|
|
438
|
+
: '';
|
|
439
|
+
const out = [
|
|
440
|
+
'## Co-change coupling',
|
|
441
|
+
'',
|
|
442
|
+
`Commits that changed ${code(target.path)}: ${coupling.targetChanges} examined (window: the last ${coupling.window})${skipped}.`,
|
|
443
|
+
];
|
|
444
|
+
if (!coupling.files.length) return [...out, 'No other file changed in the same commits.'];
|
|
445
|
+
out.push(
|
|
446
|
+
'Files changed in the same commits (confidence = co-changes / changes of the target):',
|
|
447
|
+
'',
|
|
448
|
+
'| File | Co-changes | Confidence |',
|
|
449
|
+
'|---|---:|---:|'
|
|
450
|
+
);
|
|
451
|
+
for (const file of coupling.files) {
|
|
452
|
+
const gone = file.exists === false ? ' (no longer in the tree)' : '';
|
|
453
|
+
out.push(`| ${code(file.path)}${gone} | ${file.count} | ${percent(file.confidence)} |`);
|
|
454
|
+
}
|
|
455
|
+
if (omitted.coupling) out.push(`| … ${omitted.coupling} more | | |`);
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function markdownRelated({ related }) {
|
|
460
|
+
if (!related.length) return [];
|
|
461
|
+
const labels = { adr: 'ADR', changelog: 'changelog', doc: 'doc', comment: 'comment' };
|
|
462
|
+
return [
|
|
463
|
+
'## Related text',
|
|
464
|
+
'',
|
|
465
|
+
...related.map((entry) => {
|
|
466
|
+
const heading = entry.heading ? `, "${entry.heading}"` : '';
|
|
467
|
+
const where = code(`${entry.path}:${entry.line}`);
|
|
468
|
+
return `- ${where} (${labels[entry.kind]}${heading}): ${entry.snippet}`;
|
|
469
|
+
}),
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function markdownNotes({ notes }) {
|
|
474
|
+
if (!notes.length) return [];
|
|
475
|
+
return [
|
|
476
|
+
'## Notes in the target',
|
|
477
|
+
'',
|
|
478
|
+
...notes.map(
|
|
479
|
+
(note) =>
|
|
480
|
+
`- Line ${note.line} ${note.tag}${note.author ? ` (${note.author})` : ''}: ${note.text}`
|
|
481
|
+
),
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function markdownTests({ tests }) {
|
|
486
|
+
if (!tests.length) return [];
|
|
487
|
+
const out = ['## Tests', ''];
|
|
488
|
+
for (const test of tests) {
|
|
489
|
+
const what = test.references === 'symbol' ? 'references the symbol' : 'references the file';
|
|
490
|
+
const count = `${test.totalMatches} matching line${test.totalMatches === 1 ? '' : 's'}`;
|
|
491
|
+
out.push(`- ${code(test.path)} (${what}; ${count})`);
|
|
492
|
+
for (const match of test.matches) out.push(` - ${match.line}: ${code(match.text)}`);
|
|
493
|
+
}
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function markdownQuestion({ question }) {
|
|
498
|
+
if (!question) return [];
|
|
499
|
+
return [
|
|
500
|
+
'## Question',
|
|
501
|
+
'',
|
|
502
|
+
question,
|
|
503
|
+
'',
|
|
504
|
+
'Answer from the context above. Cite the commit hashes, references and file paths that support the answer, and say so when the history does not explain something.',
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// XML
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
function xml(text) {
|
|
513
|
+
return String(text)
|
|
514
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
|
515
|
+
.replace(/&/g, '&')
|
|
516
|
+
.replace(/</g, '<')
|
|
517
|
+
.replace(/>/g, '>')
|
|
518
|
+
.replace(/"/g, '"');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function attributes(values) {
|
|
522
|
+
return Object.entries(values)
|
|
523
|
+
.filter(
|
|
524
|
+
([, value]) =>
|
|
525
|
+
value !== null &&
|
|
526
|
+
value !== undefined &&
|
|
527
|
+
value !== '' &&
|
|
528
|
+
!(Array.isArray(value) && value.length === 0)
|
|
529
|
+
)
|
|
530
|
+
.map(([key, value]) => ` ${key}="${xml(Array.isArray(value) ? value.join(' ') : value)}"`)
|
|
531
|
+
.join('');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function renderXml(data, budget) {
|
|
535
|
+
const { target, history } = data;
|
|
536
|
+
const out = ['<why_context generator="ctxman why">'];
|
|
537
|
+
if (Object.keys(data.omitted).length) {
|
|
538
|
+
out.push(`<trimmed${attributes({ budget, ...data.omitted })}/>`);
|
|
539
|
+
}
|
|
540
|
+
for (const warning of data.warnings) out.push(`<warning>${xml(warning)}</warning>`);
|
|
541
|
+
|
|
542
|
+
out.push(
|
|
543
|
+
`<target${attributes({
|
|
544
|
+
path: target.path,
|
|
545
|
+
symbol: target.symbol,
|
|
546
|
+
kind: target.kind,
|
|
547
|
+
declaration_line: target.line,
|
|
548
|
+
start_line: target.startLine,
|
|
549
|
+
end_line: target.endLine,
|
|
550
|
+
total_lines: target.totalLines,
|
|
551
|
+
whole_file: target.wholeFile,
|
|
552
|
+
revision: target.revision,
|
|
553
|
+
repository: data.repository.remote,
|
|
554
|
+
definitions_in_range: target.symbolsInRange,
|
|
555
|
+
source_omitted: target.contentOmitted,
|
|
556
|
+
})}>`
|
|
557
|
+
);
|
|
558
|
+
if (target.source !== null) {
|
|
559
|
+
out.push(
|
|
560
|
+
`<source${attributes({ omitted_lines: data.omitted.source })}>${xml(target.source)}</source>`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
out.push('</target>');
|
|
564
|
+
|
|
565
|
+
out.push(
|
|
566
|
+
`<history${attributes({
|
|
567
|
+
mode: history.mode,
|
|
568
|
+
start_line: history.startLine,
|
|
569
|
+
end_line: history.endLine,
|
|
570
|
+
since: history.since,
|
|
571
|
+
limit: history.limit,
|
|
572
|
+
limit_reached: history.limitReached,
|
|
573
|
+
reason: history.reason,
|
|
574
|
+
})}>`
|
|
575
|
+
);
|
|
576
|
+
for (const commit of history.commits) {
|
|
577
|
+
out.push(
|
|
578
|
+
`<commit${attributes({
|
|
579
|
+
sha: commit.sha,
|
|
580
|
+
short_sha: commit.shortSha,
|
|
581
|
+
date: commit.date,
|
|
582
|
+
author: commit.author,
|
|
583
|
+
email: commit.email,
|
|
584
|
+
merge: commit.merge || null,
|
|
585
|
+
shallow_boundary: commit.shallowBoundary || null,
|
|
586
|
+
refs: commit.refs,
|
|
587
|
+
})}>`
|
|
588
|
+
);
|
|
589
|
+
out.push(`<subject>${xml(commit.subject)}</subject>`);
|
|
590
|
+
if (commit.body) out.push(`<body>${xml(commit.body)}</body>`);
|
|
591
|
+
if (commit.patch) out.push(`<patch>${xml(commit.patch)}</patch>`);
|
|
592
|
+
out.push('</commit>');
|
|
593
|
+
}
|
|
594
|
+
out.push('</history>');
|
|
595
|
+
|
|
596
|
+
out.push('<references>');
|
|
597
|
+
for (const entry of flattenReferences(data.references)) {
|
|
598
|
+
const commits = entry.commits;
|
|
599
|
+
switch (entry.type) {
|
|
600
|
+
case 'issue':
|
|
601
|
+
out.push(
|
|
602
|
+
`<issue${attributes({ id: entry.id, kind: entry.kind, closes: entry.closes || null, url: entry.url, commits })}/>`
|
|
603
|
+
);
|
|
604
|
+
break;
|
|
605
|
+
case 'ticket':
|
|
606
|
+
case 'cve':
|
|
607
|
+
out.push(`<${entry.type}${attributes({ id: entry.id, commits })}/>`);
|
|
608
|
+
break;
|
|
609
|
+
case 'url':
|
|
610
|
+
out.push(`<url${attributes({ href: entry.url, commits })}/>`);
|
|
611
|
+
break;
|
|
612
|
+
default: {
|
|
613
|
+
const tag = entry.type === 'co-author' ? 'co_author' : 'reviewer';
|
|
614
|
+
out.push(`<${tag}${attributes({ commits })}>${xml(entry.value)}</${tag}>`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
out.push('</references>');
|
|
619
|
+
|
|
620
|
+
if (data.ownership) {
|
|
621
|
+
const own = data.ownership;
|
|
622
|
+
out.push(
|
|
623
|
+
`<ownership${attributes({
|
|
624
|
+
start_line: own.startLine,
|
|
625
|
+
end_line: own.endLine,
|
|
626
|
+
total_lines: own.totalLines,
|
|
627
|
+
last_changed: own.lastChanged,
|
|
628
|
+
})}>`
|
|
629
|
+
);
|
|
630
|
+
for (const author of own.authors) {
|
|
631
|
+
out.push(
|
|
632
|
+
`<author${attributes({
|
|
633
|
+
name: author.name,
|
|
634
|
+
lines: author.lines,
|
|
635
|
+
share: author.share,
|
|
636
|
+
commits: author.commits,
|
|
637
|
+
last_touched: author.lastTouched,
|
|
638
|
+
})}/>`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
out.push('</ownership>');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (data.coupling) {
|
|
645
|
+
const coupling = data.coupling;
|
|
646
|
+
out.push(
|
|
647
|
+
`<coupling${attributes({
|
|
648
|
+
window: coupling.window,
|
|
649
|
+
target_changes: coupling.targetChanges,
|
|
650
|
+
skipped_large_commits: coupling.skippedLargeCommits,
|
|
651
|
+
})}>`
|
|
652
|
+
);
|
|
653
|
+
for (const file of coupling.files) {
|
|
654
|
+
out.push(
|
|
655
|
+
`<file${attributes({
|
|
656
|
+
path: file.path,
|
|
657
|
+
count: file.count,
|
|
658
|
+
confidence: file.confidence,
|
|
659
|
+
exists: file.exists === false ? 'false' : null,
|
|
660
|
+
})}/>`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
out.push('</coupling>');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
out.push('<related>');
|
|
667
|
+
for (const entry of data.related) {
|
|
668
|
+
out.push(
|
|
669
|
+
`<mention${attributes({
|
|
670
|
+
path: entry.path,
|
|
671
|
+
line: entry.line,
|
|
672
|
+
kind: entry.kind,
|
|
673
|
+
heading: entry.heading,
|
|
674
|
+
mentions: entry.mentions,
|
|
675
|
+
})}>${xml(entry.snippet)}</mention>`
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
out.push('</related>');
|
|
679
|
+
|
|
680
|
+
out.push('<notes>');
|
|
681
|
+
for (const note of data.notes) {
|
|
682
|
+
out.push(
|
|
683
|
+
`<note${attributes({ line: note.line, tag: note.tag, author: note.author })}>${xml(note.text)}</note>`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
out.push('</notes>');
|
|
687
|
+
|
|
688
|
+
out.push('<tests>');
|
|
689
|
+
for (const test of data.tests) {
|
|
690
|
+
out.push(
|
|
691
|
+
`<test_file${attributes({
|
|
692
|
+
path: test.path,
|
|
693
|
+
references: test.references,
|
|
694
|
+
total_matches: test.totalMatches,
|
|
695
|
+
})}>`
|
|
696
|
+
);
|
|
697
|
+
for (const match of test.matches) {
|
|
698
|
+
out.push(`<match${attributes({ line: match.line })}>${xml(match.text)}</match>`);
|
|
699
|
+
}
|
|
700
|
+
out.push('</test_file>');
|
|
701
|
+
}
|
|
702
|
+
out.push('</tests>');
|
|
703
|
+
|
|
704
|
+
if (data.question) out.push(`<question>${xml(data.question)}</question>`);
|
|
705
|
+
out.push('</why_context>');
|
|
706
|
+
return out.join('\n') + '\n';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
// JSON
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
function renderJson(data, budget) {
|
|
714
|
+
const { question, omitted, ...rest } = data;
|
|
715
|
+
const document = {
|
|
716
|
+
generator: 'ctxman why',
|
|
717
|
+
...rest,
|
|
718
|
+
trimmed: Object.keys(omitted).length ? { budget, omitted } : null,
|
|
719
|
+
question: question ?? null,
|
|
720
|
+
};
|
|
721
|
+
return JSON.stringify(document, null, 2) + '\n';
|
|
722
|
+
}
|