ctxman 3.0.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/custom-profiles.example.json +20 -0
- package/.ctxman/llm-profiles.json +131 -0
- package/.ctxman/logs/ctxman-2026-07-02.log +156 -0
- 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
- package/.ctxman/wizard-profiles/code-review/.contextignore +41 -0
- package/.ctxman/wizard-profiles/code-review/.contextinclude +30 -0
- package/.ctxman/wizard-profiles/code-review/.methodignore +26 -0
- package/.ctxman/wizard-profiles/code-review/.methodinclude +47 -0
- package/.ctxman/wizard-profiles/code-review/profile.json +26 -0
- package/.ctxman/wizard-profiles/documentation/.contextignore +21 -0
- package/.ctxman/wizard-profiles/documentation/.contextinclude +24 -0
- package/.ctxman/wizard-profiles/documentation/.methodignore +19 -0
- package/.ctxman/wizard-profiles/documentation/.methodinclude +44 -0
- package/.ctxman/wizard-profiles/documentation/profile.json +27 -0
- package/.ctxman/wizard-profiles/full/.contextignore +23 -0
- package/.ctxman/wizard-profiles/full/.contextinclude +38 -0
- package/.ctxman/wizard-profiles/full/.methodignore +5 -0
- package/.ctxman/wizard-profiles/full/.methodinclude +5 -0
- package/.ctxman/wizard-profiles/full/profile.json +28 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextignore +25 -0
- package/.ctxman/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodignore +19 -0
- package/.ctxman/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/.ctxman/wizard-profiles/llm-explain/profile.json +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextignore +27 -0
- package/.ctxman/wizard-profiles/minimal/.contextinclude +14 -0
- package/.ctxman/wizard-profiles/minimal/.methodignore +22 -0
- package/.ctxman/wizard-profiles/minimal/.methodinclude +18 -0
- package/.ctxman/wizard-profiles/minimal/profile.json +26 -0
- package/.ctxman/wizard-profiles/security-audit/.contextignore +36 -0
- package/.ctxman/wizard-profiles/security-audit/.contextinclude +37 -0
- package/.ctxman/wizard-profiles/security-audit/.methodignore +29 -0
- package/.ctxman/wizard-profiles/security-audit/.methodinclude +60 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +34 -0
- package/README.md +1167 -0
- package/bin/cli.js +650 -0
- package/bin/cm-ask.js +95 -0
- package/bin/cm-gitingest.js +134 -0
- package/bin/cm-update.js +175 -0
- package/bin/mcp-server.js +31 -0
- package/ctxman.js +236 -0
- package/examples/README.md +298 -0
- package/examples/custom-llm-profiles.example.json +19 -0
- package/examples/wizard-profiles/code-review/.contextignore +41 -0
- package/examples/wizard-profiles/code-review/.contextinclude +30 -0
- package/examples/wizard-profiles/code-review/.methodignore +26 -0
- package/examples/wizard-profiles/code-review/.methodinclude +47 -0
- package/examples/wizard-profiles/code-review/profile.json +26 -0
- package/examples/wizard-profiles/documentation/.contextignore +21 -0
- package/examples/wizard-profiles/documentation/.contextinclude +24 -0
- package/examples/wizard-profiles/documentation/.methodignore +19 -0
- package/examples/wizard-profiles/documentation/.methodinclude +44 -0
- package/examples/wizard-profiles/documentation/profile.json +27 -0
- package/examples/wizard-profiles/full/.contextignore +23 -0
- package/examples/wizard-profiles/full/.contextinclude +38 -0
- package/examples/wizard-profiles/full/.methodignore +5 -0
- package/examples/wizard-profiles/full/.methodinclude +5 -0
- package/examples/wizard-profiles/full/profile.json +28 -0
- package/examples/wizard-profiles/llm-explain/.contextignore +25 -0
- package/examples/wizard-profiles/llm-explain/.contextinclude +33 -0
- package/examples/wizard-profiles/llm-explain/.methodignore +19 -0
- package/examples/wizard-profiles/llm-explain/.methodinclude +48 -0
- package/examples/wizard-profiles/llm-explain/profile.json +27 -0
- package/examples/wizard-profiles/minimal/.contextignore +27 -0
- package/examples/wizard-profiles/minimal/.contextinclude +14 -0
- package/examples/wizard-profiles/minimal/.methodignore +22 -0
- package/examples/wizard-profiles/minimal/.methodinclude +18 -0
- package/examples/wizard-profiles/minimal/profile.json +26 -0
- package/examples/wizard-profiles/security-audit/.contextignore +36 -0
- package/examples/wizard-profiles/security-audit/.contextinclude +37 -0
- package/examples/wizard-profiles/security-audit/.methodignore +29 -0
- package/examples/wizard-profiles/security-audit/.methodinclude +60 -0
- package/examples/wizard-profiles/security-audit/profile.json +34 -0
- package/index.js +68 -0
- package/lib/agents/pr-failure-agent.js +201 -0
- package/lib/analyzers/go-method-analyzer.js +126 -0
- package/lib/analyzers/method-analyzer.js +457 -0
- package/lib/analyzers/token-calculator.js +577 -0
- package/lib/api/mcp/prompts.js +319 -0
- package/lib/api/mcp/resources.js +269 -0
- package/lib/api/mcp/server.js +339 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +63 -0
- package/lib/api/rest/server.js +405 -0
- package/lib/cache/CacheManager.js +388 -0
- package/lib/core/Analyzer.js +180 -0
- package/lib/core/ContextBuilder.js +325 -0
- package/lib/core/Reporter.js +270 -0
- package/lib/core/Scanner.js +129 -0
- package/lib/formatters/format-registry.js +419 -0
- package/lib/formatters/gitingest-formatter.js +746 -0
- package/lib/formatters/toon-formatter-v1.3.js +731 -0
- package/lib/formatters/toon-formatter.js +291 -0
- package/lib/integrations/git/BlameTracker.js +142 -0
- package/lib/integrations/git/DiffAnalyzer.js +260 -0
- package/lib/integrations/git/GitClient.js +336 -0
- package/lib/parsers/gitignore-parser.js +127 -0
- package/lib/parsers/method-filter-parser.js +50 -0
- package/lib/plugins/ExporterPlugin.js +102 -0
- package/lib/plugins/LanguagePlugin.js +100 -0
- package/lib/plugins/PluginManager.js +302 -0
- package/lib/rag/DataSourcePlugin.js +31 -0
- package/lib/rag/EmbeddingProvider.js +75 -0
- package/lib/rag/Indexer.js +83 -0
- package/lib/rag/VectorStoreFactory.js +24 -0
- package/lib/rag/sources/FileDataSource.js +70 -0
- package/lib/rag/stores/BaseVectorStore.js +43 -0
- package/lib/rag/stores/ChromaDBStore.js +85 -0
- package/lib/rag/stores/LanceDBStore.js +109 -0
- package/lib/rag/stores/PineconeStore.js +74 -0
- package/lib/ui/dashboard.js +109 -0
- package/lib/ui/index.js +15 -0
- package/lib/ui/progress-bar.js +110 -0
- package/lib/ui/select-input.js +51 -0
- package/lib/ui/wizard.js +359 -0
- package/lib/utils/clipboard-utils.js +66 -0
- package/lib/utils/config-utils.js +94 -0
- package/lib/utils/error-handler.js +145 -0
- package/lib/utils/file-utils.js +89 -0
- package/lib/utils/format-converter.js +325 -0
- package/lib/utils/git-utils.js +337 -0
- package/lib/utils/llm-detector.js +409 -0
- package/lib/utils/logger.js +278 -0
- package/lib/utils/token-utils.js +200 -0
- package/lib/utils/tokenizer-adapter.js +454 -0
- package/lib/utils/updater.js +565 -0
- package/lib/watch/FileWatcher.js +208 -0
- package/lib/watch/IncrementalAnalyzer.js +206 -0
- package/package.json +118 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON Format Wrapper
|
|
3
|
+
* Wraps official @toon-format/toon package with Ctxman integration
|
|
4
|
+
*
|
|
5
|
+
* Official TOON Spec v1.3: https://github.com/toon-format/spec
|
|
6
|
+
* Reference Implementation: https://github.com/toon-format/toon
|
|
7
|
+
*
|
|
8
|
+
* Features (100% Spec Compliant):
|
|
9
|
+
* - Array length markers [N]
|
|
10
|
+
* - Delimiter options: comma, tab, pipe
|
|
11
|
+
* - Length marker prefix: # (optional)
|
|
12
|
+
* - List format for non-uniform arrays
|
|
13
|
+
* - Smart quoting rules
|
|
14
|
+
* - Type normalizations
|
|
15
|
+
* - Full decoder support
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ToonFormatter {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.options = {
|
|
21
|
+
indent: options.indent || 2,
|
|
22
|
+
delimiter: options.delimiter || ',',
|
|
23
|
+
lengthMarker: options.lengthMarker || false
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Initialize indentChar
|
|
27
|
+
this.indentChar = ' '.repeat(this.options.indent);
|
|
28
|
+
|
|
29
|
+
// Lazy load official TOON package (ESM)
|
|
30
|
+
this._toonModule = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get official TOON module (lazy load)
|
|
35
|
+
*/
|
|
36
|
+
async getToonModule() {
|
|
37
|
+
if (!this._toonModule) {
|
|
38
|
+
this._toonModule = await import('@toon-format/toon');
|
|
39
|
+
}
|
|
40
|
+
return this._toonModule;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encode data to TOON format (Async - uses official package)
|
|
45
|
+
*/
|
|
46
|
+
async encodeAsync(data, options = {}) {
|
|
47
|
+
const toon = await this.getToonModule();
|
|
48
|
+
const opts = { ...this.options, ...options };
|
|
49
|
+
return toon.encode(data, opts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encode data to TOON format (Sync - uses custom implementation for backward compat)
|
|
54
|
+
*/
|
|
55
|
+
encode(data, options = {}) {
|
|
56
|
+
// Fallback to synchronous custom implementation
|
|
57
|
+
// This maintains backward compatibility with existing code
|
|
58
|
+
const opts = { ...this.options, ...options };
|
|
59
|
+
return this.encodeSync(data, opts);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Synchronous encoder (custom implementation - basic features only)
|
|
64
|
+
* For full spec compliance, use encodeAsync()
|
|
65
|
+
*/
|
|
66
|
+
encodeSync(data, options = {}) {
|
|
67
|
+
const { indent = 2 } = options;
|
|
68
|
+
this.indentChar = ' '.repeat(indent);
|
|
69
|
+
return this.encodeValue(data, 0, true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
encodeValue(value, indent = 0, compact = true) {
|
|
73
|
+
if (value === null || value === undefined) return 'null';
|
|
74
|
+
if (typeof value === 'boolean') return String(value);
|
|
75
|
+
if (typeof value === 'number') return String(value);
|
|
76
|
+
if (typeof value === 'string') return this.encodeString(value);
|
|
77
|
+
if (Array.isArray(value)) return this.encodeArray(value, indent, compact);
|
|
78
|
+
if (typeof value === 'object') return this.encodeObject(value, indent, compact);
|
|
79
|
+
return 'null';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
encodeString(str) {
|
|
83
|
+
const escaped = str
|
|
84
|
+
.replace(/\\/g, '\\\\')
|
|
85
|
+
.replace(/"/g, '\\"')
|
|
86
|
+
.replace(/\n/g, '\\n')
|
|
87
|
+
.replace(/\r/g, '\\r')
|
|
88
|
+
.replace(/\t/g, '\\t');
|
|
89
|
+
|
|
90
|
+
if (/[\s,:{}\[\]]/.test(escaped)) {
|
|
91
|
+
return `"${escaped}"`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return escaped;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
encodeArray(arr, indent = 0, compact = true) {
|
|
98
|
+
if (arr.length === 0) return '[]';
|
|
99
|
+
|
|
100
|
+
if (this.isTabularArray(arr)) {
|
|
101
|
+
return this.encodeTabular(arr, indent, compact);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return this.encodeRegularArray(arr, indent, compact);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isTabularArray(arr) {
|
|
108
|
+
if (arr.length < 2) return false;
|
|
109
|
+
if (!arr.every(item => typeof item === 'object' && item !== null && !Array.isArray(item))) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const firstKeys = Object.keys(arr[0]).sort();
|
|
114
|
+
return arr.every(item => {
|
|
115
|
+
const keys = Object.keys(item).sort();
|
|
116
|
+
return keys.length === firstKeys.length &&
|
|
117
|
+
keys.every((key, i) => key === firstKeys[i]);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
encodeTabular(arr, indent = 0, compact = true) {
|
|
122
|
+
const prefix = this.indentChar.repeat(indent);
|
|
123
|
+
const keys = Object.keys(arr[0]).sort();
|
|
124
|
+
|
|
125
|
+
let result = `{${keys.join(',')}}:\n`;
|
|
126
|
+
|
|
127
|
+
for (const item of arr) {
|
|
128
|
+
result += prefix + this.indentChar;
|
|
129
|
+
const values = keys.map(key => this.encodeValue(item[key], 0, true));
|
|
130
|
+
result += values.join(',') + '\n';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
encodeRegularArray(arr, indent = 0, compact = true) {
|
|
137
|
+
const prefix = this.indentChar.repeat(indent);
|
|
138
|
+
|
|
139
|
+
if (compact && arr.every(item => ['string', 'number', 'boolean'].includes(typeof item) || item === null)) {
|
|
140
|
+
return '[' + arr.map(item => this.encodeValue(item, 0, true)).join(',') + ']';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let result = '[\n';
|
|
144
|
+
for (let i = 0; i < arr.length; i++) {
|
|
145
|
+
result += prefix + this.indentChar + this.encodeValue(arr[i], indent + 1, compact);
|
|
146
|
+
if (i < arr.length - 1) result += ',';
|
|
147
|
+
result += '\n';
|
|
148
|
+
}
|
|
149
|
+
result += prefix + ']';
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
encodeObject(obj, indent = 0, compact = true) {
|
|
155
|
+
const keys = Object.keys(obj);
|
|
156
|
+
if (keys.length === 0) return '{}';
|
|
157
|
+
|
|
158
|
+
const prefix = this.indentChar.repeat(indent);
|
|
159
|
+
let result = '{\n';
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < keys.length; i++) {
|
|
162
|
+
const key = keys[i];
|
|
163
|
+
const value = obj[key];
|
|
164
|
+
result += prefix + this.indentChar + key + ': ';
|
|
165
|
+
|
|
166
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
167
|
+
result += '\n' + this.encodeObject(value, indent + 1, compact);
|
|
168
|
+
} else {
|
|
169
|
+
result += this.encodeValue(value, indent + 1, compact);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (i < keys.length - 1) result += ',';
|
|
173
|
+
result += '\n';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
result += prefix + '}';
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Decode TOON string to JavaScript value (Async - uses official package)
|
|
182
|
+
*/
|
|
183
|
+
async decodeAsync(toonString, options = {}) {
|
|
184
|
+
const toon = await this.getToonModule();
|
|
185
|
+
return toon.decode(toonString, options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Decode TOON string (Sync - throws error, decoder not available in sync mode)
|
|
190
|
+
*/
|
|
191
|
+
decode(toonString, options = {}) {
|
|
192
|
+
throw new Error('Synchronous decoder not available. Use decodeAsync() instead.');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate TOON string
|
|
197
|
+
*/
|
|
198
|
+
validate(toonString) {
|
|
199
|
+
const errors = [];
|
|
200
|
+
let braceDepth = 0;
|
|
201
|
+
let bracketDepth = 0;
|
|
202
|
+
|
|
203
|
+
for (const char of toonString) {
|
|
204
|
+
if (char === '{') braceDepth++;
|
|
205
|
+
if (char === '}') braceDepth--;
|
|
206
|
+
if (char === '[') bracketDepth++;
|
|
207
|
+
if (char === ']') bracketDepth--;
|
|
208
|
+
|
|
209
|
+
if (braceDepth < 0) errors.push('Unbalanced braces: extra closing brace');
|
|
210
|
+
if (bracketDepth < 0) errors.push('Unbalanced brackets: extra closing bracket');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (braceDepth > 0) errors.push('Unbalanced braces: missing closing braces');
|
|
214
|
+
if (bracketDepth > 0) errors.push('Unbalanced brackets: missing closing brackets');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
valid: errors.length === 0,
|
|
218
|
+
errors
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Estimate tokens (GPT-4 compatible)
|
|
224
|
+
*/
|
|
225
|
+
estimateTokens(toonString) {
|
|
226
|
+
return Math.ceil(toonString.length / 4);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Optimize TOON output
|
|
231
|
+
*/
|
|
232
|
+
optimize(toonString) {
|
|
233
|
+
const lines = toonString.split('\n');
|
|
234
|
+
const optimized = lines.map(line => line.trimEnd()).join('\n');
|
|
235
|
+
return optimized.replace(/\n{3,}/g, '\n\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Minify TOON output
|
|
240
|
+
*/
|
|
241
|
+
minify(toonString) {
|
|
242
|
+
return toonString
|
|
243
|
+
.split('\n')
|
|
244
|
+
.map(line => line.trim())
|
|
245
|
+
.filter(line => line.length > 0)
|
|
246
|
+
.join('\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Compare TOON with JSON (token savings)
|
|
251
|
+
*/
|
|
252
|
+
compareWithJSON(data) {
|
|
253
|
+
const toon = this.encode(data);
|
|
254
|
+
const json = JSON.stringify(data, null, 2);
|
|
255
|
+
|
|
256
|
+
const toonSize = toon.length;
|
|
257
|
+
const jsonSize = json.length;
|
|
258
|
+
const savings = jsonSize - toonSize;
|
|
259
|
+
const savingsPercentage = ((savings / jsonSize) * 100).toFixed(1);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
toon,
|
|
263
|
+
json,
|
|
264
|
+
toonSize,
|
|
265
|
+
jsonSize,
|
|
266
|
+
savings,
|
|
267
|
+
savingsPercentage: parseFloat(savingsPercentage),
|
|
268
|
+
toonTokens: this.estimateTokens(toon),
|
|
269
|
+
jsonTokens: this.estimateTokens(json)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Compare with official TOON encoder (async)
|
|
275
|
+
*/
|
|
276
|
+
async compareWithOfficial(data, options = {}) {
|
|
277
|
+
const customEncoded = this.encode(data);
|
|
278
|
+
const officialEncoded = await this.encodeAsync(data, options);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
custom: customEncoded,
|
|
282
|
+
official: officialEncoded,
|
|
283
|
+
customSize: customEncoded.length,
|
|
284
|
+
officialSize: officialEncoded.length,
|
|
285
|
+
difference: customEncoded.length - officialEncoded.length,
|
|
286
|
+
officialIsBetter: officialEncoded.length < customEncoded.length
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default ToonFormatter;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlameTracker - Author Attribution Module
|
|
3
|
+
* v3.0.0 - Complete Git integration
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Track file ownership
|
|
7
|
+
* - Author contribution analysis
|
|
8
|
+
* - Code ownership distribution
|
|
9
|
+
* - Hot spot detection
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import GitClient from './GitClient.js';
|
|
13
|
+
import { getLogger } from '../../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
const logger = getLogger('BlameTracker');
|
|
16
|
+
|
|
17
|
+
export class BlameTracker {
|
|
18
|
+
constructor(repositoryPath) {
|
|
19
|
+
this.git = new GitClient(repositoryPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get primary author for file (most commits)
|
|
24
|
+
* @param {string} filePath
|
|
25
|
+
* @returns {Author|null}
|
|
26
|
+
*/
|
|
27
|
+
getPrimaryAuthor(filePath) {
|
|
28
|
+
const authors = this.git.getFileAuthors(filePath);
|
|
29
|
+
return authors.length > 0 ? authors[0] : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get author contribution distribution
|
|
34
|
+
* @param {Array<string>} filePaths
|
|
35
|
+
* @returns {Map<string, AuthorStats>}
|
|
36
|
+
*/
|
|
37
|
+
getAuthorContributions(filePaths) {
|
|
38
|
+
const contributions = new Map();
|
|
39
|
+
|
|
40
|
+
for (const filePath of filePaths) {
|
|
41
|
+
const authors = this.git.getFileAuthors(filePath);
|
|
42
|
+
|
|
43
|
+
for (const author of authors) {
|
|
44
|
+
if (!contributions.has(author.email)) {
|
|
45
|
+
contributions.set(author.email, {
|
|
46
|
+
name: author.name,
|
|
47
|
+
email: author.email,
|
|
48
|
+
filesOwned: 0,
|
|
49
|
+
totalCommits: 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stats = contributions.get(author.email);
|
|
54
|
+
stats.filesOwned++;
|
|
55
|
+
stats.totalCommits += author.commits;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return contributions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get code ownership map
|
|
64
|
+
* @param {Array<string>} filePaths
|
|
65
|
+
* @returns {Map<string, Author>}
|
|
66
|
+
*/
|
|
67
|
+
getOwnershipMap(filePaths) {
|
|
68
|
+
const ownership = new Map();
|
|
69
|
+
|
|
70
|
+
for (const filePath of filePaths) {
|
|
71
|
+
const owner = this.getPrimaryAuthor(filePath);
|
|
72
|
+
if (owner) {
|
|
73
|
+
ownership.set(filePath, owner);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ownership;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect hot spots (frequently changed files)
|
|
82
|
+
* @param {Array<string>} filePaths
|
|
83
|
+
* @param {number} threshold - Minimum commits to be considered hot
|
|
84
|
+
* @returns {Array<HotSpot>}
|
|
85
|
+
*/
|
|
86
|
+
detectHotSpots(filePaths, threshold = 10) {
|
|
87
|
+
const hotSpots = [];
|
|
88
|
+
|
|
89
|
+
for (const filePath of filePaths) {
|
|
90
|
+
const commitCount = this.git.getCommitCount(filePath);
|
|
91
|
+
|
|
92
|
+
if (commitCount >= threshold) {
|
|
93
|
+
const authors = this.git.getFileAuthors(filePath);
|
|
94
|
+
|
|
95
|
+
hotSpots.push({
|
|
96
|
+
path: filePath,
|
|
97
|
+
commitCount,
|
|
98
|
+
authorCount: authors.length,
|
|
99
|
+
primaryAuthor: authors[0],
|
|
100
|
+
risk: this.calculateRiskScore(commitCount, authors.length)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return hotSpots.sort((a, b) => b.risk - a.risk);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Calculate risk score for a file
|
|
110
|
+
* @param {number} commitCount
|
|
111
|
+
* @param {number} authorCount
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
calculateRiskScore(commitCount, authorCount) {
|
|
115
|
+
// High commit count + multiple authors = higher risk
|
|
116
|
+
const commitScore = Math.log(commitCount + 1) * 10;
|
|
117
|
+
const authorScore = authorCount * 5;
|
|
118
|
+
return commitScore + authorScore;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get file ownership summary
|
|
123
|
+
* @param {Array<string>} filePaths
|
|
124
|
+
* @returns {object}
|
|
125
|
+
*/
|
|
126
|
+
getOwnershipSummary(filePaths) {
|
|
127
|
+
const contributions = this.getAuthorContributions(filePaths);
|
|
128
|
+
|
|
129
|
+
const topContributors = Array.from(contributions.values())
|
|
130
|
+
.sort((a, b) => b.totalCommits - a.totalCommits)
|
|
131
|
+
.slice(0, 10);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
totalAuthors: contributions.size,
|
|
135
|
+
totalFiles: filePaths.length,
|
|
136
|
+
topContributors,
|
|
137
|
+
averageFilesPerAuthor: filePaths.length / contributions.size
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default BlameTracker;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiffAnalyzer - Git Diff Analysis Module
|
|
3
|
+
* v3.0.0 - Complete Git integration
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Analyze git diffs
|
|
7
|
+
* - Detect change impact
|
|
8
|
+
* - Find related files
|
|
9
|
+
* - Generate change summaries
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import GitClient from './GitClient.js';
|
|
13
|
+
import { getLogger } from '../../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
const logger = getLogger('DiffAnalyzer');
|
|
16
|
+
|
|
17
|
+
export class DiffAnalyzer {
|
|
18
|
+
constructor(repositoryPath) {
|
|
19
|
+
this.git = new GitClient(repositoryPath);
|
|
20
|
+
this.repoPath = repositoryPath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Analyze changes since a reference point
|
|
25
|
+
* @param {string} since - Commit/branch reference
|
|
26
|
+
* @returns {ChangeAnalysis}
|
|
27
|
+
*/
|
|
28
|
+
analyzeChanges(since = null) {
|
|
29
|
+
logger.info(`Analyzing changes since: ${since || 'uncommitted'}`);
|
|
30
|
+
|
|
31
|
+
const changedFiles = since
|
|
32
|
+
? this.git.getChangedFiles(since)
|
|
33
|
+
: this.git.getAllModifiedFiles();
|
|
34
|
+
|
|
35
|
+
const analysis = {
|
|
36
|
+
changedFiles: changedFiles,
|
|
37
|
+
totalChangedFiles: changedFiles.length,
|
|
38
|
+
since: since,
|
|
39
|
+
relatedFiles: [],
|
|
40
|
+
impact: this.calculateImpact(changedFiles)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return analysis;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get changed files with detailed diff
|
|
48
|
+
* @param {string} since
|
|
49
|
+
* @returns {Array<FileChange>}
|
|
50
|
+
*/
|
|
51
|
+
getDetailedChanges(since = null) {
|
|
52
|
+
const changedFiles = since
|
|
53
|
+
? this.git.getChangedFiles(since)
|
|
54
|
+
: this.git.getAllModifiedFiles();
|
|
55
|
+
|
|
56
|
+
return changedFiles.map(filePath => {
|
|
57
|
+
const diff = this.getFileDiff(filePath, since);
|
|
58
|
+
return {
|
|
59
|
+
path: filePath,
|
|
60
|
+
...diff
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get diff for specific file
|
|
67
|
+
* @param {string} filePath
|
|
68
|
+
* @param {string} since
|
|
69
|
+
* @returns {FileDiff}
|
|
70
|
+
*/
|
|
71
|
+
getFileDiff(filePath, since = null) {
|
|
72
|
+
try {
|
|
73
|
+
const command = since
|
|
74
|
+
? `diff ${since} -- "${filePath}"`
|
|
75
|
+
: `diff -- "${filePath}"`;
|
|
76
|
+
|
|
77
|
+
const diffOutput = this.git.exec(command);
|
|
78
|
+
const stats = this.parseDiffStats(diffOutput);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
added: stats.added,
|
|
82
|
+
deleted: stats.deleted,
|
|
83
|
+
modified: stats.added + stats.deleted,
|
|
84
|
+
diff: diffOutput
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.warn(`Failed to get diff for ${filePath}: ${error.message}`);
|
|
88
|
+
return { added: 0, deleted: 0, modified: 0, diff: '' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse diff statistics
|
|
94
|
+
* @param {string} diffOutput
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
parseDiffStats(diffOutput) {
|
|
98
|
+
let added = 0;
|
|
99
|
+
let deleted = 0;
|
|
100
|
+
|
|
101
|
+
const lines = diffOutput.split('\n');
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
104
|
+
added++;
|
|
105
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
106
|
+
deleted++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { added, deleted };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculate change impact score
|
|
115
|
+
* @param {Array<string>} changedFiles
|
|
116
|
+
* @returns {object}
|
|
117
|
+
*/
|
|
118
|
+
calculateImpact(changedFiles) {
|
|
119
|
+
const impact = {
|
|
120
|
+
level: 'low',
|
|
121
|
+
score: 0,
|
|
122
|
+
details: {
|
|
123
|
+
coreFiles: 0,
|
|
124
|
+
configFiles: 0,
|
|
125
|
+
testFiles: 0,
|
|
126
|
+
documentationFiles: 0
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const file of changedFiles) {
|
|
131
|
+
const fileLower = file.toLowerCase();
|
|
132
|
+
|
|
133
|
+
// Core files (high impact)
|
|
134
|
+
if (fileLower.includes('src/') || fileLower.includes('lib/') ||
|
|
135
|
+
fileLower.includes('index.') || fileLower.includes('main.') ||
|
|
136
|
+
fileLower.includes('app.') || fileLower.includes('server.')) {
|
|
137
|
+
impact.details.coreFiles++;
|
|
138
|
+
impact.score += 10;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Config files (medium-high impact)
|
|
142
|
+
if (fileLower.endsWith('.json') || fileLower.endsWith('.yml') ||
|
|
143
|
+
fileLower.endsWith('.yaml') || fileLower.endsWith('.config.js')) {
|
|
144
|
+
impact.details.configFiles++;
|
|
145
|
+
impact.score += 7;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Test files (medium impact)
|
|
149
|
+
if (fileLower.includes('test') || fileLower.includes('spec')) {
|
|
150
|
+
impact.details.testFiles++;
|
|
151
|
+
impact.score += 3;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Documentation (low impact)
|
|
155
|
+
if (fileLower.endsWith('.md') || fileLower.includes('docs/')) {
|
|
156
|
+
impact.details.documentationFiles++;
|
|
157
|
+
impact.score += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Determine impact level
|
|
162
|
+
if (impact.score >= 50) {
|
|
163
|
+
impact.level = 'critical';
|
|
164
|
+
} else if (impact.score >= 25) {
|
|
165
|
+
impact.level = 'high';
|
|
166
|
+
} else if (impact.score >= 10) {
|
|
167
|
+
impact.level = 'medium';
|
|
168
|
+
} else {
|
|
169
|
+
impact.level = 'low';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return impact;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Find related files based on imports/dependencies
|
|
177
|
+
* @param {Array<string>} changedFiles
|
|
178
|
+
* @returns {Array<string>}
|
|
179
|
+
*/
|
|
180
|
+
findRelatedFiles(changedFiles) {
|
|
181
|
+
// TODO: Implement import graph analysis
|
|
182
|
+
// For now, return empty array
|
|
183
|
+
logger.debug('Related files detection not yet implemented');
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get file change frequency (how often file is modified)
|
|
189
|
+
* @param {string} filePath
|
|
190
|
+
* @returns {number}
|
|
191
|
+
*/
|
|
192
|
+
getFileChangeFrequency(filePath) {
|
|
193
|
+
return this.git.getCommitCount(filePath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Compare two branches
|
|
198
|
+
* @param {string} base - Base branch
|
|
199
|
+
* @param {string} compare - Compare branch
|
|
200
|
+
* @returns {BranchComparison}
|
|
201
|
+
*/
|
|
202
|
+
compareBranches(base, compare) {
|
|
203
|
+
try {
|
|
204
|
+
const command = `diff --name-status ${base}...${compare}`;
|
|
205
|
+
const output = this.git.exec(command);
|
|
206
|
+
|
|
207
|
+
const changes = {
|
|
208
|
+
added: [],
|
|
209
|
+
modified: [],
|
|
210
|
+
deleted: [],
|
|
211
|
+
renamed: []
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (!output) return changes;
|
|
215
|
+
|
|
216
|
+
const lines = output.split('\n');
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
const [status, ...pathParts] = line.split('\t');
|
|
219
|
+
const filePath = pathParts.join('\t');
|
|
220
|
+
|
|
221
|
+
if (status === 'A') changes.added.push(filePath);
|
|
222
|
+
else if (status === 'M') changes.modified.push(filePath);
|
|
223
|
+
else if (status === 'D') changes.deleted.push(filePath);
|
|
224
|
+
else if (status.startsWith('R')) changes.renamed.push(filePath);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return changes;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error(`Branch comparison failed: ${error.message}`);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get commit range statistics
|
|
236
|
+
* @param {string} range - Commit range (e.g., 'v1.0.0..v2.0.0')
|
|
237
|
+
* @returns {object}
|
|
238
|
+
*/
|
|
239
|
+
getRangeStats(range) {
|
|
240
|
+
try {
|
|
241
|
+
const command = `diff --shortstat ${range}`;
|
|
242
|
+
const output = this.git.exec(command);
|
|
243
|
+
|
|
244
|
+
// Parse: "3 files changed, 45 insertions(+), 12 deletions(-)"
|
|
245
|
+
const match = output.match(/(\d+) file[s]? changed(?:, (\d+) insertion[s]?\(\+\))?(?:, (\d+) deletion[s]?\(-\))?/);
|
|
246
|
+
|
|
247
|
+
if (!match) return null;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
filesChanged: parseInt(match[1], 10),
|
|
251
|
+
insertions: parseInt(match[2] || 0, 10),
|
|
252
|
+
deletions: parseInt(match[3] || 0, 10)
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default DiffAnalyzer;
|