composto-ai 0.1.1 → 0.2.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/README.md +130 -192
- package/dist/chunk-MFNGSHTZ.js +1163 -0
- package/dist/index.js +31 -1164
- package/dist/mcp/server.js +202 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,1079 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
benchmarkFile,
|
|
4
|
+
computeHealthFromTrends,
|
|
5
|
+
detectDecay,
|
|
6
|
+
detectHotspots,
|
|
7
|
+
detectInconsistencies,
|
|
8
|
+
estimateTokens,
|
|
9
|
+
generateLayer,
|
|
10
|
+
getGitLog,
|
|
11
|
+
loadConfig,
|
|
12
|
+
packContext,
|
|
13
|
+
runDetector,
|
|
14
|
+
summarize
|
|
15
|
+
} from "./chunk-MFNGSHTZ.js";
|
|
2
16
|
|
|
3
17
|
// src/cli/commands.ts
|
|
4
|
-
import { readFileSync
|
|
5
|
-
import { join
|
|
6
|
-
|
|
7
|
-
// src/config/loader.ts
|
|
8
|
-
import { readFileSync, existsSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
import { parse } from "yaml";
|
|
11
|
-
var DEFAULT_CONFIG = {
|
|
12
|
-
watchers: {
|
|
13
|
-
security: {
|
|
14
|
-
enabled: true,
|
|
15
|
-
severity: { "src/**": "warning", "tests/**": "info" }
|
|
16
|
-
},
|
|
17
|
-
deadCode: { enabled: true, trigger: "on-commit" },
|
|
18
|
-
consoleLog: { enabled: true, severity: { "src/**": "warning", "tests/**": "info" } }
|
|
19
|
-
},
|
|
20
|
-
agents: {
|
|
21
|
-
fixer: { enabled: true, model: "haiku" },
|
|
22
|
-
reviewer: { enabled: false, model: "sonnet" }
|
|
23
|
-
},
|
|
24
|
-
ir: {
|
|
25
|
-
deltaContextLines: 3,
|
|
26
|
-
confidenceThreshold: 0.6,
|
|
27
|
-
genericPatterns: "default"
|
|
28
|
-
},
|
|
29
|
-
trends: {
|
|
30
|
-
enabled: true,
|
|
31
|
-
hotspotThreshold: 10,
|
|
32
|
-
bugFixRatioThreshold: 0.5,
|
|
33
|
-
decayCheckTrigger: "on-commit",
|
|
34
|
-
fullReportSchedule: "weekly"
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
function deepMerge(target, source) {
|
|
38
|
-
const result = { ...target };
|
|
39
|
-
for (const key of Object.keys(source)) {
|
|
40
|
-
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
41
|
-
result[key] = deepMerge(target[key] ?? {}, source[key]);
|
|
42
|
-
} else {
|
|
43
|
-
result[key] = source[key];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
function parseConfig(yamlContent) {
|
|
49
|
-
if (!yamlContent.trim()) return { ...DEFAULT_CONFIG };
|
|
50
|
-
const parsed = parse(yamlContent) ?? {};
|
|
51
|
-
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
52
|
-
}
|
|
53
|
-
function loadConfig(projectPath) {
|
|
54
|
-
const configPath = join(projectPath, ".composto", "config.yaml");
|
|
55
|
-
if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
|
|
56
|
-
const content = readFileSync(configPath, "utf-8");
|
|
57
|
-
return parseConfig(content);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/watcher/detector.ts
|
|
61
|
-
import picomatch from "picomatch";
|
|
62
|
-
function getSeverity(filePath, severityMap) {
|
|
63
|
-
for (const [glob, severity] of Object.entries(severityMap)) {
|
|
64
|
-
if (picomatch.isMatch(filePath, glob)) return severity;
|
|
65
|
-
}
|
|
66
|
-
return "info";
|
|
67
|
-
}
|
|
68
|
-
var SECRET_PATTERNS = [
|
|
69
|
-
/["'](sk-[a-zA-Z0-9-]{20,})["']/,
|
|
70
|
-
/["'](AKIA[0-9A-Z]{16})["']/,
|
|
71
|
-
/["'](ghp_[a-zA-Z0-9]{36})["']/,
|
|
72
|
-
/(?:password|secret|token|api_?key)\s*[:=]\s*["']([^"']{8,})["']/i
|
|
73
|
-
];
|
|
74
|
-
function securityRule(code, filePath, severityMap) {
|
|
75
|
-
const findings = [];
|
|
76
|
-
const severity = getSeverity(filePath, severityMap);
|
|
77
|
-
const lines = code.split("\n");
|
|
78
|
-
for (let i = 0; i < lines.length; i++) {
|
|
79
|
-
const line = lines[i];
|
|
80
|
-
if (line.trim().startsWith("//") || line.trim().startsWith("/*")) continue;
|
|
81
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
82
|
-
if (pattern.test(line)) {
|
|
83
|
-
findings.push({
|
|
84
|
-
watcherId: "security",
|
|
85
|
-
severity,
|
|
86
|
-
file: filePath,
|
|
87
|
-
line: i + 1,
|
|
88
|
-
message: "Potential hardcoded secret detected",
|
|
89
|
-
action: {
|
|
90
|
-
type: "agent-required",
|
|
91
|
-
agentHint: { role: "fixer", model: "haiku", contextFiles: [filePath] }
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return findings;
|
|
99
|
-
}
|
|
100
|
-
function consoleLogRule(code, filePath, severityMap) {
|
|
101
|
-
const findings = [];
|
|
102
|
-
const severity = getSeverity(filePath, severityMap);
|
|
103
|
-
const lines = code.split("\n");
|
|
104
|
-
for (let i = 0; i < lines.length; i++) {
|
|
105
|
-
if (/\bconsole\.(log|debug|info|warn)\b/.test(lines[i])) {
|
|
106
|
-
findings.push({
|
|
107
|
-
watcherId: "consoleLog",
|
|
108
|
-
severity,
|
|
109
|
-
file: filePath,
|
|
110
|
-
line: i + 1,
|
|
111
|
-
message: "console.log detected \u2014 likely debug artifact",
|
|
112
|
-
action: { type: "auto-fix", autoFix: "remove-line" }
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return findings;
|
|
117
|
-
}
|
|
118
|
-
var RULES = {
|
|
119
|
-
security: securityRule,
|
|
120
|
-
consoleLog: consoleLogRule
|
|
121
|
-
};
|
|
122
|
-
function runDetector(code, filePath, watcherConfigs) {
|
|
123
|
-
const findings = [];
|
|
124
|
-
for (const [name, config] of Object.entries(watcherConfigs)) {
|
|
125
|
-
if (!config.enabled) continue;
|
|
126
|
-
const rule = RULES[name];
|
|
127
|
-
if (rule && config.severity) {
|
|
128
|
-
findings.push(...rule(code, filePath, config.severity));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return findings;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// src/ir/structure.ts
|
|
135
|
-
var CLASSIFIERS = [
|
|
136
|
-
[/^(function|def|fn|func)\b/, "function-start"],
|
|
137
|
-
[/^(class|struct|interface)\b/, "type-start"],
|
|
138
|
-
[/^(if|else|elif|switch|match|case)\b/, "branch"],
|
|
139
|
-
[/^(for|while|loop|do)\b/, "loop"],
|
|
140
|
-
[/^(return|yield)\b/, "exit"],
|
|
141
|
-
[/^(import|require|use|from)\b/, "import"],
|
|
142
|
-
[/^(export|pub|public)\b/, "export"],
|
|
143
|
-
[/^(const|let|var|val|mut)\b/, "assignment"],
|
|
144
|
-
[/^(try|catch|except|finally)\b/, "error-handling"],
|
|
145
|
-
[/^(async|await)\b/, "async"],
|
|
146
|
-
[/^(\/\/|\/\*|#)/, "comment"]
|
|
147
|
-
];
|
|
148
|
-
function classifyLine(firstToken) {
|
|
149
|
-
if (firstToken === "") return "blank";
|
|
150
|
-
for (const [pattern, type] of CLASSIFIERS) {
|
|
151
|
-
if (pattern.test(firstToken)) return type;
|
|
152
|
-
}
|
|
153
|
-
return "unknown";
|
|
154
|
-
}
|
|
155
|
-
function extractStructure(code) {
|
|
156
|
-
const lines = code.split("\n");
|
|
157
|
-
return lines.map((raw, i) => {
|
|
158
|
-
const indent = raw.search(/\S/);
|
|
159
|
-
const trimmed = raw.trim();
|
|
160
|
-
const firstToken = trimmed.split(/[\s({<]/)[0];
|
|
161
|
-
const type = classifyLine(firstToken);
|
|
162
|
-
return {
|
|
163
|
-
line: i + 1,
|
|
164
|
-
indent: indent === -1 ? -1 : indent,
|
|
165
|
-
type,
|
|
166
|
-
raw
|
|
167
|
-
};
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// src/ir/fingerprint.ts
|
|
172
|
-
var PATTERNS = [
|
|
173
|
-
// import type { x, y } from "module"
|
|
174
|
-
{
|
|
175
|
-
match: /^import\s+type\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
176
|
-
transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
|
|
177
|
-
confidence: 0.95
|
|
178
|
-
},
|
|
179
|
-
// import { x, y } from "module"
|
|
180
|
-
{
|
|
181
|
-
match: /^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
182
|
-
transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
|
|
183
|
-
confidence: 0.95
|
|
184
|
-
},
|
|
185
|
-
// import type x from "module"
|
|
186
|
-
{
|
|
187
|
-
match: /^import\s+type\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
188
|
-
transform: (m) => `USE:${m[2]}{${m[1]}}`,
|
|
189
|
-
confidence: 0.95
|
|
190
|
-
},
|
|
191
|
-
// import x from "module"
|
|
192
|
-
{
|
|
193
|
-
match: /^import\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
194
|
-
transform: (m) => `USE:${m[2]}{${m[1]}}`,
|
|
195
|
-
confidence: 0.95
|
|
196
|
-
},
|
|
197
|
-
// const x = require("module")
|
|
198
|
-
{
|
|
199
|
-
match: /^(?:const|let|var)\s+(\w+)\s*=\s*require\(["']([^"']+)["']\);?\s*$/,
|
|
200
|
-
transform: (m) => `USE:${m[2]}{${m[1]}}`,
|
|
201
|
-
confidence: 0.95
|
|
202
|
-
},
|
|
203
|
-
// export function name(params) {
|
|
204
|
-
{
|
|
205
|
-
match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
|
|
206
|
-
transform: (m) => `OUT FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
|
|
207
|
-
confidence: 0.95
|
|
208
|
-
},
|
|
209
|
-
// function name(params) {
|
|
210
|
-
{
|
|
211
|
-
match: /^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
|
|
212
|
-
transform: (m) => `FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
|
|
213
|
-
confidence: 0.95
|
|
214
|
-
},
|
|
215
|
-
// export class Name extends Base {
|
|
216
|
-
{
|
|
217
|
-
match: /^export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
|
|
218
|
-
transform: (m) => `OUT CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
|
|
219
|
-
confidence: 0.95
|
|
220
|
-
},
|
|
221
|
-
// class Name extends Base {
|
|
222
|
-
{
|
|
223
|
-
match: /^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
|
|
224
|
-
transform: (m) => `CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
|
|
225
|
-
confidence: 0.95
|
|
226
|
-
},
|
|
227
|
-
// if (cond) return expr;
|
|
228
|
-
{
|
|
229
|
-
match: /^if\s*\(([^)]+)\)\s*return\s+(.+);?\s*$/,
|
|
230
|
-
transform: (m) => `IF:${m[1].trim()} -> RET ${m[2].trim().replace(/;$/, "")}`,
|
|
231
|
-
confidence: 0.95
|
|
232
|
-
},
|
|
233
|
-
// if (cond) {
|
|
234
|
-
{
|
|
235
|
-
match: /^if\s*\(([^)]+)\)\s*\{?\s*$/,
|
|
236
|
-
transform: (m) => `IF:${m[1].trim()}`,
|
|
237
|
-
confidence: 0.9
|
|
238
|
-
},
|
|
239
|
-
// for (... of/in ...) {
|
|
240
|
-
{
|
|
241
|
-
match: /^for\s*\((?:const|let|var)\s+(\w+)\s+(?:of|in)\s+(\w+)\)\s*\{?\s*$/,
|
|
242
|
-
transform: (m) => `LOOP:${m[2]} -> ${m[1]}`,
|
|
243
|
-
confidence: 0.9
|
|
244
|
-
},
|
|
245
|
-
// return expr
|
|
246
|
-
{
|
|
247
|
-
match: /^return\s+(.+);?\s*$/,
|
|
248
|
-
transform: (m) => `RET ${m[1].trim().replace(/;$/, "")}`,
|
|
249
|
-
confidence: 0.95
|
|
250
|
-
},
|
|
251
|
-
// return;
|
|
252
|
-
{
|
|
253
|
-
match: /^return;?\s*$/,
|
|
254
|
-
transform: () => "RET",
|
|
255
|
-
confidence: 0.95
|
|
256
|
-
},
|
|
257
|
-
// try {
|
|
258
|
-
{
|
|
259
|
-
match: /^try\s*\{\s*$/,
|
|
260
|
-
transform: () => "TRY:",
|
|
261
|
-
confidence: 0.9
|
|
262
|
-
},
|
|
263
|
-
// catch (e) {
|
|
264
|
-
{
|
|
265
|
-
match: /^(?:\}\s*)?catch\s*\((\w+)\)\s*\{?\s*$/,
|
|
266
|
-
transform: (m) => `CATCH:${m[1]}`,
|
|
267
|
-
confidence: 0.9
|
|
268
|
-
},
|
|
269
|
-
// switch (expr) {
|
|
270
|
-
{
|
|
271
|
-
match: /^switch\s*\(([^)]+)\)\s*\{?\s*$/,
|
|
272
|
-
transform: (m) => `SWITCH:${m[1].trim()}`,
|
|
273
|
-
confidence: 0.9
|
|
274
|
-
},
|
|
275
|
-
// case "value": / case value:
|
|
276
|
-
{
|
|
277
|
-
match: /^case\s+(.+)\s*:\s*$/,
|
|
278
|
-
transform: (m) => `CASE:${m[1].trim()}`,
|
|
279
|
-
confidence: 0.9
|
|
280
|
-
},
|
|
281
|
-
// default:
|
|
282
|
-
{
|
|
283
|
-
match: /^default\s*:\s*$/,
|
|
284
|
-
transform: () => "DEFAULT:",
|
|
285
|
-
confidence: 0.9
|
|
286
|
-
},
|
|
287
|
-
// export type Name = ...
|
|
288
|
-
{
|
|
289
|
-
match: /^export\s+type\s+(\w+)(?:<[^>]+>)?\s*=\s*(.+);?\s*$/,
|
|
290
|
-
transform: (m) => `OUT TYPE:${m[1]}`,
|
|
291
|
-
confidence: 0.9
|
|
292
|
-
},
|
|
293
|
-
// if (cond) expr; (inline if with method call)
|
|
294
|
-
{
|
|
295
|
-
match: /^if\s*\(([^)]+)\)\s+(\w+.+);?\s*$/,
|
|
296
|
-
transform: (m) => `IF:${m[1].trim()} -> ${m[2].replace(/;$/, "").trim().slice(0, 50)}`,
|
|
297
|
-
confidence: 0.9
|
|
298
|
-
},
|
|
299
|
-
// export async function name( (multiline signature)
|
|
300
|
-
{
|
|
301
|
-
match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(\s*$/,
|
|
302
|
-
transform: (m) => `OUT FN:${m[1]}(`,
|
|
303
|
-
confidence: 0.95
|
|
304
|
-
},
|
|
305
|
-
// interface/type property — name: type;
|
|
306
|
-
{
|
|
307
|
-
match: /^\s*(\w+)\??\s*:\s*(.+);?\s*$/,
|
|
308
|
-
transform: (m) => `PROP:${m[1]}: ${m[2].replace(/;$/, "").trim()}`,
|
|
309
|
-
confidence: 0.75
|
|
310
|
-
},
|
|
311
|
-
// const x = await expr;
|
|
312
|
-
{
|
|
313
|
-
match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*await\s+(.+);?\s*$/,
|
|
314
|
-
transform: (m) => {
|
|
315
|
-
const prefix = m[0].startsWith("export") ? "OUT " : "";
|
|
316
|
-
return `${prefix}AWAIT:VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
|
|
317
|
-
},
|
|
318
|
-
confidence: 0.85
|
|
319
|
-
},
|
|
320
|
-
// export const name = async (params) => { OR export const name = (params) => expr;
|
|
321
|
-
{
|
|
322
|
-
match: /^export\s+(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
|
|
323
|
-
transform: (m) => {
|
|
324
|
-
const asyncPrefix = m[2] ? "ASYNC " : "";
|
|
325
|
-
const body = m[4].replace(/[{;]\s*$/, "").trim();
|
|
326
|
-
return `OUT ${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
|
|
327
|
-
},
|
|
328
|
-
confidence: 0.9
|
|
329
|
-
},
|
|
330
|
-
// const name = async (params) => { OR const name = (params) => expr;
|
|
331
|
-
{
|
|
332
|
-
match: /^(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
|
|
333
|
-
transform: (m) => {
|
|
334
|
-
const asyncPrefix = m[2] ? "ASYNC " : "";
|
|
335
|
-
const body = m[4].replace(/[{;]\s*$/, "").trim();
|
|
336
|
-
return `${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
|
|
337
|
-
},
|
|
338
|
-
confidence: 0.9
|
|
339
|
-
},
|
|
340
|
-
// get name() {
|
|
341
|
-
{
|
|
342
|
-
match: /^\s*get\s+(\w+)\s*\(\)\s*(?::\s*\S+\s*)?\{?\s*$/,
|
|
343
|
-
transform: (m) => `GET:${m[1]}()`,
|
|
344
|
-
confidence: 0.9
|
|
345
|
-
},
|
|
346
|
-
// set name(value) {
|
|
347
|
-
{
|
|
348
|
-
match: /^\s*set\s+(\w+)\s*\(([^)]*)\)\s*\{?\s*$/,
|
|
349
|
-
transform: (m) => `SET:${m[1]}(${m[2].replace(/\s/g, "")})`,
|
|
350
|
-
confidence: 0.9
|
|
351
|
-
},
|
|
352
|
-
// methodName(params) { (inside class body, indented)
|
|
353
|
-
{
|
|
354
|
-
match: /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{\s*$/,
|
|
355
|
-
transform: (m) => {
|
|
356
|
-
const name = m[1];
|
|
357
|
-
if (["if", "for", "while", "switch", "catch", "function"].includes(name)) return `${name}`;
|
|
358
|
-
return `METHOD:${name}(${m[2].replace(/\s/g, "")})`;
|
|
359
|
-
},
|
|
360
|
-
confidence: 0.9
|
|
361
|
-
},
|
|
362
|
-
// const { a, b } = expr (object destructuring — before regular assignment)
|
|
363
|
-
{
|
|
364
|
-
match: /^(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(.+);?\s*$/,
|
|
365
|
-
transform: (m) => `VAR:{${m[1].replace(/\s/g, "")}} = ${m[2].replace(/;$/, "").trim()}`,
|
|
366
|
-
confidence: 0.9
|
|
367
|
-
},
|
|
368
|
-
// const [a, b] = expr (destructuring — before regular assignment)
|
|
369
|
-
{
|
|
370
|
-
match: /^(?:const|let|var)\s+\[([^\]]+)\]\s*=\s*(.+);?\s*$/,
|
|
371
|
-
transform: (m) => `VAR:[${m[1].replace(/\s/g, "")}] = ${m[2].replace(/;$/, "").trim()}`,
|
|
372
|
-
confidence: 0.9
|
|
373
|
-
},
|
|
374
|
-
// const name = value;
|
|
375
|
-
{
|
|
376
|
-
match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+);?\s*$/,
|
|
377
|
-
transform: (m) => {
|
|
378
|
-
const prefix = m[0].startsWith("export") ? "OUT " : "";
|
|
379
|
-
return `${prefix}VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
|
|
380
|
-
},
|
|
381
|
-
confidence: 0.85
|
|
382
|
-
}
|
|
383
|
-
];
|
|
384
|
-
function fingerprintLine(line) {
|
|
385
|
-
const trimmed = line.trim();
|
|
386
|
-
if (trimmed === "" || trimmed === "{" || trimmed === "}" || trimmed === "});" || trimmed === ");") {
|
|
387
|
-
return { type: "fingerprint", ir: "", confidence: 1 };
|
|
388
|
-
}
|
|
389
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
390
|
-
return { type: "fingerprint", ir: "", confidence: 1 };
|
|
391
|
-
}
|
|
392
|
-
for (const pattern of PATTERNS) {
|
|
393
|
-
const match = trimmed.match(pattern.match);
|
|
394
|
-
if (match) {
|
|
395
|
-
const ir = pattern.transform(match);
|
|
396
|
-
if (pattern.confidence > 0.9) {
|
|
397
|
-
return { type: "fingerprint", ir, confidence: pattern.confidence };
|
|
398
|
-
}
|
|
399
|
-
return {
|
|
400
|
-
type: "fingerprint+hint",
|
|
401
|
-
ir,
|
|
402
|
-
hint: trimmed,
|
|
403
|
-
confidence: pattern.confidence
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return { type: "raw", ir: trimmed, confidence: 0.1 };
|
|
408
|
-
}
|
|
409
|
-
function fingerprintFile(code, confidenceThreshold = 0.6) {
|
|
410
|
-
const lines = code.split("\n");
|
|
411
|
-
const irLines = [];
|
|
412
|
-
for (const line of lines) {
|
|
413
|
-
const indent = line.search(/\S/);
|
|
414
|
-
const indentStr = indent > 0 ? " ".repeat(Math.floor(indent / 2)) : "";
|
|
415
|
-
const result = fingerprintLine(line);
|
|
416
|
-
if (result.ir === "") continue;
|
|
417
|
-
if (result.confidence >= confidenceThreshold) {
|
|
418
|
-
irLines.push(`${indentStr}${result.ir}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return irLines.join("\n");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// src/ir/health.ts
|
|
425
|
-
var CHURN_THRESHOLD = 10;
|
|
426
|
-
var FIX_RATIO_THRESHOLD = 0.5;
|
|
427
|
-
function buildHealthTag(health) {
|
|
428
|
-
const parts = [];
|
|
429
|
-
if (health.churn > CHURN_THRESHOLD) parts.push(`HOT:${health.churn}/30`);
|
|
430
|
-
if (health.fixRatio > FIX_RATIO_THRESHOLD) parts.push(`FIX:${Math.round(health.fixRatio * 100)}%`);
|
|
431
|
-
if (health.coverageTrend === "down") parts.push("COV:\u2193");
|
|
432
|
-
if (health.consistency === "low") parts.push("INCON");
|
|
433
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
434
|
-
}
|
|
435
|
-
function annotateIR(ir, health) {
|
|
436
|
-
const tag = buildHealthTag(health);
|
|
437
|
-
if (!tag) return ir;
|
|
438
|
-
const lines = ir.split("\n");
|
|
439
|
-
lines[0] = `${lines[0]} ${tag}`;
|
|
440
|
-
return lines.join("\n");
|
|
441
|
-
}
|
|
442
|
-
function computeHealthFromTrends(file, trends) {
|
|
443
|
-
const hotspot = trends.hotspots.find((h) => h.file === file);
|
|
444
|
-
const decay = trends.decaySignals.find((d) => d.file === file);
|
|
445
|
-
const inconsistency = trends.inconsistencies.find((i) => i.file === file);
|
|
446
|
-
return {
|
|
447
|
-
churn: hotspot?.changesInLast30Commits ?? 0,
|
|
448
|
-
fixRatio: hotspot?.bugFixRatio ?? 0,
|
|
449
|
-
coverageTrend: decay?.trend === "declining" ? "down" : decay?.trend === "improving" ? "up" : "stable",
|
|
450
|
-
staleness: "",
|
|
451
|
-
authorCount: hotspot?.authorCount ?? 0,
|
|
452
|
-
consistency: inconsistency ? "low" : "high"
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// src/parser/init.ts
|
|
457
|
-
import { Parser, Language } from "web-tree-sitter";
|
|
458
|
-
import { resolve, dirname } from "path";
|
|
459
|
-
import { existsSync as existsSync2 } from "fs";
|
|
460
|
-
import { fileURLToPath } from "url";
|
|
461
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
462
|
-
var initialized = false;
|
|
463
|
-
var cache = /* @__PURE__ */ new Map();
|
|
464
|
-
function grammarPath(lang) {
|
|
465
|
-
const distPath = resolve(__dirname, "grammars", `tree-sitter-${lang}.wasm`);
|
|
466
|
-
if (existsSync2(distPath)) return distPath;
|
|
467
|
-
const devPath = resolve(__dirname, "../../grammars", `tree-sitter-${lang}.wasm`);
|
|
468
|
-
if (existsSync2(devPath)) return devPath;
|
|
469
|
-
throw new Error(`Grammar not found for ${lang}`);
|
|
470
|
-
}
|
|
471
|
-
async function getParser(lang) {
|
|
472
|
-
if (!initialized) {
|
|
473
|
-
await Parser.init();
|
|
474
|
-
initialized = true;
|
|
475
|
-
}
|
|
476
|
-
const cached = cache.get(lang);
|
|
477
|
-
if (cached) return cached;
|
|
478
|
-
const parser = new Parser();
|
|
479
|
-
const language = await Language.load(grammarPath(lang));
|
|
480
|
-
parser.setLanguage(language);
|
|
481
|
-
const result = { parser, language };
|
|
482
|
-
cache.set(lang, result);
|
|
483
|
-
return result;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// src/parser/languages.ts
|
|
487
|
-
import { extname } from "path";
|
|
488
|
-
var EXT_MAP = {
|
|
489
|
-
".ts": "typescript",
|
|
490
|
-
".tsx": "typescript",
|
|
491
|
-
".js": "javascript",
|
|
492
|
-
".jsx": "javascript",
|
|
493
|
-
".mjs": "javascript",
|
|
494
|
-
".py": "python",
|
|
495
|
-
".go": "go",
|
|
496
|
-
".rs": "rust"
|
|
497
|
-
};
|
|
498
|
-
var SUPPORTED_EXTENSIONS = Object.keys(EXT_MAP);
|
|
499
|
-
function detectLanguage(filePath) {
|
|
500
|
-
const ext = extname(filePath);
|
|
501
|
-
return EXT_MAP[ext] ?? null;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// src/ir/ast-walker.ts
|
|
505
|
-
var TIER_MAP = {
|
|
506
|
-
// Tier 1 — structural declarations
|
|
507
|
-
import_statement: "T1_KEEP",
|
|
508
|
-
function_declaration: "T1_KEEP",
|
|
509
|
-
class_declaration: "T1_KEEP",
|
|
510
|
-
interface_declaration: "T1_KEEP",
|
|
511
|
-
type_alias_declaration: "T1_KEEP",
|
|
512
|
-
enum_declaration: "T1_KEEP",
|
|
513
|
-
// Tier 2 — control flow
|
|
514
|
-
if_statement: "T2_CONTROL",
|
|
515
|
-
else_clause: "WALK_ONLY",
|
|
516
|
-
for_statement: "T2_CONTROL",
|
|
517
|
-
for_in_statement: "T2_CONTROL",
|
|
518
|
-
while_statement: "T2_CONTROL",
|
|
519
|
-
do_statement: "T2_CONTROL",
|
|
520
|
-
switch_statement: "T2_CONTROL",
|
|
521
|
-
switch_case: "T2_CONTROL",
|
|
522
|
-
switch_default: "T2_CONTROL",
|
|
523
|
-
return_statement: "T2_CONTROL",
|
|
524
|
-
throw_statement: "T2_CONTROL",
|
|
525
|
-
try_statement: "T2_CONTROL",
|
|
526
|
-
catch_clause: "T2_CONTROL",
|
|
527
|
-
// Tier 3 — compressible expressions
|
|
528
|
-
lexical_declaration: "T3_COMPRESS",
|
|
529
|
-
expression_statement: "T3_COMPRESS",
|
|
530
|
-
// Walk-only — containers that need traversal but no emission
|
|
531
|
-
program: "WALK_ONLY",
|
|
532
|
-
statement_block: "WALK_ONLY",
|
|
533
|
-
class_body: "WALK_ONLY",
|
|
534
|
-
switch_body: "WALK_ONLY",
|
|
535
|
-
export_statement: "WALK_ONLY"
|
|
536
|
-
};
|
|
537
|
-
function tierOf(nodeType) {
|
|
538
|
-
return TIER_MAP[nodeType] ?? "T4_DROP";
|
|
539
|
-
}
|
|
540
|
-
function collapseText(text, maxLen) {
|
|
541
|
-
const collapsed = text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
542
|
-
if (collapsed.length <= maxLen) return collapsed;
|
|
543
|
-
return collapsed.slice(0, maxLen - 3) + "...";
|
|
544
|
-
}
|
|
545
|
-
function getTypeParams(node) {
|
|
546
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
547
|
-
const child = node.child(i);
|
|
548
|
-
if (child.type === "type_parameters") {
|
|
549
|
-
return child.text;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return "";
|
|
553
|
-
}
|
|
554
|
-
function isExported(node) {
|
|
555
|
-
return node.parent?.type === "export_statement";
|
|
556
|
-
}
|
|
557
|
-
function isAsync(node) {
|
|
558
|
-
return node.text.trimStart().startsWith("async");
|
|
559
|
-
}
|
|
560
|
-
function extractCondition(node) {
|
|
561
|
-
const condNode = node.childForFieldName("condition") ?? (() => {
|
|
562
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
563
|
-
const c = node.child(i);
|
|
564
|
-
if (c.type === "parenthesized_expression") return c;
|
|
565
|
-
}
|
|
566
|
-
return null;
|
|
567
|
-
})();
|
|
568
|
-
if (!condNode) return "...";
|
|
569
|
-
const text = condNode.text.replace(/^\(/, "").replace(/\)$/, "").trim();
|
|
570
|
-
return text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
571
|
-
}
|
|
572
|
-
function emitTier2(node) {
|
|
573
|
-
switch (node.type) {
|
|
574
|
-
case "if_statement": {
|
|
575
|
-
const cond = extractCondition(node);
|
|
576
|
-
return `IF:${cond}`;
|
|
577
|
-
}
|
|
578
|
-
case "else_clause":
|
|
579
|
-
return "ELSE:";
|
|
580
|
-
case "for_statement":
|
|
581
|
-
case "for_in_statement":
|
|
582
|
-
return "LOOP";
|
|
583
|
-
case "while_statement": {
|
|
584
|
-
const cond = extractCondition(node);
|
|
585
|
-
return `WHILE:${cond}`;
|
|
586
|
-
}
|
|
587
|
-
case "do_statement": {
|
|
588
|
-
const cond = extractCondition(node);
|
|
589
|
-
return `WHILE:${cond}`;
|
|
590
|
-
}
|
|
591
|
-
case "switch_statement": {
|
|
592
|
-
const expr = node.childForFieldName("value") ?? node.childForFieldName("condition") ?? (() => {
|
|
593
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
594
|
-
const c = node.child(i);
|
|
595
|
-
if (c.type === "parenthesized_expression") return c;
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
})();
|
|
599
|
-
const text = expr ? expr.text.replace(/^\(/, "").replace(/\)$/, "").trim() : "...";
|
|
600
|
-
return `SWITCH:${text.length > 60 ? text.slice(0, 57) + "..." : text}`;
|
|
601
|
-
}
|
|
602
|
-
case "switch_case": {
|
|
603
|
-
let value = null;
|
|
604
|
-
const valNode = node.childForFieldName("value");
|
|
605
|
-
if (valNode) {
|
|
606
|
-
value = valNode.text;
|
|
607
|
-
} else {
|
|
608
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
609
|
-
const c = node.child(i);
|
|
610
|
-
if (c.type !== "case" && c.type !== ":" && c.childCount === 0 && c.text === "case") continue;
|
|
611
|
-
if (c.type !== "case" && c.text !== "case" && c.text !== ":") {
|
|
612
|
-
value = c.text;
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return `CASE:${value ?? "..."}`;
|
|
618
|
-
}
|
|
619
|
-
case "switch_default":
|
|
620
|
-
return "DEFAULT:";
|
|
621
|
-
case "return_statement": {
|
|
622
|
-
let retText = "";
|
|
623
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
624
|
-
const c = node.child(i);
|
|
625
|
-
if (c.text !== "return" && c.text !== ";") {
|
|
626
|
-
retText += (retText ? " " : "") + c.text;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
retText = retText.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
630
|
-
if (!retText) return "RET";
|
|
631
|
-
return `RET ${retText.length > 60 ? retText.slice(0, 57) + "..." : retText}`;
|
|
632
|
-
}
|
|
633
|
-
case "throw_statement": {
|
|
634
|
-
let throwText = "";
|
|
635
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
636
|
-
const c = node.child(i);
|
|
637
|
-
if (c.text !== "throw" && c.text !== ";") {
|
|
638
|
-
throwText += (throwText ? " " : "") + c.text;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
throwText = throwText.trim();
|
|
642
|
-
return `THROW:${throwText.length > 60 ? throwText.slice(0, 57) + "..." : throwText}`;
|
|
643
|
-
}
|
|
644
|
-
case "try_statement":
|
|
645
|
-
return "TRY";
|
|
646
|
-
case "catch_clause": {
|
|
647
|
-
const param = node.childForFieldName("parameter");
|
|
648
|
-
const paramText = param ? param.text : "...";
|
|
649
|
-
return `CATCH:${paramText}`;
|
|
650
|
-
}
|
|
651
|
-
default:
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
function emitTier1(node) {
|
|
656
|
-
const exported = isExported(node);
|
|
657
|
-
const outPrefix = exported ? "OUT " : "";
|
|
658
|
-
switch (node.type) {
|
|
659
|
-
case "import_statement": {
|
|
660
|
-
const text = collapseText(node.text, 80);
|
|
661
|
-
return `USE:${text}`;
|
|
662
|
-
}
|
|
663
|
-
case "function_declaration": {
|
|
664
|
-
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
665
|
-
const rawParams = node.childForFieldName("parameters")?.text ?? "()";
|
|
666
|
-
const params = collapseText(rawParams, 60);
|
|
667
|
-
const asyncPrefix = isAsync(node) ? "ASYNC " : "";
|
|
668
|
-
return `${outPrefix}${asyncPrefix}FN:${name}${params}`;
|
|
669
|
-
}
|
|
670
|
-
case "class_declaration": {
|
|
671
|
-
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
672
|
-
const typeParams = getTypeParams(node);
|
|
673
|
-
return `${outPrefix}CLASS:${name}${typeParams}`;
|
|
674
|
-
}
|
|
675
|
-
case "interface_declaration": {
|
|
676
|
-
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
677
|
-
const typeParams = getTypeParams(node);
|
|
678
|
-
return `${outPrefix}INTERFACE:${name}${typeParams}`;
|
|
679
|
-
}
|
|
680
|
-
case "type_alias_declaration": {
|
|
681
|
-
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
682
|
-
return `${outPrefix}TYPE:${name}`;
|
|
683
|
-
}
|
|
684
|
-
case "enum_declaration": {
|
|
685
|
-
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
686
|
-
return `${outPrefix}ENUM:${name}`;
|
|
687
|
-
}
|
|
688
|
-
default:
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function emitTier3(node) {
|
|
693
|
-
switch (node.type) {
|
|
694
|
-
case "lexical_declaration": {
|
|
695
|
-
let declarator = null;
|
|
696
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
697
|
-
const c = node.child(i);
|
|
698
|
-
if (c.type === "variable_declarator") {
|
|
699
|
-
declarator = c;
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (!declarator) return null;
|
|
704
|
-
const name = declarator.childForFieldName("name")?.text ?? "?";
|
|
705
|
-
const value = declarator.childForFieldName("value");
|
|
706
|
-
if (value) {
|
|
707
|
-
if (value.type === "arrow_function") {
|
|
708
|
-
const asyncPrefix = isAsync(value) ? "ASYNC " : "";
|
|
709
|
-
const params = value.childForFieldName("parameters")?.text ?? "()";
|
|
710
|
-
return `${asyncPrefix}FN:${name}${collapseText(params, 60)} => ...`;
|
|
711
|
-
}
|
|
712
|
-
if (value.type === "await_expression") {
|
|
713
|
-
const callee = value.childCount > 1 ? value.child(1).text : "...";
|
|
714
|
-
return `AWAIT:${name}=${collapseText(callee, 40)}`;
|
|
715
|
-
}
|
|
716
|
-
if (node.parent?.type === "statement_block") return null;
|
|
717
|
-
const vt = value.type;
|
|
718
|
-
if (vt === "number" || vt === "true" || vt === "false") return null;
|
|
719
|
-
if (vt === "object" || vt === "array") return null;
|
|
720
|
-
if (vt === "new_expression" || vt === "call_expression") return null;
|
|
721
|
-
const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
|
|
722
|
-
return `VAR:${name} = ${collapseText(valText, 50)}`;
|
|
723
|
-
}
|
|
724
|
-
return null;
|
|
725
|
-
}
|
|
726
|
-
case "expression_statement": {
|
|
727
|
-
const expr = node.child(0);
|
|
728
|
-
if (!expr) return null;
|
|
729
|
-
if (expr.type === "await_expression") {
|
|
730
|
-
return null;
|
|
731
|
-
}
|
|
732
|
-
if (expr.type === "call_expression") {
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
default:
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
function walkNode(node, depth, lines) {
|
|
742
|
-
const tier = tierOf(node.type);
|
|
743
|
-
switch (tier) {
|
|
744
|
-
case "T1_KEEP": {
|
|
745
|
-
const ir = emitTier1(node);
|
|
746
|
-
if (ir) lines.push(ir);
|
|
747
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
748
|
-
const child = node.child(i);
|
|
749
|
-
const childType = child.type;
|
|
750
|
-
if (childType === "statement_block" || childType === "class_body") {
|
|
751
|
-
walkNode(child, depth + 1, lines);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
break;
|
|
755
|
-
}
|
|
756
|
-
case "T2_CONTROL": {
|
|
757
|
-
if (depth > 4 && node.type !== "return_statement" && node.type !== "throw_statement" && node.type !== "switch_case" && node.type !== "switch_default") break;
|
|
758
|
-
if (node.type === "if_statement") {
|
|
759
|
-
let hasElse = false;
|
|
760
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
761
|
-
if (node.child(i).type === "else_clause") {
|
|
762
|
-
hasElse = true;
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
if (!hasElse) {
|
|
767
|
-
const body = node.childForFieldName("consequence") ?? (() => {
|
|
768
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
769
|
-
const c = node.child(i);
|
|
770
|
-
if (c.type === "statement_block") return c;
|
|
771
|
-
}
|
|
772
|
-
return null;
|
|
773
|
-
})();
|
|
774
|
-
if (body) {
|
|
775
|
-
let singleStmt = null;
|
|
776
|
-
if (body.type === "statement_block") {
|
|
777
|
-
const stmts = [];
|
|
778
|
-
for (let i = 0; i < body.childCount; i++) {
|
|
779
|
-
const c = body.child(i);
|
|
780
|
-
if (c.type !== "{" && c.type !== "}") stmts.push(c);
|
|
781
|
-
}
|
|
782
|
-
if (stmts.length === 1) singleStmt = stmts[0];
|
|
783
|
-
} else if (body.type === "return_statement" || body.type === "throw_statement") {
|
|
784
|
-
singleStmt = body;
|
|
785
|
-
}
|
|
786
|
-
if (singleStmt && (singleStmt.type === "return_statement" || singleStmt.type === "throw_statement")) {
|
|
787
|
-
const cond = extractCondition(node);
|
|
788
|
-
const retLine = emitTier2(singleStmt);
|
|
789
|
-
if (retLine) {
|
|
790
|
-
const indent2 = " ".repeat(depth);
|
|
791
|
-
lines.push(`${indent2}IF:${cond} \u2192 ${retLine}`);
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
const line = emitTier2(node);
|
|
799
|
-
const indent = " ".repeat(depth);
|
|
800
|
-
if (line) lines.push(indent + line);
|
|
801
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
802
|
-
walkNode(node.child(i), depth + 1, lines);
|
|
803
|
-
}
|
|
804
|
-
break;
|
|
805
|
-
}
|
|
806
|
-
case "T3_COMPRESS": {
|
|
807
|
-
if (depth > 4) break;
|
|
808
|
-
const line = emitTier3(node);
|
|
809
|
-
const indent = " ".repeat(depth);
|
|
810
|
-
if (line) lines.push(indent + line);
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
case "WALK_ONLY": {
|
|
814
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
815
|
-
const child = node.child(i);
|
|
816
|
-
if (node.type === "export_statement") {
|
|
817
|
-
if (child.type === "export" || child.type === "default" || child.text === "export" || child.text === "default") {
|
|
818
|
-
if (child.childCount === 0 && (child.text === "export" || child.text === "default")) {
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
walkNode(child, depth + 1, lines);
|
|
824
|
-
}
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
case "T4_DROP":
|
|
828
|
-
default:
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
async function astWalkIR(code, filePath) {
|
|
833
|
-
const lang = detectLanguage(filePath);
|
|
834
|
-
if (!lang) return null;
|
|
835
|
-
const { parser } = await getParser(lang);
|
|
836
|
-
const tree = parser.parse(code);
|
|
837
|
-
const root = tree.rootNode;
|
|
838
|
-
const lines = [];
|
|
839
|
-
walkNode(root, 0, lines);
|
|
840
|
-
if (lines.length === 0) return null;
|
|
841
|
-
const merged = [];
|
|
842
|
-
let useBlock = [];
|
|
843
|
-
for (const line of lines) {
|
|
844
|
-
if (line.startsWith("USE:")) {
|
|
845
|
-
const m = line.match(/from\s+["']([^"']+)["']/);
|
|
846
|
-
useBlock.push(m ? m[1] : line.slice(4));
|
|
847
|
-
} else {
|
|
848
|
-
if (useBlock.length > 0) {
|
|
849
|
-
if (useBlock.length <= 3) {
|
|
850
|
-
for (const mod of useBlock) merged.push(`USE:${mod}`);
|
|
851
|
-
} else {
|
|
852
|
-
merged.push(`USE:[${useBlock.join(", ")}]`);
|
|
853
|
-
}
|
|
854
|
-
useBlock = [];
|
|
855
|
-
}
|
|
856
|
-
merged.push(line);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
if (useBlock.length > 0) {
|
|
860
|
-
if (useBlock.length <= 3) {
|
|
861
|
-
for (const mod of useBlock) merged.push(`USE:${mod}`);
|
|
862
|
-
} else {
|
|
863
|
-
merged.push(`USE:[${useBlock.join(", ")}]`);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return merged.join("\n");
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// src/ir/layers.ts
|
|
870
|
-
function generateL0(code, filePath) {
|
|
871
|
-
const structure = extractStructure(code);
|
|
872
|
-
const topLevel = structure.filter(
|
|
873
|
-
(s) => s.indent === 0 && ["function-start", "type-start", "export"].includes(s.type)
|
|
874
|
-
);
|
|
875
|
-
const declarations = topLevel.map((s) => {
|
|
876
|
-
const name = s.raw.match(
|
|
877
|
-
/(?:function|class|interface|const|let|var|export\s+(?:default\s+)?(?:function|class|async\s+function))\s+(\w+)/
|
|
878
|
-
)?.[1] ?? "unknown";
|
|
879
|
-
return ` ${s.type === "type-start" ? "CLASS" : "FN"}:${name} L${s.line}`;
|
|
880
|
-
});
|
|
881
|
-
return `${filePath}
|
|
882
|
-
${declarations.join("\n")}`;
|
|
883
|
-
}
|
|
884
|
-
async function generateL1(code, filePath, health) {
|
|
885
|
-
const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
|
|
886
|
-
if (health) {
|
|
887
|
-
return annotateIR(ir, health);
|
|
888
|
-
}
|
|
889
|
-
return ir;
|
|
890
|
-
}
|
|
891
|
-
function generateL2(delta, health) {
|
|
892
|
-
const parts = [`FILE: ${delta.file}`];
|
|
893
|
-
for (const hunk of delta.hunks) {
|
|
894
|
-
if (hunk.functionScope) parts.push(`SCOPE: ${hunk.functionScope}`);
|
|
895
|
-
parts.push(`CHANGED: ${hunk.changed.join("\n ")}`);
|
|
896
|
-
if (hunk.surroundingIR) parts.push(`CONTEXT: ${hunk.surroundingIR}`);
|
|
897
|
-
if (hunk.blame) {
|
|
898
|
-
parts.push(`BLAME: ${hunk.blame.author}, ${hunk.blame.date}, commit:"${hunk.blame.commitMessage}"`);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const ir = parts.join("\n");
|
|
902
|
-
if (health) return annotateIR(ir, health);
|
|
903
|
-
return ir;
|
|
904
|
-
}
|
|
905
|
-
function generateL3(code, startLine, endLine) {
|
|
906
|
-
const lines = code.split("\n");
|
|
907
|
-
return lines.slice(startLine - 1, endLine).join("\n");
|
|
908
|
-
}
|
|
909
|
-
async function generateLayer(layer, options) {
|
|
910
|
-
switch (layer) {
|
|
911
|
-
case "L0":
|
|
912
|
-
return generateL0(options.code, options.filePath);
|
|
913
|
-
case "L1":
|
|
914
|
-
return generateL1(options.code, options.filePath, options.health);
|
|
915
|
-
case "L2":
|
|
916
|
-
if (!options.delta) return generateL1(options.code, options.filePath, options.health);
|
|
917
|
-
return generateL2(options.delta, options.health);
|
|
918
|
-
case "L3":
|
|
919
|
-
if (options.lineRange) {
|
|
920
|
-
return generateL3(options.code, options.lineRange.start, options.lineRange.end);
|
|
921
|
-
}
|
|
922
|
-
return options.code;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// src/trends/git-log-parser.ts
|
|
927
|
-
import { execSync } from "child_process";
|
|
928
|
-
var BUG_FIX_PATTERNS = [
|
|
929
|
-
/\bfix\b/i,
|
|
930
|
-
/\bbugfix\b/i,
|
|
931
|
-
/\bhotfix\b/i,
|
|
932
|
-
/\bpatch\b/i,
|
|
933
|
-
/\bresolve\b/i,
|
|
934
|
-
/\bbug\b/i
|
|
935
|
-
];
|
|
936
|
-
function isBugFixCommit(message) {
|
|
937
|
-
return BUG_FIX_PATTERNS.some((p) => p.test(message));
|
|
938
|
-
}
|
|
939
|
-
function parseGitLogOutput(output) {
|
|
940
|
-
const entries = [];
|
|
941
|
-
const lines = output.split("\n");
|
|
942
|
-
let i = 0;
|
|
943
|
-
while (i < lines.length) {
|
|
944
|
-
const line = lines[i].trim();
|
|
945
|
-
if (!line || !line.includes("|")) {
|
|
946
|
-
i++;
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
const [hash, author, date, ...messageParts] = line.split("|");
|
|
950
|
-
const message = messageParts.join("|");
|
|
951
|
-
const files = [];
|
|
952
|
-
i++;
|
|
953
|
-
while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
|
|
954
|
-
const fileLine = lines[i].trim();
|
|
955
|
-
if (fileLine) files.push(fileLine);
|
|
956
|
-
i++;
|
|
957
|
-
}
|
|
958
|
-
entries.push({ hash, author, date, message, files });
|
|
959
|
-
}
|
|
960
|
-
return entries;
|
|
961
|
-
}
|
|
962
|
-
function getGitLog(repoPath, count = 100) {
|
|
963
|
-
try {
|
|
964
|
-
const output = execSync(
|
|
965
|
-
`git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
|
|
966
|
-
{ cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
|
|
967
|
-
);
|
|
968
|
-
return parseGitLogOutput(output);
|
|
969
|
-
} catch {
|
|
970
|
-
return [];
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// src/trends/hotspot.ts
|
|
975
|
-
function detectHotspots(entries, options) {
|
|
976
|
-
const fileStats = /* @__PURE__ */ new Map();
|
|
977
|
-
for (const entry of entries) {
|
|
978
|
-
const isFix = isBugFixCommit(entry.message);
|
|
979
|
-
for (const file of entry.files) {
|
|
980
|
-
const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
|
|
981
|
-
stats.changes++;
|
|
982
|
-
if (isFix) stats.fixes++;
|
|
983
|
-
stats.authors.add(entry.author);
|
|
984
|
-
fileStats.set(file, stats);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
const hotspots = [];
|
|
988
|
-
for (const [file, stats] of fileStats) {
|
|
989
|
-
const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
|
|
990
|
-
if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
|
|
991
|
-
hotspots.push({
|
|
992
|
-
file,
|
|
993
|
-
changesInLast30Commits: stats.changes,
|
|
994
|
-
bugFixRatio: fixRatio,
|
|
995
|
-
authorCount: stats.authors.size
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// src/trends/decay.ts
|
|
1003
|
-
function detectDecay(entries) {
|
|
1004
|
-
const fileChanges = /* @__PURE__ */ new Map();
|
|
1005
|
-
for (const entry of entries) {
|
|
1006
|
-
for (const file of entry.files) {
|
|
1007
|
-
const changes = fileChanges.get(file) ?? [];
|
|
1008
|
-
changes.push({ date: entry.date });
|
|
1009
|
-
fileChanges.set(file, changes);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
const signals = [];
|
|
1013
|
-
for (const [file, changes] of fileChanges) {
|
|
1014
|
-
if (changes.length < 4) continue;
|
|
1015
|
-
const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
|
|
1016
|
-
const firstDate = new Date(sorted[0].date).getTime();
|
|
1017
|
-
const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
|
|
1018
|
-
const midDate = firstDate + (lastDate - firstDate) / 2;
|
|
1019
|
-
const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
|
|
1020
|
-
const secondHalfCount = sorted.length - firstHalfCount;
|
|
1021
|
-
if (secondHalfCount > firstHalfCount) {
|
|
1022
|
-
signals.push({
|
|
1023
|
-
file,
|
|
1024
|
-
metric: "churn",
|
|
1025
|
-
trend: "declining",
|
|
1026
|
-
dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
return signals;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// src/trends/inconsistency.ts
|
|
1034
|
-
function detectInconsistencies(entries, minAuthors = 3) {
|
|
1035
|
-
const fileAuthors = /* @__PURE__ */ new Map();
|
|
1036
|
-
for (const entry of entries) {
|
|
1037
|
-
for (const file of entry.files) {
|
|
1038
|
-
const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
|
|
1039
|
-
const commits = authors.get(entry.author) ?? [];
|
|
1040
|
-
commits.push(entry.message);
|
|
1041
|
-
authors.set(entry.author, commits);
|
|
1042
|
-
fileAuthors.set(file, authors);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
const inconsistencies = [];
|
|
1046
|
-
for (const [file, authors] of fileAuthors) {
|
|
1047
|
-
if (authors.size >= minAuthors) {
|
|
1048
|
-
const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
|
|
1049
|
-
author,
|
|
1050
|
-
style: categorizeStyle(commits)
|
|
1051
|
-
}));
|
|
1052
|
-
inconsistencies.push({ file, patterns });
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
return inconsistencies;
|
|
1056
|
-
}
|
|
1057
|
-
function categorizeStyle(commits) {
|
|
1058
|
-
const types = commits.map((m) => {
|
|
1059
|
-
if (m.match(/^fix/i)) return "fix";
|
|
1060
|
-
if (m.match(/^feat/i)) return "feature";
|
|
1061
|
-
if (m.match(/^refactor/i)) return "refactor";
|
|
1062
|
-
return "other";
|
|
1063
|
-
});
|
|
1064
|
-
const primary = mode(types);
|
|
1065
|
-
return `primarily ${primary} (${commits.length} commits)`;
|
|
1066
|
-
}
|
|
1067
|
-
function mode(arr) {
|
|
1068
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1069
|
-
for (const item of arr) {
|
|
1070
|
-
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
1071
|
-
}
|
|
1072
|
-
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
1073
|
-
}
|
|
18
|
+
import { readFileSync, readdirSync } from "fs";
|
|
19
|
+
import { join, relative } from "path";
|
|
1074
20
|
|
|
1075
21
|
// src/router/router.ts
|
|
1076
|
-
import
|
|
22
|
+
import picomatch from "picomatch";
|
|
1077
23
|
var DEFAULT_ROUTES = [
|
|
1078
24
|
{ pattern: "**/auth/**", agents: ["reviewer"], irLayer: "L1" },
|
|
1079
25
|
{ pattern: "**/*.test.*", agents: ["reviewer"], irLayer: "L0" },
|
|
@@ -1090,7 +36,7 @@ var FALLBACK = {
|
|
|
1090
36
|
};
|
|
1091
37
|
function route(finding, rules) {
|
|
1092
38
|
for (const rule of rules) {
|
|
1093
|
-
if (!
|
|
39
|
+
if (!picomatch.isMatch(finding.file, rule.pattern)) continue;
|
|
1094
40
|
if (rule.contentSignal) {
|
|
1095
41
|
const content = finding.message + (finding.file ?? "");
|
|
1096
42
|
if (!rule.contentSignal.test(content)) continue;
|
|
@@ -1168,35 +114,6 @@ var CLIAdapter = class {
|
|
|
1168
114
|
}
|
|
1169
115
|
};
|
|
1170
116
|
|
|
1171
|
-
// src/benchmark/tokenizer.ts
|
|
1172
|
-
function estimateTokens(text) {
|
|
1173
|
-
if (!text) return 0;
|
|
1174
|
-
const tokens = text.split(/[\s]+|(?<=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])|(?=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])/).filter(Boolean);
|
|
1175
|
-
return tokens.length;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// src/benchmark/runner.ts
|
|
1179
|
-
async function benchmarkFile(code, filePath) {
|
|
1180
|
-
const rawTokens = estimateTokens(code);
|
|
1181
|
-
const irL0 = await generateLayer("L0", { code, filePath, health: null });
|
|
1182
|
-
const irL1 = await generateLayer("L1", { code, filePath, health: null });
|
|
1183
|
-
const irL0Tokens = estimateTokens(irL0);
|
|
1184
|
-
const irL1Tokens = estimateTokens(irL1);
|
|
1185
|
-
const astResult = await astWalkIR(code, filePath);
|
|
1186
|
-
const engine = astResult !== null ? "AST" : "FP";
|
|
1187
|
-
const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
|
|
1188
|
-
return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, engine };
|
|
1189
|
-
}
|
|
1190
|
-
function summarize(results) {
|
|
1191
|
-
const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
|
|
1192
|
-
const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
|
|
1193
|
-
const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
|
|
1194
|
-
const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
|
|
1195
|
-
const astCount = results.filter((r) => r.engine === "AST").length;
|
|
1196
|
-
const fpCount = results.filter((r) => r.engine === "FP").length;
|
|
1197
|
-
return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, astCount, fpCount };
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
117
|
// src/benchmark/quality.ts
|
|
1201
118
|
var BENCHMARK_PROMPTS = [
|
|
1202
119
|
{
|
|
@@ -1263,63 +180,13 @@ async function runQualityBenchmark(code, filePath, apiKey, promptId = "understan
|
|
|
1263
180
|
return { file: filePath, raw: rawResult, ir: irResult, savedPercent };
|
|
1264
181
|
}
|
|
1265
182
|
|
|
1266
|
-
// src/context/packer.ts
|
|
1267
|
-
async function packContext(files, options) {
|
|
1268
|
-
const { budget, hotspots } = options;
|
|
1269
|
-
const hotspotSet = new Set(hotspots.map((h) => h.file));
|
|
1270
|
-
const entries = [];
|
|
1271
|
-
let totalTokens = 0;
|
|
1272
|
-
for (const file of files) {
|
|
1273
|
-
const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
|
|
1274
|
-
const l0Tokens = estimateTokens(l0);
|
|
1275
|
-
entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
|
|
1276
|
-
totalTokens += l0Tokens;
|
|
1277
|
-
}
|
|
1278
|
-
if (totalTokens > budget) {
|
|
1279
|
-
const truncated = [];
|
|
1280
|
-
let used = 0;
|
|
1281
|
-
for (const entry of entries) {
|
|
1282
|
-
if (used + entry.tokens <= budget) {
|
|
1283
|
-
truncated.push(entry);
|
|
1284
|
-
used += entry.tokens;
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
return { entries: truncated, totalTokens: used, budget, filesAtL0: truncated.length, filesAtL1: 0 };
|
|
1288
|
-
}
|
|
1289
|
-
const upgradeOrder = entries.map((e, i) => ({ index: i, path: e.path, rawTokens: files[i].rawTokens, isHotspot: hotspotSet.has(e.path) })).sort((a, b) => {
|
|
1290
|
-
if (a.isHotspot && !b.isHotspot) return -1;
|
|
1291
|
-
if (!a.isHotspot && b.isHotspot) return 1;
|
|
1292
|
-
return b.rawTokens - a.rawTokens;
|
|
1293
|
-
});
|
|
1294
|
-
let filesAtL1 = 0;
|
|
1295
|
-
for (const item of upgradeOrder) {
|
|
1296
|
-
const file = files[item.index];
|
|
1297
|
-
const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
|
|
1298
|
-
const l1Tokens = estimateTokens(l1);
|
|
1299
|
-
const currentL0Tokens = entries[item.index].tokens;
|
|
1300
|
-
const additionalTokens = l1Tokens - currentL0Tokens;
|
|
1301
|
-
if (totalTokens + additionalTokens <= budget) {
|
|
1302
|
-
entries[item.index] = { path: item.path, layer: "L1", ir: l1, tokens: l1Tokens };
|
|
1303
|
-
totalTokens += additionalTokens;
|
|
1304
|
-
filesAtL1++;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
return {
|
|
1308
|
-
entries,
|
|
1309
|
-
totalTokens,
|
|
1310
|
-
budget,
|
|
1311
|
-
filesAtL0: entries.length - filesAtL1,
|
|
1312
|
-
filesAtL1
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
183
|
// src/cli/commands.ts
|
|
1317
184
|
function collectFiles(dir, extensions) {
|
|
1318
185
|
const files = [];
|
|
1319
186
|
try {
|
|
1320
187
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
1321
188
|
for (const entry of entries) {
|
|
1322
|
-
const fullPath =
|
|
189
|
+
const fullPath = join(dir, entry.name);
|
|
1323
190
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
|
|
1324
191
|
if (entry.isDirectory()) {
|
|
1325
192
|
files.push(...collectFiles(fullPath, extensions));
|
|
@@ -1340,7 +207,7 @@ function runScan(projectPath) {
|
|
|
1340
207
|
`);
|
|
1341
208
|
const allFindings = [];
|
|
1342
209
|
for (const file of files) {
|
|
1343
|
-
const code =
|
|
210
|
+
const code = readFileSync(file, "utf-8");
|
|
1344
211
|
const relPath = relative(projectPath, file);
|
|
1345
212
|
const findings = runDetector(code, relPath, config.watchers);
|
|
1346
213
|
allFindings.push(...findings);
|
|
@@ -1381,7 +248,7 @@ function runTrends(projectPath) {
|
|
|
1381
248
|
}
|
|
1382
249
|
async function runIR(projectPath, filePath, layer) {
|
|
1383
250
|
const config = loadConfig(projectPath);
|
|
1384
|
-
const code =
|
|
251
|
+
const code = readFileSync(filePath, "utf-8");
|
|
1385
252
|
const relPath = relative(projectPath, filePath);
|
|
1386
253
|
const entries = getGitLog(projectPath, 100);
|
|
1387
254
|
const trends = {
|
|
@@ -1409,7 +276,7 @@ async function runBenchmark(projectPath) {
|
|
|
1409
276
|
`);
|
|
1410
277
|
const results = [];
|
|
1411
278
|
for (const file of files) {
|
|
1412
|
-
const code =
|
|
279
|
+
const code = readFileSync(file, "utf-8");
|
|
1413
280
|
const relPath = relative(projectPath, file);
|
|
1414
281
|
results.push(await benchmarkFile(code, relPath));
|
|
1415
282
|
}
|
|
@@ -1448,7 +315,7 @@ async function runBenchmarkQuality(projectPath, filePath) {
|
|
|
1448
315
|
console.error(" Error: ANTHROPIC_API_KEY environment variable is required.");
|
|
1449
316
|
process.exit(1);
|
|
1450
317
|
}
|
|
1451
|
-
const code =
|
|
318
|
+
const code = readFileSync(filePath, "utf-8");
|
|
1452
319
|
const relPath = relative(projectPath, filePath);
|
|
1453
320
|
console.log("composto v0.1.0 \u2014 quality benchmark\n");
|
|
1454
321
|
console.log(` File: ${relPath}
|
|
@@ -1492,7 +359,7 @@ async function runContext(projectPath, budget) {
|
|
|
1492
359
|
fixRatioThreshold: config.trends.bugFixRatioThreshold
|
|
1493
360
|
});
|
|
1494
361
|
const fileInputs = files.map((file) => {
|
|
1495
|
-
const code =
|
|
362
|
+
const code = readFileSync(file, "utf-8");
|
|
1496
363
|
const relPath = relative(projectPath, file);
|
|
1497
364
|
return { path: relPath, code, rawTokens: estimateTokens(code) };
|
|
1498
365
|
});
|
|
@@ -1524,17 +391,17 @@ async function runContext(projectPath, budget) {
|
|
|
1524
391
|
}
|
|
1525
392
|
|
|
1526
393
|
// src/index.ts
|
|
1527
|
-
import { resolve
|
|
394
|
+
import { resolve } from "path";
|
|
1528
395
|
var args = process.argv.slice(2);
|
|
1529
396
|
var command = args[0];
|
|
1530
397
|
switch (command) {
|
|
1531
398
|
case "scan": {
|
|
1532
|
-
const projectPath =
|
|
399
|
+
const projectPath = resolve(args[1] ?? ".");
|
|
1533
400
|
runScan(projectPath);
|
|
1534
401
|
break;
|
|
1535
402
|
}
|
|
1536
403
|
case "trends": {
|
|
1537
|
-
const projectPath =
|
|
404
|
+
const projectPath = resolve(args[1] ?? ".");
|
|
1538
405
|
runTrends(projectPath);
|
|
1539
406
|
break;
|
|
1540
407
|
}
|
|
@@ -1545,11 +412,11 @@ switch (command) {
|
|
|
1545
412
|
console.error("Usage: composto ir <file> [L0|L1|L2|L3]");
|
|
1546
413
|
process.exit(1);
|
|
1547
414
|
}
|
|
1548
|
-
await runIR(
|
|
415
|
+
await runIR(resolve("."), resolve(filePath), layer);
|
|
1549
416
|
break;
|
|
1550
417
|
}
|
|
1551
418
|
case "benchmark": {
|
|
1552
|
-
const projectPath =
|
|
419
|
+
const projectPath = resolve(args[1] ?? ".");
|
|
1553
420
|
await runBenchmark(projectPath);
|
|
1554
421
|
break;
|
|
1555
422
|
}
|
|
@@ -1559,11 +426,11 @@ switch (command) {
|
|
|
1559
426
|
console.error("Usage: composto benchmark-quality <file>");
|
|
1560
427
|
process.exit(1);
|
|
1561
428
|
}
|
|
1562
|
-
await runBenchmarkQuality(
|
|
429
|
+
await runBenchmarkQuality(resolve("."), resolve(filePath));
|
|
1563
430
|
break;
|
|
1564
431
|
}
|
|
1565
432
|
case "context": {
|
|
1566
|
-
const projectPath =
|
|
433
|
+
const projectPath = resolve(args[1] ?? ".");
|
|
1567
434
|
const budgetFlag = args.indexOf("--budget");
|
|
1568
435
|
const budget = budgetFlag !== -1 && args[budgetFlag + 1] ? parseInt(args[budgetFlag + 1], 10) : 4e3;
|
|
1569
436
|
await runContext(projectPath, budget);
|