composto-ai 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/dist/index.js +2514 -54
- package/dist/mcp/server.js +2396 -40
- package/dist/memory/api.js +986 -0
- package/dist/memory/pool.js +57 -0
- package/dist/memory/worker.js +448 -0
- package/package.json +4 -1
- package/dist/chunk-AARGW2GV.js +0 -1531
package/dist/index.js
CHANGED
|
@@ -1,25 +1,1308 @@
|
|
|
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-AARGW2GV.js";
|
|
16
2
|
|
|
17
3
|
// src/cli/commands.ts
|
|
18
|
-
import { readFileSync
|
|
19
|
-
import {
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { relative as relative2, join as join6 } from "path";
|
|
20
6
|
|
|
21
|
-
// src/
|
|
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
|
|
22
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 (JS/TS)
|
|
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 1 — Python
|
|
514
|
+
function_definition: "T1_KEEP",
|
|
515
|
+
class_definition: "T1_KEEP",
|
|
516
|
+
import_from_statement: "T1_KEEP",
|
|
517
|
+
decorated_definition: "T1_KEEP",
|
|
518
|
+
// Tier 1 — class members (qualified methods only, fields dropped as noise)
|
|
519
|
+
method_definition: "T1_KEEP",
|
|
520
|
+
// JS/TS class method
|
|
521
|
+
// Tier 1 — Go
|
|
522
|
+
function_item: "T1_KEEP",
|
|
523
|
+
// Rust
|
|
524
|
+
method_declaration: "T1_KEEP",
|
|
525
|
+
// Go
|
|
526
|
+
type_declaration: "T1_KEEP",
|
|
527
|
+
// Go
|
|
528
|
+
import_declaration: "T1_KEEP",
|
|
529
|
+
// Go
|
|
530
|
+
use_declaration: "T1_KEEP",
|
|
531
|
+
// Rust
|
|
532
|
+
struct_item: "T1_KEEP",
|
|
533
|
+
// Rust
|
|
534
|
+
enum_item: "T1_KEEP",
|
|
535
|
+
// Rust
|
|
536
|
+
trait_item: "T1_KEEP",
|
|
537
|
+
// Rust
|
|
538
|
+
impl_item: "T1_KEEP",
|
|
539
|
+
// Rust
|
|
540
|
+
// Tier 2 — control flow (universal)
|
|
541
|
+
if_statement: "T2_CONTROL",
|
|
542
|
+
if_expression: "T2_CONTROL",
|
|
543
|
+
// Rust
|
|
544
|
+
else_clause: "WALK_ONLY",
|
|
545
|
+
elif_clause: "T2_CONTROL",
|
|
546
|
+
// Python
|
|
547
|
+
for_statement: "T2_CONTROL",
|
|
548
|
+
for_in_statement: "T2_CONTROL",
|
|
549
|
+
for_expression: "T2_CONTROL",
|
|
550
|
+
// Rust
|
|
551
|
+
while_statement: "T2_CONTROL",
|
|
552
|
+
do_statement: "T2_CONTROL",
|
|
553
|
+
switch_statement: "T2_CONTROL",
|
|
554
|
+
switch_case: "T2_CONTROL",
|
|
555
|
+
switch_default: "T2_CONTROL",
|
|
556
|
+
match_expression: "T2_CONTROL",
|
|
557
|
+
// Rust
|
|
558
|
+
return_statement: "T2_CONTROL",
|
|
559
|
+
return_expression: "T2_CONTROL",
|
|
560
|
+
// Rust
|
|
561
|
+
throw_statement: "T2_CONTROL",
|
|
562
|
+
raise_statement: "T2_CONTROL",
|
|
563
|
+
// Python
|
|
564
|
+
try_statement: "T2_CONTROL",
|
|
565
|
+
catch_clause: "T2_CONTROL",
|
|
566
|
+
except_clause: "T2_CONTROL",
|
|
567
|
+
// Python
|
|
568
|
+
with_statement: "T2_CONTROL",
|
|
569
|
+
// Python
|
|
570
|
+
defer_statement: "T2_CONTROL",
|
|
571
|
+
// Go
|
|
572
|
+
// Tier 3 — compressible expressions
|
|
573
|
+
lexical_declaration: "T3_COMPRESS",
|
|
574
|
+
expression_statement: "T3_COMPRESS",
|
|
575
|
+
assignment: "T3_COMPRESS",
|
|
576
|
+
// Python
|
|
577
|
+
short_var_declaration: "T3_COMPRESS",
|
|
578
|
+
// Go
|
|
579
|
+
// Walk-only — containers
|
|
580
|
+
program: "WALK_ONLY",
|
|
581
|
+
module: "WALK_ONLY",
|
|
582
|
+
// Python
|
|
583
|
+
statement_block: "WALK_ONLY",
|
|
584
|
+
block: "WALK_ONLY",
|
|
585
|
+
// Python/Go/Rust
|
|
586
|
+
class_body: "WALK_ONLY",
|
|
587
|
+
// JS/TS
|
|
588
|
+
switch_body: "WALK_ONLY",
|
|
589
|
+
export_statement: "WALK_ONLY",
|
|
590
|
+
source_file: "WALK_ONLY"
|
|
591
|
+
// Go/Rust
|
|
592
|
+
};
|
|
593
|
+
function tierOf(nodeType) {
|
|
594
|
+
return TIER_MAP[nodeType] ?? "T4_DROP";
|
|
595
|
+
}
|
|
596
|
+
function collapseText(text, maxLen) {
|
|
597
|
+
const collapsed = text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
598
|
+
if (collapsed.length <= maxLen) return collapsed;
|
|
599
|
+
return collapsed.slice(0, maxLen - 3) + "...";
|
|
600
|
+
}
|
|
601
|
+
function getTypeParams(node) {
|
|
602
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
603
|
+
const child = node.child(i);
|
|
604
|
+
if (child.type === "type_parameters") {
|
|
605
|
+
return child.text;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return "";
|
|
609
|
+
}
|
|
610
|
+
function isExported(node) {
|
|
611
|
+
return node.parent?.type === "export_statement";
|
|
612
|
+
}
|
|
613
|
+
function isAsync(node) {
|
|
614
|
+
return node.text.trimStart().startsWith("async");
|
|
615
|
+
}
|
|
616
|
+
function extractDocComment(node) {
|
|
617
|
+
const exported = node.parent?.type === "export_statement";
|
|
618
|
+
if (!exported) return null;
|
|
619
|
+
const prev = node.parent?.previousNamedSibling;
|
|
620
|
+
if (!prev || prev.type !== "comment") return null;
|
|
621
|
+
const text = prev.text;
|
|
622
|
+
if (!text.startsWith("/**")) return null;
|
|
623
|
+
const body = text.replace(/^\/\*\*|\*\/$/g, "").replace(/^\s*\*\s?/gm, "").trim();
|
|
624
|
+
const hasDeprecated = /@deprecated\b/.test(body);
|
|
625
|
+
if (hasDeprecated) return "@deprecated";
|
|
626
|
+
const beforeTags = body.split(/@\w+/)[0].trim();
|
|
627
|
+
const firstLine = beforeTags.split("\n")[0].trim();
|
|
628
|
+
if (!firstLine || firstLine.length < 5) return null;
|
|
629
|
+
return `"${firstLine.length > 30 ? firstLine.slice(0, 27) + "..." : firstLine}"`;
|
|
630
|
+
}
|
|
631
|
+
function extractPythonDocstring(bodyNode) {
|
|
632
|
+
if (!bodyNode) return null;
|
|
633
|
+
for (let i = 0; i < bodyNode.childCount; i++) {
|
|
634
|
+
const child = bodyNode.child(i);
|
|
635
|
+
if (child.type === "expression_statement" && child.childCount > 0) {
|
|
636
|
+
const expr = child.child(0);
|
|
637
|
+
if (expr.type === "string") {
|
|
638
|
+
const text = expr.text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
|
|
639
|
+
const firstLine = text.split("\n")[0].trim();
|
|
640
|
+
if (firstLine.length < 5) return null;
|
|
641
|
+
return `"${firstLine.length > 30 ? firstLine.slice(0, 27) + "..." : firstLine}"`;
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function extractCondition(node) {
|
|
649
|
+
const condNode = node.childForFieldName("condition") ?? (() => {
|
|
650
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
651
|
+
const c = node.child(i);
|
|
652
|
+
if (c.type === "parenthesized_expression") return c;
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
})();
|
|
656
|
+
if (!condNode) return "...";
|
|
657
|
+
const text = condNode.text.replace(/^\(/, "").replace(/\)$/, "").trim();
|
|
658
|
+
return text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
659
|
+
}
|
|
660
|
+
function emitTier2(node) {
|
|
661
|
+
switch (node.type) {
|
|
662
|
+
case "if_statement": {
|
|
663
|
+
const cond = extractCondition(node);
|
|
664
|
+
return `IF:${cond}`;
|
|
665
|
+
}
|
|
666
|
+
case "else_clause":
|
|
667
|
+
return "ELSE:";
|
|
668
|
+
case "for_statement":
|
|
669
|
+
case "for_in_statement":
|
|
670
|
+
return "LOOP";
|
|
671
|
+
case "while_statement": {
|
|
672
|
+
const cond = extractCondition(node);
|
|
673
|
+
return `WHILE:${cond}`;
|
|
674
|
+
}
|
|
675
|
+
case "do_statement": {
|
|
676
|
+
const cond = extractCondition(node);
|
|
677
|
+
return `WHILE:${cond}`;
|
|
678
|
+
}
|
|
679
|
+
case "switch_statement": {
|
|
680
|
+
const expr = node.childForFieldName("value") ?? node.childForFieldName("condition") ?? (() => {
|
|
681
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
682
|
+
const c = node.child(i);
|
|
683
|
+
if (c.type === "parenthesized_expression") return c;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
})();
|
|
687
|
+
const text = expr ? expr.text.replace(/^\(/, "").replace(/\)$/, "").trim() : "...";
|
|
688
|
+
return `SWITCH:${text.length > 60 ? text.slice(0, 57) + "..." : text}`;
|
|
689
|
+
}
|
|
690
|
+
case "switch_case": {
|
|
691
|
+
let value = null;
|
|
692
|
+
const valNode = node.childForFieldName("value");
|
|
693
|
+
if (valNode) {
|
|
694
|
+
value = valNode.text;
|
|
695
|
+
} else {
|
|
696
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
697
|
+
const c = node.child(i);
|
|
698
|
+
if (c.type !== "case" && c.type !== ":" && c.childCount === 0 && c.text === "case") continue;
|
|
699
|
+
if (c.type !== "case" && c.text !== "case" && c.text !== ":") {
|
|
700
|
+
value = c.text;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return `CASE:${value ?? "..."}`;
|
|
706
|
+
}
|
|
707
|
+
case "switch_default":
|
|
708
|
+
return "DEFAULT:";
|
|
709
|
+
case "return_statement": {
|
|
710
|
+
let retText = "";
|
|
711
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
712
|
+
const c = node.child(i);
|
|
713
|
+
if (c.text !== "return" && c.text !== ";") {
|
|
714
|
+
retText += (retText ? " " : "") + c.text;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
retText = retText.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
718
|
+
if (!retText) return "RET";
|
|
719
|
+
return `RET ${retText.length > 60 ? retText.slice(0, 57) + "..." : retText}`;
|
|
720
|
+
}
|
|
721
|
+
case "throw_statement": {
|
|
722
|
+
let throwText = "";
|
|
723
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
724
|
+
const c = node.child(i);
|
|
725
|
+
if (c.text !== "throw" && c.text !== ";") {
|
|
726
|
+
throwText += (throwText ? " " : "") + c.text;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
throwText = throwText.trim();
|
|
730
|
+
return `THROW:${throwText.length > 60 ? throwText.slice(0, 57) + "..." : throwText}`;
|
|
731
|
+
}
|
|
732
|
+
case "try_statement":
|
|
733
|
+
return "TRY";
|
|
734
|
+
case "catch_clause": {
|
|
735
|
+
const param = node.childForFieldName("parameter");
|
|
736
|
+
const paramText = param ? param.text : "...";
|
|
737
|
+
return `CATCH:${paramText}`;
|
|
738
|
+
}
|
|
739
|
+
// Python
|
|
740
|
+
case "raise_statement": {
|
|
741
|
+
const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
|
|
742
|
+
return `RAISE:${val.length > 50 ? val.slice(0, 47) + "..." : val}`;
|
|
743
|
+
}
|
|
744
|
+
case "except_clause":
|
|
745
|
+
return "EXCEPT";
|
|
746
|
+
case "elif_clause": {
|
|
747
|
+
const cond = extractCondition(node);
|
|
748
|
+
return `ELIF:${cond}`;
|
|
749
|
+
}
|
|
750
|
+
case "with_statement":
|
|
751
|
+
return "WITH";
|
|
752
|
+
// Rust
|
|
753
|
+
case "if_expression": {
|
|
754
|
+
const cond = extractCondition(node);
|
|
755
|
+
return `IF:${cond}`;
|
|
756
|
+
}
|
|
757
|
+
case "for_expression":
|
|
758
|
+
return "LOOP";
|
|
759
|
+
case "match_expression":
|
|
760
|
+
return "MATCH";
|
|
761
|
+
case "return_expression": {
|
|
762
|
+
const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
|
|
763
|
+
return `RET ${val.length > 60 ? val.slice(0, 57) + "..." : val}`.trimEnd();
|
|
764
|
+
}
|
|
765
|
+
// Go
|
|
766
|
+
case "defer_statement":
|
|
767
|
+
return "DEFER";
|
|
768
|
+
default:
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function emitTier1(node) {
|
|
773
|
+
const exported = isExported(node);
|
|
774
|
+
const outPrefix = exported ? "OUT " : "";
|
|
775
|
+
switch (node.type) {
|
|
776
|
+
case "import_statement": {
|
|
777
|
+
const text = collapseText(node.text, 80);
|
|
778
|
+
return `USE:${text}`;
|
|
779
|
+
}
|
|
780
|
+
case "function_declaration": {
|
|
781
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
782
|
+
const rawParams = node.childForFieldName("parameters")?.text ?? "()";
|
|
783
|
+
const params = collapseText(rawParams, 60);
|
|
784
|
+
const asyncPrefix = isAsync(node) ? "ASYNC " : "";
|
|
785
|
+
const doc = extractDocComment(node);
|
|
786
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
787
|
+
return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
|
|
788
|
+
}
|
|
789
|
+
case "class_declaration": {
|
|
790
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
791
|
+
const typeParams = getTypeParams(node);
|
|
792
|
+
const doc = extractDocComment(node);
|
|
793
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
794
|
+
return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
|
|
795
|
+
}
|
|
796
|
+
case "interface_declaration": {
|
|
797
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
798
|
+
const typeParams = getTypeParams(node);
|
|
799
|
+
const doc = extractDocComment(node);
|
|
800
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
801
|
+
return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
|
|
802
|
+
}
|
|
803
|
+
case "type_alias_declaration": {
|
|
804
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
805
|
+
const doc = extractDocComment(node);
|
|
806
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
807
|
+
return `${docPrefix}${outPrefix}TYPE:${name}`;
|
|
808
|
+
}
|
|
809
|
+
case "enum_declaration": {
|
|
810
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
811
|
+
const doc = extractDocComment(node);
|
|
812
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
813
|
+
return `${docPrefix}${outPrefix}ENUM:${name}`;
|
|
814
|
+
}
|
|
815
|
+
case "method_definition": {
|
|
816
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
817
|
+
if (name.startsWith("#") || name.startsWith("_")) return null;
|
|
818
|
+
let enclosingClass = null;
|
|
819
|
+
let parent = node.parent;
|
|
820
|
+
while (parent) {
|
|
821
|
+
if (parent.type === "class_declaration" || parent.type === "class_definition") {
|
|
822
|
+
enclosingClass = parent.childForFieldName("name")?.text ?? null;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
parent = parent.parent;
|
|
826
|
+
}
|
|
827
|
+
const params = node.childForFieldName("parameters")?.text ?? "()";
|
|
828
|
+
const asyncPrefix = isAsync(node) ? "ASYNC " : "";
|
|
829
|
+
const qualifiedName = enclosingClass ? `${enclosingClass}.${name}` : name;
|
|
830
|
+
return `${asyncPrefix}METHOD:${qualifiedName}${collapseText(params, 40)}`;
|
|
831
|
+
}
|
|
832
|
+
// Python
|
|
833
|
+
case "function_definition": {
|
|
834
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
835
|
+
const params = node.childForFieldName("parameters")?.text ?? "()";
|
|
836
|
+
const returnType = node.childForFieldName("return_type")?.text ?? "";
|
|
837
|
+
const rt = returnType ? ` -> ${returnType}` : "";
|
|
838
|
+
const body = node.childForFieldName("body");
|
|
839
|
+
const doc = extractPythonDocstring(body);
|
|
840
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
841
|
+
return `${docPrefix}FN:${name}${collapseText(params, 60)}${rt}`;
|
|
842
|
+
}
|
|
843
|
+
case "class_definition": {
|
|
844
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
845
|
+
const superclass = node.childForFieldName("superclasses")?.text ?? "";
|
|
846
|
+
const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
|
|
847
|
+
const body = node.childForFieldName("body");
|
|
848
|
+
const doc = extractPythonDocstring(body);
|
|
849
|
+
const docPrefix = doc ? `${doc} ` : "";
|
|
850
|
+
return `${docPrefix}CLASS:${name}${sc}`;
|
|
851
|
+
}
|
|
852
|
+
case "import_from_statement": {
|
|
853
|
+
return `USE:${collapseText(node.text, 80)}`;
|
|
854
|
+
}
|
|
855
|
+
case "decorated_definition": {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
// Go
|
|
859
|
+
case "method_declaration":
|
|
860
|
+
case "type_declaration":
|
|
861
|
+
case "import_declaration": {
|
|
862
|
+
return `${collapseText(node.text, 80)}`;
|
|
863
|
+
}
|
|
864
|
+
// Rust
|
|
865
|
+
case "function_item": {
|
|
866
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
867
|
+
const params = node.childForFieldName("parameters")?.text ?? "()";
|
|
868
|
+
return `FN:${name}${collapseText(params, 60)}`;
|
|
869
|
+
}
|
|
870
|
+
case "struct_item":
|
|
871
|
+
case "enum_item":
|
|
872
|
+
case "trait_item":
|
|
873
|
+
case "impl_item":
|
|
874
|
+
case "use_declaration": {
|
|
875
|
+
const firstLine = node.text.split("\n")[0];
|
|
876
|
+
return collapseText(firstLine, 80);
|
|
877
|
+
}
|
|
878
|
+
default:
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function emitTier3(node) {
|
|
883
|
+
switch (node.type) {
|
|
884
|
+
case "lexical_declaration": {
|
|
885
|
+
let declarator = null;
|
|
886
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
887
|
+
const c = node.child(i);
|
|
888
|
+
if (c.type === "variable_declarator") {
|
|
889
|
+
declarator = c;
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!declarator) return null;
|
|
894
|
+
const name = declarator.childForFieldName("name")?.text ?? "?";
|
|
895
|
+
const value = declarator.childForFieldName("value");
|
|
896
|
+
if (value) {
|
|
897
|
+
if (value.type === "arrow_function") {
|
|
898
|
+
const asyncPrefix = isAsync(value) ? "ASYNC " : "";
|
|
899
|
+
const params = value.childForFieldName("parameters")?.text ?? "()";
|
|
900
|
+
return `${asyncPrefix}FN:${name}${collapseText(params, 60)} => ...`;
|
|
901
|
+
}
|
|
902
|
+
if (value.type === "await_expression") {
|
|
903
|
+
const callee = value.childCount > 1 ? value.child(1).text : "...";
|
|
904
|
+
return `AWAIT:${name}=${collapseText(callee, 40)}`;
|
|
905
|
+
}
|
|
906
|
+
if (node.parent?.type === "statement_block") return null;
|
|
907
|
+
const vt = value.type;
|
|
908
|
+
if (vt === "number" || vt === "true" || vt === "false") return null;
|
|
909
|
+
if (vt === "object" || vt === "array") return null;
|
|
910
|
+
if (vt === "new_expression" || vt === "call_expression") return null;
|
|
911
|
+
const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
|
|
912
|
+
return `VAR:${name} = ${collapseText(valText, 50)}`;
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
case "expression_statement": {
|
|
917
|
+
const expr = node.child(0);
|
|
918
|
+
if (!expr) return null;
|
|
919
|
+
if (expr.type === "await_expression") {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
if (expr.type === "call_expression") {
|
|
923
|
+
const callee = expr.child(0)?.text ?? "";
|
|
924
|
+
if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
|
|
925
|
+
const args2 = expr.child(1);
|
|
926
|
+
if (args2 && args2.childCount >= 4) {
|
|
927
|
+
const child = args2.child(1)?.text ?? "?";
|
|
928
|
+
const parent = args2.child(3)?.text ?? "?";
|
|
929
|
+
const shortChild = child.length > 30 ? child.slice(0, 27) + "..." : child;
|
|
930
|
+
const shortParent = parent.length > 30 ? parent.slice(0, 27) + "..." : parent;
|
|
931
|
+
return `EXTENDS:${shortChild} < ${shortParent}`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
default:
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function walkNode(node, depth, lines) {
|
|
943
|
+
const tier = tierOf(node.type);
|
|
944
|
+
switch (tier) {
|
|
945
|
+
case "T1_KEEP": {
|
|
946
|
+
const ir = emitTier1(node);
|
|
947
|
+
if (ir) lines.push(ir);
|
|
948
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
949
|
+
const child = node.child(i);
|
|
950
|
+
const childType = child.type;
|
|
951
|
+
if (childType === "statement_block" || childType === "class_body" || childType === "block" || // Python/Go/Rust
|
|
952
|
+
childType === "body" || // Python class/function body
|
|
953
|
+
childType === "declaration_list") {
|
|
954
|
+
walkNode(child, depth + 1, lines);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
case "T2_CONTROL": {
|
|
960
|
+
if (depth > 4 && node.type !== "return_statement" && node.type !== "throw_statement" && node.type !== "switch_case" && node.type !== "switch_default") break;
|
|
961
|
+
if (node.type === "if_statement") {
|
|
962
|
+
let hasElse = false;
|
|
963
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
964
|
+
if (node.child(i).type === "else_clause") {
|
|
965
|
+
hasElse = true;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (!hasElse) {
|
|
970
|
+
const body = node.childForFieldName("consequence") ?? (() => {
|
|
971
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
972
|
+
const c = node.child(i);
|
|
973
|
+
if (c.type === "statement_block") return c;
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
})();
|
|
977
|
+
if (body) {
|
|
978
|
+
let singleStmt = null;
|
|
979
|
+
if (body.type === "statement_block") {
|
|
980
|
+
const stmts = [];
|
|
981
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
982
|
+
const c = body.child(i);
|
|
983
|
+
if (c.type !== "{" && c.type !== "}") stmts.push(c);
|
|
984
|
+
}
|
|
985
|
+
if (stmts.length === 1) singleStmt = stmts[0];
|
|
986
|
+
} else if (body.type === "return_statement" || body.type === "throw_statement") {
|
|
987
|
+
singleStmt = body;
|
|
988
|
+
}
|
|
989
|
+
if (singleStmt && (singleStmt.type === "return_statement" || singleStmt.type === "throw_statement")) {
|
|
990
|
+
const cond = extractCondition(node);
|
|
991
|
+
const retLine = emitTier2(singleStmt);
|
|
992
|
+
if (retLine) {
|
|
993
|
+
const indent2 = " ".repeat(depth);
|
|
994
|
+
lines.push(`${indent2}IF:${cond} \u2192 ${retLine}`);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const line = emitTier2(node);
|
|
1002
|
+
const indent = " ".repeat(depth);
|
|
1003
|
+
if (line) lines.push(indent + line);
|
|
1004
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1005
|
+
walkNode(node.child(i), depth + 1, lines);
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
case "T3_COMPRESS": {
|
|
1010
|
+
if (depth > 4) break;
|
|
1011
|
+
const line = emitTier3(node);
|
|
1012
|
+
const indent = " ".repeat(depth);
|
|
1013
|
+
if (line) lines.push(indent + line);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
case "WALK_ONLY": {
|
|
1017
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1018
|
+
const child = node.child(i);
|
|
1019
|
+
if (node.type === "export_statement") {
|
|
1020
|
+
if (child.type === "export" || child.type === "default" || child.text === "export" || child.text === "default") {
|
|
1021
|
+
if (child.childCount === 0 && (child.text === "export" || child.text === "default")) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
walkNode(child, depth + 1, lines);
|
|
1027
|
+
}
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case "T4_DROP":
|
|
1031
|
+
default:
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async function astWalkIR(code, filePath) {
|
|
1036
|
+
const lang = detectLanguage(filePath);
|
|
1037
|
+
if (!lang) return null;
|
|
1038
|
+
const { parser } = await getParser(lang);
|
|
1039
|
+
const tree = parser.parse(code);
|
|
1040
|
+
const root = tree.rootNode;
|
|
1041
|
+
const lines = [];
|
|
1042
|
+
walkNode(root, 0, lines);
|
|
1043
|
+
if (lines.length === 0) return null;
|
|
1044
|
+
const pass1 = [];
|
|
1045
|
+
let useBlock = [];
|
|
1046
|
+
for (const line of lines) {
|
|
1047
|
+
if (line.startsWith("USE:")) {
|
|
1048
|
+
const m = line.match(/from\s+["']([^"']+)["']/);
|
|
1049
|
+
useBlock.push(m ? m[1] : line.slice(4));
|
|
1050
|
+
} else {
|
|
1051
|
+
if (useBlock.length > 0) {
|
|
1052
|
+
if (useBlock.length <= 3) {
|
|
1053
|
+
for (const mod of useBlock) pass1.push(`USE:${mod}`);
|
|
1054
|
+
} else {
|
|
1055
|
+
pass1.push(`USE:[${useBlock.join(", ")}]`);
|
|
1056
|
+
}
|
|
1057
|
+
useBlock = [];
|
|
1058
|
+
}
|
|
1059
|
+
pass1.push(line);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (useBlock.length > 0) {
|
|
1063
|
+
if (useBlock.length <= 3) {
|
|
1064
|
+
for (const mod of useBlock) pass1.push(`USE:${mod}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
pass1.push(`USE:[${useBlock.join(", ")}]`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const merged = [];
|
|
1070
|
+
let guardBlock = [];
|
|
1071
|
+
for (const line of pass1) {
|
|
1072
|
+
const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
|
|
1073
|
+
if (guardMatch) {
|
|
1074
|
+
guardBlock.push(guardMatch[2].trim());
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (guardBlock.length > 0) {
|
|
1078
|
+
if (guardBlock.length < 3) {
|
|
1079
|
+
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1080
|
+
} else {
|
|
1081
|
+
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
1082
|
+
}
|
|
1083
|
+
guardBlock = [];
|
|
1084
|
+
}
|
|
1085
|
+
merged.push(line);
|
|
1086
|
+
}
|
|
1087
|
+
if (guardBlock.length > 0) {
|
|
1088
|
+
if (guardBlock.length < 3) {
|
|
1089
|
+
for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
|
|
1090
|
+
} else {
|
|
1091
|
+
merged.push(` GUARD:[${guardBlock.join(", ")}]`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return merged.join("\n");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/ir/layers.ts
|
|
1098
|
+
function generateL0(code, filePath) {
|
|
1099
|
+
const structure = extractStructure(code);
|
|
1100
|
+
const topLevel = structure.filter(
|
|
1101
|
+
(s) => s.indent === 0 && ["function-start", "type-start", "export"].includes(s.type)
|
|
1102
|
+
);
|
|
1103
|
+
const declarations = topLevel.map((s) => {
|
|
1104
|
+
const name = s.raw.match(
|
|
1105
|
+
/(?:function|class|interface|const|let|var|export\s+(?:default\s+)?(?:function|class|async\s+function))\s+(\w+)/
|
|
1106
|
+
)?.[1] ?? "unknown";
|
|
1107
|
+
return ` ${s.type === "type-start" ? "CLASS" : "FN"}:${name} L${s.line}`;
|
|
1108
|
+
});
|
|
1109
|
+
return `${filePath}
|
|
1110
|
+
${declarations.join("\n")}`;
|
|
1111
|
+
}
|
|
1112
|
+
async function generateL1(code, filePath, health) {
|
|
1113
|
+
const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
|
|
1114
|
+
const result = ir.length < code.length ? ir : code;
|
|
1115
|
+
if (health) {
|
|
1116
|
+
return annotateIR(result, health);
|
|
1117
|
+
}
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1120
|
+
function generateL2(delta, health) {
|
|
1121
|
+
const parts = [`FILE: ${delta.file}`];
|
|
1122
|
+
for (const hunk of delta.hunks) {
|
|
1123
|
+
if (hunk.functionScope) parts.push(`SCOPE: ${hunk.functionScope}`);
|
|
1124
|
+
parts.push(`CHANGED: ${hunk.changed.join("\n ")}`);
|
|
1125
|
+
if (hunk.surroundingIR) parts.push(`CONTEXT: ${hunk.surroundingIR}`);
|
|
1126
|
+
if (hunk.blame) {
|
|
1127
|
+
parts.push(`BLAME: ${hunk.blame.author}, ${hunk.blame.date}, commit:"${hunk.blame.commitMessage}"`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const ir = parts.join("\n");
|
|
1131
|
+
if (health) return annotateIR(ir, health);
|
|
1132
|
+
return ir;
|
|
1133
|
+
}
|
|
1134
|
+
function generateL3(code, startLine, endLine) {
|
|
1135
|
+
const lines = code.split("\n");
|
|
1136
|
+
return lines.slice(startLine - 1, endLine).join("\n");
|
|
1137
|
+
}
|
|
1138
|
+
async function generateLayer(layer, options) {
|
|
1139
|
+
switch (layer) {
|
|
1140
|
+
case "L0":
|
|
1141
|
+
return generateL0(options.code, options.filePath);
|
|
1142
|
+
case "L1":
|
|
1143
|
+
return generateL1(options.code, options.filePath, options.health);
|
|
1144
|
+
case "L2":
|
|
1145
|
+
if (!options.delta) return generateL1(options.code, options.filePath, options.health);
|
|
1146
|
+
return generateL2(options.delta, options.health);
|
|
1147
|
+
case "L3":
|
|
1148
|
+
if (options.lineRange) {
|
|
1149
|
+
return generateL3(options.code, options.lineRange.start, options.lineRange.end);
|
|
1150
|
+
}
|
|
1151
|
+
return options.code;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/trends/git-log-parser.ts
|
|
1156
|
+
import { execSync } from "child_process";
|
|
1157
|
+
var BUG_FIX_PATTERNS = [
|
|
1158
|
+
/\bfix\b/i,
|
|
1159
|
+
/\bbugfix\b/i,
|
|
1160
|
+
/\bhotfix\b/i,
|
|
1161
|
+
/\bpatch\b/i,
|
|
1162
|
+
/\bresolve\b/i,
|
|
1163
|
+
/\bbug\b/i
|
|
1164
|
+
];
|
|
1165
|
+
function isBugFixCommit(message) {
|
|
1166
|
+
return BUG_FIX_PATTERNS.some((p) => p.test(message));
|
|
1167
|
+
}
|
|
1168
|
+
function parseGitLogOutput(output) {
|
|
1169
|
+
const entries = [];
|
|
1170
|
+
const lines = output.split("\n");
|
|
1171
|
+
let i = 0;
|
|
1172
|
+
while (i < lines.length) {
|
|
1173
|
+
const line = lines[i].trim();
|
|
1174
|
+
if (!line || !line.includes("|")) {
|
|
1175
|
+
i++;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
const [hash, author, date, ...messageParts] = line.split("|");
|
|
1179
|
+
const message = messageParts.join("|");
|
|
1180
|
+
const files = [];
|
|
1181
|
+
i++;
|
|
1182
|
+
while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
|
|
1183
|
+
const fileLine = lines[i].trim();
|
|
1184
|
+
if (fileLine) files.push(fileLine);
|
|
1185
|
+
i++;
|
|
1186
|
+
}
|
|
1187
|
+
entries.push({ hash, author, date, message, files });
|
|
1188
|
+
}
|
|
1189
|
+
return entries;
|
|
1190
|
+
}
|
|
1191
|
+
function getGitLog(repoPath, count = 100) {
|
|
1192
|
+
try {
|
|
1193
|
+
const output = execSync(
|
|
1194
|
+
`git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
|
|
1195
|
+
{ cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
|
|
1196
|
+
);
|
|
1197
|
+
return parseGitLogOutput(output);
|
|
1198
|
+
} catch {
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/trends/hotspot.ts
|
|
1204
|
+
function detectHotspots(entries, options) {
|
|
1205
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const entry of entries) {
|
|
1207
|
+
const isFix = isBugFixCommit(entry.message);
|
|
1208
|
+
for (const file of entry.files) {
|
|
1209
|
+
const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
|
|
1210
|
+
stats.changes++;
|
|
1211
|
+
if (isFix) stats.fixes++;
|
|
1212
|
+
stats.authors.add(entry.author);
|
|
1213
|
+
fileStats.set(file, stats);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const hotspots = [];
|
|
1217
|
+
for (const [file, stats] of fileStats) {
|
|
1218
|
+
const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
|
|
1219
|
+
if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
|
|
1220
|
+
hotspots.push({
|
|
1221
|
+
file,
|
|
1222
|
+
changesInLast30Commits: stats.changes,
|
|
1223
|
+
bugFixRatio: fixRatio,
|
|
1224
|
+
authorCount: stats.authors.size
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/trends/decay.ts
|
|
1232
|
+
function detectDecay(entries) {
|
|
1233
|
+
const fileChanges = /* @__PURE__ */ new Map();
|
|
1234
|
+
for (const entry of entries) {
|
|
1235
|
+
for (const file of entry.files) {
|
|
1236
|
+
const changes = fileChanges.get(file) ?? [];
|
|
1237
|
+
changes.push({ date: entry.date });
|
|
1238
|
+
fileChanges.set(file, changes);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const signals = [];
|
|
1242
|
+
for (const [file, changes] of fileChanges) {
|
|
1243
|
+
if (changes.length < 4) continue;
|
|
1244
|
+
const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
|
|
1245
|
+
const firstDate = new Date(sorted[0].date).getTime();
|
|
1246
|
+
const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
|
|
1247
|
+
const midDate = firstDate + (lastDate - firstDate) / 2;
|
|
1248
|
+
const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
|
|
1249
|
+
const secondHalfCount = sorted.length - firstHalfCount;
|
|
1250
|
+
if (secondHalfCount > firstHalfCount) {
|
|
1251
|
+
signals.push({
|
|
1252
|
+
file,
|
|
1253
|
+
metric: "churn",
|
|
1254
|
+
trend: "declining",
|
|
1255
|
+
dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return signals;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// src/trends/inconsistency.ts
|
|
1263
|
+
function detectInconsistencies(entries, minAuthors = 3) {
|
|
1264
|
+
const fileAuthors = /* @__PURE__ */ new Map();
|
|
1265
|
+
for (const entry of entries) {
|
|
1266
|
+
for (const file of entry.files) {
|
|
1267
|
+
const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
|
|
1268
|
+
const commits = authors.get(entry.author) ?? [];
|
|
1269
|
+
commits.push(entry.message);
|
|
1270
|
+
authors.set(entry.author, commits);
|
|
1271
|
+
fileAuthors.set(file, authors);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const inconsistencies = [];
|
|
1275
|
+
for (const [file, authors] of fileAuthors) {
|
|
1276
|
+
if (authors.size >= minAuthors) {
|
|
1277
|
+
const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
|
|
1278
|
+
author,
|
|
1279
|
+
style: categorizeStyle(commits)
|
|
1280
|
+
}));
|
|
1281
|
+
inconsistencies.push({ file, patterns });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return inconsistencies;
|
|
1285
|
+
}
|
|
1286
|
+
function categorizeStyle(commits) {
|
|
1287
|
+
const types = commits.map((m) => {
|
|
1288
|
+
if (m.match(/^fix/i)) return "fix";
|
|
1289
|
+
if (m.match(/^feat/i)) return "feature";
|
|
1290
|
+
if (m.match(/^refactor/i)) return "refactor";
|
|
1291
|
+
return "other";
|
|
1292
|
+
});
|
|
1293
|
+
const primary = mode(types);
|
|
1294
|
+
return `primarily ${primary} (${commits.length} commits)`;
|
|
1295
|
+
}
|
|
1296
|
+
function mode(arr) {
|
|
1297
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1298
|
+
for (const item of arr) {
|
|
1299
|
+
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
1300
|
+
}
|
|
1301
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/router/router.ts
|
|
1305
|
+
import picomatch2 from "picomatch";
|
|
23
1306
|
var DEFAULT_ROUTES = [
|
|
24
1307
|
{ pattern: "**/auth/**", agents: ["reviewer"], irLayer: "L1" },
|
|
25
1308
|
{ pattern: "**/*.test.*", agents: ["reviewer"], irLayer: "L0" },
|
|
@@ -36,7 +1319,7 @@ var FALLBACK = {
|
|
|
36
1319
|
};
|
|
37
1320
|
function route(finding, rules) {
|
|
38
1321
|
for (const rule of rules) {
|
|
39
|
-
if (!
|
|
1322
|
+
if (!picomatch2.isMatch(finding.file, rule.pattern)) continue;
|
|
40
1323
|
if (rule.contentSignal) {
|
|
41
1324
|
const content = finding.message + (finding.file ?? "");
|
|
42
1325
|
if (!rule.contentSignal.test(content)) continue;
|
|
@@ -114,6 +1397,35 @@ var CLIAdapter = class {
|
|
|
114
1397
|
}
|
|
115
1398
|
};
|
|
116
1399
|
|
|
1400
|
+
// src/benchmark/tokenizer.ts
|
|
1401
|
+
function estimateTokens(text) {
|
|
1402
|
+
if (!text) return 0;
|
|
1403
|
+
const tokens = text.split(/[\s]+|(?<=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])|(?=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])/).filter(Boolean);
|
|
1404
|
+
return tokens.length;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/benchmark/runner.ts
|
|
1408
|
+
async function benchmarkFile(code, filePath) {
|
|
1409
|
+
const rawTokens = estimateTokens(code);
|
|
1410
|
+
const irL0 = await generateLayer("L0", { code, filePath, health: null });
|
|
1411
|
+
const irL1 = await generateLayer("L1", { code, filePath, health: null });
|
|
1412
|
+
const irL0Tokens = estimateTokens(irL0);
|
|
1413
|
+
const irL1Tokens = estimateTokens(irL1);
|
|
1414
|
+
const astResult = await astWalkIR(code, filePath);
|
|
1415
|
+
const engine = astResult !== null ? "AST" : "FP";
|
|
1416
|
+
const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
|
|
1417
|
+
return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, engine };
|
|
1418
|
+
}
|
|
1419
|
+
function summarize(results) {
|
|
1420
|
+
const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
|
|
1421
|
+
const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
|
|
1422
|
+
const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
|
|
1423
|
+
const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
|
|
1424
|
+
const astCount = results.filter((r) => r.engine === "AST").length;
|
|
1425
|
+
const fpCount = results.filter((r) => r.engine === "FP").length;
|
|
1426
|
+
return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, astCount, fpCount };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
117
1429
|
// src/benchmark/quality.ts
|
|
118
1430
|
var BENCHMARK_PROMPTS = [
|
|
119
1431
|
{
|
|
@@ -180,35 +1492,1089 @@ async function runQualityBenchmark(code, filePath, apiKey, promptId = "understan
|
|
|
180
1492
|
return { file: filePath, raw: rawResult, ir: irResult, savedPercent };
|
|
181
1493
|
}
|
|
182
1494
|
|
|
183
|
-
// src/
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
1495
|
+
// src/context/packer.ts
|
|
1496
|
+
function escapeRegex(s) {
|
|
1497
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1498
|
+
}
|
|
1499
|
+
function findTargetFile(files, target) {
|
|
1500
|
+
const t = escapeRegex(target);
|
|
1501
|
+
const declarationPatterns = [
|
|
1502
|
+
// JS/TS declarations
|
|
1503
|
+
new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${t}\\b`),
|
|
1504
|
+
new RegExp(`(?:export\\s+)?class\\s+${t}\\b`),
|
|
1505
|
+
new RegExp(`(?:export\\s+)?interface\\s+${t}\\b`),
|
|
1506
|
+
new RegExp(`(?:export\\s+)?type\\s+${t}\\b`),
|
|
1507
|
+
new RegExp(`(?:export\\s+)?enum\\s+${t}\\b`),
|
|
1508
|
+
new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${t}\\b`),
|
|
1509
|
+
// Python
|
|
1510
|
+
new RegExp(`\\bdef\\s+${t}\\b`),
|
|
1511
|
+
// Rust
|
|
1512
|
+
new RegExp(`\\bfn\\s+${t}\\b`),
|
|
1513
|
+
new RegExp(`\\bstruct\\s+${t}\\b`),
|
|
1514
|
+
new RegExp(`\\btrait\\s+${t}\\b`),
|
|
1515
|
+
// Go
|
|
1516
|
+
new RegExp(`\\bfunc\\s+${t}\\b`),
|
|
1517
|
+
new RegExp(`\\btype\\s+${t}\\b`),
|
|
1518
|
+
// Object method shorthand
|
|
1519
|
+
new RegExp(`\\b${t}\\s*:\\s*(?:async\\s+)?function\\b`),
|
|
1520
|
+
new RegExp(`\\b${t}\\s*\\([^)]*\\)\\s*\\{`)
|
|
1521
|
+
];
|
|
1522
|
+
for (const pattern of declarationPatterns) {
|
|
1523
|
+
const match2 = files.find((f) => pattern.test(f.code));
|
|
1524
|
+
if (match2) return match2.path;
|
|
1525
|
+
}
|
|
1526
|
+
const fallback = new RegExp(`\\b${t}\\s*\\(`);
|
|
1527
|
+
const match = files.find((f) => fallback.test(f.code));
|
|
1528
|
+
return match ? match.path : null;
|
|
1529
|
+
}
|
|
1530
|
+
function findRelatedFiles(files, targetPath) {
|
|
1531
|
+
const related = /* @__PURE__ */ new Set();
|
|
1532
|
+
const targetFile = files.find((f) => f.path === targetPath);
|
|
1533
|
+
if (!targetFile) return related;
|
|
1534
|
+
const importPattern = /(?:import|require)\s*(?:\([^)]*|\{[^}]*\}|\w+)?\s*(?:from)?\s*["']([^"']+)["']/g;
|
|
1535
|
+
const imports = [...targetFile.code.matchAll(importPattern)].map((m) => m[1]);
|
|
1536
|
+
for (const imp of imports) {
|
|
1537
|
+
const match = files.find((f) => {
|
|
1538
|
+
const basename = f.path.replace(/\.[^.]+$/, "");
|
|
1539
|
+
return imp.includes(basename) || basename.endsWith(imp.replace(/^\.\.?\//, "").replace(/\.[^.]+$/, ""));
|
|
1540
|
+
});
|
|
1541
|
+
if (match) related.add(match.path);
|
|
1542
|
+
}
|
|
1543
|
+
const targetBasename = targetPath.replace(/\.[^.]+$/, "").split("/").pop() ?? "";
|
|
1544
|
+
for (const file of files) {
|
|
1545
|
+
if (file.path === targetPath) continue;
|
|
1546
|
+
if (file.code.includes(targetBasename)) {
|
|
1547
|
+
related.add(file.path);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return related;
|
|
1551
|
+
}
|
|
1552
|
+
async function packContext(files, options) {
|
|
1553
|
+
const { budget, hotspots, target } = options;
|
|
1554
|
+
const hotspotSet = new Set(hotspots.map((h) => h.file));
|
|
1555
|
+
let targetPath = null;
|
|
1556
|
+
let relatedFiles = /* @__PURE__ */ new Set();
|
|
1557
|
+
if (target) {
|
|
1558
|
+
targetPath = findTargetFile(files, target);
|
|
1559
|
+
if (targetPath) {
|
|
1560
|
+
relatedFiles = findRelatedFiles(files, targetPath);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const entries = [];
|
|
1564
|
+
let totalTokens = 0;
|
|
1565
|
+
let filesAtL3 = 0;
|
|
1566
|
+
let targetDowngraded = false;
|
|
1567
|
+
if (targetPath) {
|
|
1568
|
+
const targetFile = files.find((f) => f.path === targetPath);
|
|
1569
|
+
const rawTokens = estimateTokens(targetFile.code);
|
|
1570
|
+
if (rawTokens <= budget * 0.6) {
|
|
1571
|
+
entries.push({
|
|
1572
|
+
path: targetPath,
|
|
1573
|
+
layer: "L3",
|
|
1574
|
+
ir: targetFile.code,
|
|
1575
|
+
tokens: rawTokens,
|
|
1576
|
+
isTarget: true
|
|
1577
|
+
});
|
|
1578
|
+
totalTokens += rawTokens;
|
|
1579
|
+
filesAtL3 = 1;
|
|
1580
|
+
} else {
|
|
1581
|
+
targetDowngraded = true;
|
|
1582
|
+
const l1 = await generateLayer("L1", { code: targetFile.code, filePath: targetFile.path, health: null });
|
|
1583
|
+
const l1Tokens = estimateTokens(l1);
|
|
1584
|
+
entries.push({
|
|
1585
|
+
path: targetPath,
|
|
1586
|
+
layer: "L1",
|
|
1587
|
+
ir: l1,
|
|
1588
|
+
tokens: l1Tokens,
|
|
1589
|
+
isTarget: true
|
|
1590
|
+
});
|
|
1591
|
+
totalTokens += l1Tokens;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
for (const file of files) {
|
|
1595
|
+
if (file.path === targetPath) continue;
|
|
1596
|
+
const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
|
|
1597
|
+
const l0Tokens = estimateTokens(l0);
|
|
1598
|
+
entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
|
|
1599
|
+
totalTokens += l0Tokens;
|
|
1600
|
+
}
|
|
1601
|
+
if (totalTokens > budget) {
|
|
1602
|
+
const truncated = entries.filter((e) => e.isTarget);
|
|
1603
|
+
let used = truncated.reduce((s, e) => s + e.tokens, 0);
|
|
188
1604
|
for (const entry of entries) {
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
194
|
-
files.push(fullPath);
|
|
1605
|
+
if (entry.isTarget) continue;
|
|
1606
|
+
if (used + entry.tokens <= budget) {
|
|
1607
|
+
truncated.push(entry);
|
|
1608
|
+
used += entry.tokens;
|
|
195
1609
|
}
|
|
196
1610
|
}
|
|
1611
|
+
return {
|
|
1612
|
+
entries: truncated,
|
|
1613
|
+
totalTokens: used,
|
|
1614
|
+
budget,
|
|
1615
|
+
filesAtL0: truncated.filter((e) => e.layer === "L0").length,
|
|
1616
|
+
filesAtL1: truncated.filter((e) => e.layer === "L1").length,
|
|
1617
|
+
filesAtL3,
|
|
1618
|
+
targetFile: targetPath ?? void 0,
|
|
1619
|
+
targetDowngraded
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
const upgradeOrder = entries.map((e, i) => ({
|
|
1623
|
+
index: i,
|
|
1624
|
+
path: e.path,
|
|
1625
|
+
rawTokens: files.find((f) => f.path === e.path)?.rawTokens ?? 0,
|
|
1626
|
+
isHotspot: hotspotSet.has(e.path),
|
|
1627
|
+
isRelated: relatedFiles.has(e.path),
|
|
1628
|
+
isTarget: e.isTarget ?? false
|
|
1629
|
+
})).filter((x) => x.isTarget === false && entries[x.index].layer === "L0").sort((a, b) => {
|
|
1630
|
+
if (a.isRelated && !b.isRelated) return -1;
|
|
1631
|
+
if (!a.isRelated && b.isRelated) return 1;
|
|
1632
|
+
if (a.isHotspot && !b.isHotspot) return -1;
|
|
1633
|
+
if (!a.isHotspot && b.isHotspot) return 1;
|
|
1634
|
+
return b.rawTokens - a.rawTokens;
|
|
1635
|
+
});
|
|
1636
|
+
let filesAtL1 = 0;
|
|
1637
|
+
for (const item of upgradeOrder) {
|
|
1638
|
+
const file = files.find((f) => f.path === item.path);
|
|
1639
|
+
const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
|
|
1640
|
+
const l1Tokens = estimateTokens(l1);
|
|
1641
|
+
const currentTokens = entries[item.index].tokens;
|
|
1642
|
+
const additional = l1Tokens - currentTokens;
|
|
1643
|
+
if (totalTokens + additional <= budget) {
|
|
1644
|
+
entries[item.index] = {
|
|
1645
|
+
path: item.path,
|
|
1646
|
+
layer: "L1",
|
|
1647
|
+
ir: l1,
|
|
1648
|
+
tokens: l1Tokens
|
|
1649
|
+
};
|
|
1650
|
+
totalTokens += additional;
|
|
1651
|
+
filesAtL1++;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
entries,
|
|
1656
|
+
totalTokens,
|
|
1657
|
+
budget,
|
|
1658
|
+
filesAtL0: entries.filter((e) => e.layer === "L0").length,
|
|
1659
|
+
filesAtL1,
|
|
1660
|
+
filesAtL3,
|
|
1661
|
+
targetFile: targetPath ?? void 0,
|
|
1662
|
+
targetDowngraded
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/utils/collectFiles.ts
|
|
1667
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1668
|
+
import { join as join2, relative } from "path";
|
|
1669
|
+
import ignore from "ignore";
|
|
1670
|
+
var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1671
|
+
function loadGitignore(dir) {
|
|
1672
|
+
const ig = ignore();
|
|
1673
|
+
try {
|
|
1674
|
+
const raw = readFileSync2(join2(dir, ".gitignore"), "utf-8");
|
|
1675
|
+
ig.add(raw);
|
|
197
1676
|
} catch {
|
|
198
1677
|
}
|
|
1678
|
+
return ig;
|
|
1679
|
+
}
|
|
1680
|
+
function collectFiles(dir, extensions, projectPath) {
|
|
1681
|
+
const root = projectPath ?? dir;
|
|
1682
|
+
const files = [];
|
|
1683
|
+
function walk(currentDir, parentIgnore) {
|
|
1684
|
+
const ig = loadGitignore(currentDir);
|
|
1685
|
+
const merged = ignore().add(parentIgnore).add(ig);
|
|
1686
|
+
try {
|
|
1687
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
1688
|
+
for (const entry of entries) {
|
|
1689
|
+
if (entry.name.startsWith(".") || ALWAYS_SKIP.has(entry.name)) continue;
|
|
1690
|
+
const fullPath = join2(currentDir, entry.name);
|
|
1691
|
+
const relPath = relative(root, fullPath);
|
|
1692
|
+
if (merged.ignores(relPath)) continue;
|
|
1693
|
+
if (entry.isDirectory()) {
|
|
1694
|
+
walk(fullPath, merged);
|
|
1695
|
+
} else if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
1696
|
+
files.push(fullPath);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
walk(dir, ignore());
|
|
199
1703
|
return files;
|
|
200
1704
|
}
|
|
1705
|
+
|
|
1706
|
+
// src/memory/db.ts
|
|
1707
|
+
import Database from "better-sqlite3";
|
|
1708
|
+
import { mkdirSync } from "fs";
|
|
1709
|
+
import { dirname as dirname2 } from "path";
|
|
1710
|
+
function openDatabase(path) {
|
|
1711
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
1712
|
+
const db = new Database(path);
|
|
1713
|
+
db.pragma("journal_mode = WAL");
|
|
1714
|
+
db.pragma("synchronous = NORMAL");
|
|
1715
|
+
db.pragma("foreign_keys = ON");
|
|
1716
|
+
return db;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/memory/schema.ts
|
|
1720
|
+
var CURRENT_VERSION = 1;
|
|
1721
|
+
var V1_SQL = `
|
|
1722
|
+
CREATE TABLE IF NOT EXISTS index_state (
|
|
1723
|
+
key TEXT PRIMARY KEY,
|
|
1724
|
+
value TEXT NOT NULL
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
1728
|
+
sha TEXT PRIMARY KEY,
|
|
1729
|
+
parent_sha TEXT,
|
|
1730
|
+
author TEXT NOT NULL,
|
|
1731
|
+
timestamp INTEGER NOT NULL,
|
|
1732
|
+
subject TEXT NOT NULL,
|
|
1733
|
+
is_fix INTEGER NOT NULL,
|
|
1734
|
+
is_revert INTEGER NOT NULL,
|
|
1735
|
+
reverts_sha TEXT,
|
|
1736
|
+
FOREIGN KEY (reverts_sha) REFERENCES commits(sha)
|
|
1737
|
+
);
|
|
1738
|
+
CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
|
|
1739
|
+
CREATE INDEX IF NOT EXISTS idx_commits_is_fix ON commits(is_fix) WHERE is_fix = 1;
|
|
1740
|
+
|
|
1741
|
+
CREATE TABLE IF NOT EXISTS file_touches (
|
|
1742
|
+
commit_sha TEXT NOT NULL,
|
|
1743
|
+
file_path TEXT NOT NULL,
|
|
1744
|
+
adds INTEGER NOT NULL,
|
|
1745
|
+
dels INTEGER NOT NULL,
|
|
1746
|
+
change_type TEXT NOT NULL,
|
|
1747
|
+
renamed_from TEXT,
|
|
1748
|
+
PRIMARY KEY (commit_sha, file_path),
|
|
1749
|
+
FOREIGN KEY (commit_sha) REFERENCES commits(sha)
|
|
1750
|
+
);
|
|
1751
|
+
CREATE INDEX IF NOT EXISTS idx_ft_file ON file_touches(file_path);
|
|
1752
|
+
|
|
1753
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
1754
|
+
id INTEGER PRIMARY KEY,
|
|
1755
|
+
file_path TEXT NOT NULL,
|
|
1756
|
+
kind TEXT NOT NULL,
|
|
1757
|
+
qualified_name TEXT NOT NULL,
|
|
1758
|
+
first_seen_sha TEXT NOT NULL,
|
|
1759
|
+
last_seen_sha TEXT,
|
|
1760
|
+
UNIQUE (file_path, kind, qualified_name)
|
|
1761
|
+
);
|
|
1762
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
|
|
1763
|
+
|
|
1764
|
+
CREATE TABLE IF NOT EXISTS symbol_touches (
|
|
1765
|
+
commit_sha TEXT NOT NULL,
|
|
1766
|
+
symbol_id INTEGER NOT NULL,
|
|
1767
|
+
change_type TEXT NOT NULL,
|
|
1768
|
+
PRIMARY KEY (commit_sha, symbol_id),
|
|
1769
|
+
FOREIGN KEY (commit_sha) REFERENCES commits(sha),
|
|
1770
|
+
FOREIGN KEY (symbol_id) REFERENCES symbols(id)
|
|
1771
|
+
);
|
|
1772
|
+
CREATE INDEX IF NOT EXISTS idx_st_symbol ON symbol_touches(symbol_id);
|
|
1773
|
+
|
|
1774
|
+
CREATE TABLE IF NOT EXISTS fix_links (
|
|
1775
|
+
fix_commit_sha TEXT NOT NULL,
|
|
1776
|
+
suspected_break_sha TEXT NOT NULL,
|
|
1777
|
+
evidence_type TEXT NOT NULL,
|
|
1778
|
+
confidence REAL NOT NULL,
|
|
1779
|
+
window_hours INTEGER,
|
|
1780
|
+
PRIMARY KEY (fix_commit_sha, suspected_break_sha, evidence_type),
|
|
1781
|
+
FOREIGN KEY (fix_commit_sha) REFERENCES commits(sha),
|
|
1782
|
+
FOREIGN KEY (suspected_break_sha) REFERENCES commits(sha)
|
|
1783
|
+
);
|
|
1784
|
+
CREATE INDEX IF NOT EXISTS idx_fl_break ON fix_links(suspected_break_sha);
|
|
1785
|
+
|
|
1786
|
+
CREATE TABLE IF NOT EXISTS signal_calibration (
|
|
1787
|
+
signal_type TEXT PRIMARY KEY,
|
|
1788
|
+
precision REAL NOT NULL,
|
|
1789
|
+
sample_size INTEGER NOT NULL,
|
|
1790
|
+
last_computed_sha TEXT NOT NULL,
|
|
1791
|
+
computed_at INTEGER NOT NULL
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
CREATE TABLE IF NOT EXISTS file_index_state (
|
|
1795
|
+
file_path TEXT PRIMARY KEY,
|
|
1796
|
+
last_commit_indexed TEXT NOT NULL,
|
|
1797
|
+
last_blob_indexed TEXT,
|
|
1798
|
+
indexed_at INTEGER NOT NULL,
|
|
1799
|
+
parse_failed INTEGER NOT NULL DEFAULT 0,
|
|
1800
|
+
FOREIGN KEY (last_commit_indexed) REFERENCES commits(sha)
|
|
1801
|
+
);
|
|
1802
|
+
`;
|
|
1803
|
+
function runMigrations(db) {
|
|
1804
|
+
const current = db.pragma("user_version", { simple: true });
|
|
1805
|
+
if (current >= CURRENT_VERSION) return;
|
|
1806
|
+
db.exec("BEGIN");
|
|
1807
|
+
try {
|
|
1808
|
+
db.exec(V1_SQL);
|
|
1809
|
+
db.pragma(`user_version = ${CURRENT_VERSION}`);
|
|
1810
|
+
db.exec("COMMIT");
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
db.exec("ROLLBACK");
|
|
1813
|
+
throw err;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/memory/git.ts
|
|
1818
|
+
import { execSync as execSync2 } from "child_process";
|
|
1819
|
+
function run(cwd, cmd, timeoutMs = 1e4) {
|
|
1820
|
+
return execSync2(cmd, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
1821
|
+
}
|
|
1822
|
+
function revParseHead(cwd) {
|
|
1823
|
+
return run(cwd, "git rev-parse HEAD");
|
|
1824
|
+
}
|
|
1825
|
+
function isShallowRepo(cwd) {
|
|
1826
|
+
return run(cwd, "git rev-parse --is-shallow-repository") === "true";
|
|
1827
|
+
}
|
|
1828
|
+
function revListCount(cwd, from, to) {
|
|
1829
|
+
if (from === to) return 0;
|
|
1830
|
+
const out = run(cwd, `git rev-list --count ${from}..${to}`);
|
|
1831
|
+
return parseInt(out, 10);
|
|
1832
|
+
}
|
|
1833
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
1834
|
+
try {
|
|
1835
|
+
execSync2(`git merge-base --is-ancestor ${ancestor} ${descendant}`, {
|
|
1836
|
+
cwd,
|
|
1837
|
+
stdio: "ignore"
|
|
1838
|
+
});
|
|
1839
|
+
return true;
|
|
1840
|
+
} catch {
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function countCommits(cwd) {
|
|
1845
|
+
const out = run(cwd, "git rev-list --count HEAD");
|
|
1846
|
+
return parseInt(out, 10);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/memory/freshness.ts
|
|
1850
|
+
function ensureFresh(db, repoPath) {
|
|
1851
|
+
const head = revParseHead(repoPath);
|
|
1852
|
+
const row = db.prepare("SELECT value FROM index_state WHERE key = 'last_indexed_sha'").get();
|
|
1853
|
+
if (!row) {
|
|
1854
|
+
return {
|
|
1855
|
+
tazelik: "bootstrapping",
|
|
1856
|
+
head,
|
|
1857
|
+
delta: { from: null, to: head },
|
|
1858
|
+
behind_by: 0,
|
|
1859
|
+
rewritten: false
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
const last = row.value;
|
|
1863
|
+
if (last === head) {
|
|
1864
|
+
return { tazelik: "fresh", head, delta: null, behind_by: 0, rewritten: false };
|
|
1865
|
+
}
|
|
1866
|
+
const reachable = isAncestor(repoPath, last, head);
|
|
1867
|
+
if (!reachable) {
|
|
1868
|
+
return {
|
|
1869
|
+
tazelik: "bootstrapping",
|
|
1870
|
+
head,
|
|
1871
|
+
delta: { from: null, to: head },
|
|
1872
|
+
behind_by: 0,
|
|
1873
|
+
rewritten: true
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
const behind_by = revListCount(repoPath, last, head);
|
|
1877
|
+
return {
|
|
1878
|
+
tazelik: "catching_up",
|
|
1879
|
+
head,
|
|
1880
|
+
delta: { from: last, to: head },
|
|
1881
|
+
behind_by,
|
|
1882
|
+
rewritten: false
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// src/memory/signals/calibration-lookup.ts
|
|
1887
|
+
function getCalibration(db, type, fallbackPrecision) {
|
|
1888
|
+
const row = db.prepare("SELECT precision, sample_size FROM signal_calibration WHERE signal_type = ?").get(type);
|
|
1889
|
+
if (!row) {
|
|
1890
|
+
return { precision: fallbackPrecision, sampleSize: 0, source: "heuristic" };
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
precision: row.precision,
|
|
1894
|
+
sampleSize: row.sample_size,
|
|
1895
|
+
source: "repo-calibrated"
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/memory/signals/revert-match.ts
|
|
1900
|
+
var STRENGTH_BY_EVIDENCE = {
|
|
1901
|
+
revert_marker: 1,
|
|
1902
|
+
short_followup_fix: 0.7,
|
|
1903
|
+
same_region_fix_chain: 0.4
|
|
1904
|
+
};
|
|
1905
|
+
var FALLBACK_PRECISION = 0.5;
|
|
1906
|
+
var MAX_EVIDENCE = 5;
|
|
1907
|
+
function computeRevertMatch(db, filePath) {
|
|
1908
|
+
const rows = db.prepare(`
|
|
1909
|
+
SELECT fl.evidence_type, fl.confidence, fl.suspected_break_sha,
|
|
1910
|
+
c.subject, c.timestamp
|
|
1911
|
+
FROM fix_links fl
|
|
1912
|
+
JOIN file_touches ft ON ft.commit_sha = fl.suspected_break_sha
|
|
1913
|
+
JOIN commits c ON c.sha = fl.suspected_break_sha
|
|
1914
|
+
WHERE ft.file_path = ?
|
|
1915
|
+
ORDER BY c.timestamp DESC
|
|
1916
|
+
LIMIT ?
|
|
1917
|
+
`).all(filePath, MAX_EVIDENCE);
|
|
1918
|
+
const cal = getCalibration(db, "revert_match", FALLBACK_PRECISION);
|
|
1919
|
+
if (rows.length === 0) {
|
|
1920
|
+
return {
|
|
1921
|
+
type: "revert_match",
|
|
1922
|
+
strength: 0,
|
|
1923
|
+
precision: cal.precision,
|
|
1924
|
+
sample_size: cal.sampleSize,
|
|
1925
|
+
evidence: []
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
let strength = 0;
|
|
1929
|
+
const evidence = [];
|
|
1930
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1931
|
+
for (const r of rows) {
|
|
1932
|
+
const s = STRENGTH_BY_EVIDENCE[r.evidence_type] ?? 0;
|
|
1933
|
+
if (s > strength) strength = s;
|
|
1934
|
+
evidence.push({
|
|
1935
|
+
commit_sha: r.suspected_break_sha,
|
|
1936
|
+
subject: r.subject,
|
|
1937
|
+
days_ago: Math.floor((now - r.timestamp) / 86400),
|
|
1938
|
+
evidence_type: r.evidence_type
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
return {
|
|
1942
|
+
type: "revert_match",
|
|
1943
|
+
strength,
|
|
1944
|
+
precision: cal.precision,
|
|
1945
|
+
sample_size: cal.sampleSize,
|
|
1946
|
+
evidence
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/memory/signals/hotspot.ts
|
|
1951
|
+
var WINDOW_SECONDS = 90 * 86400;
|
|
1952
|
+
var SATURATION_TOUCHES = 30;
|
|
1953
|
+
var FALLBACK_PRECISION2 = 0.3;
|
|
1954
|
+
function computeHotspot(db, filePath) {
|
|
1955
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1956
|
+
const lowerBound = now - WINDOW_SECONDS;
|
|
1957
|
+
const row = db.prepare(`
|
|
1958
|
+
SELECT COUNT(*) AS n
|
|
1959
|
+
FROM file_touches ft
|
|
1960
|
+
JOIN commits c ON c.sha = ft.commit_sha
|
|
1961
|
+
WHERE ft.file_path = ? AND c.timestamp >= ?
|
|
1962
|
+
`).get(filePath, lowerBound);
|
|
1963
|
+
const touches = row.n;
|
|
1964
|
+
const strength = Math.min(1, touches / SATURATION_TOUCHES);
|
|
1965
|
+
const cal = getCalibration(db, "hotspot", FALLBACK_PRECISION2);
|
|
1966
|
+
return {
|
|
1967
|
+
type: "hotspot",
|
|
1968
|
+
strength,
|
|
1969
|
+
precision: cal.precision,
|
|
1970
|
+
sample_size: cal.sampleSize,
|
|
1971
|
+
evidence: [],
|
|
1972
|
+
touches_90d: touches
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/memory/signals/fix-ratio.ts
|
|
1977
|
+
var WINDOW_COMMITS = 30;
|
|
1978
|
+
var DEAD_ZONE = 0.3;
|
|
1979
|
+
var SATURATION_OVER_DEAD_ZONE = 0.5;
|
|
1980
|
+
var FALLBACK_PRECISION3 = 0.3;
|
|
1981
|
+
function computeFixRatio(db, filePath) {
|
|
1982
|
+
const rows = db.prepare(`
|
|
1983
|
+
SELECT c.is_fix
|
|
1984
|
+
FROM file_touches ft
|
|
1985
|
+
JOIN commits c ON c.sha = ft.commit_sha
|
|
1986
|
+
WHERE ft.file_path = ?
|
|
1987
|
+
ORDER BY c.timestamp DESC
|
|
1988
|
+
LIMIT ?
|
|
1989
|
+
`).all(filePath, WINDOW_COMMITS);
|
|
1990
|
+
const cal = getCalibration(db, "fix_ratio", FALLBACK_PRECISION3);
|
|
1991
|
+
if (rows.length === 0) {
|
|
1992
|
+
return {
|
|
1993
|
+
type: "fix_ratio",
|
|
1994
|
+
strength: 0,
|
|
1995
|
+
precision: cal.precision,
|
|
1996
|
+
sample_size: cal.sampleSize,
|
|
1997
|
+
evidence: [],
|
|
1998
|
+
ratio: 0
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
const fixes = rows.filter((r) => r.is_fix === 1).length;
|
|
2002
|
+
const ratio = fixes / rows.length;
|
|
2003
|
+
const strength = Math.max(0, Math.min(1, (ratio - DEAD_ZONE) / SATURATION_OVER_DEAD_ZONE));
|
|
2004
|
+
return {
|
|
2005
|
+
type: "fix_ratio",
|
|
2006
|
+
strength,
|
|
2007
|
+
precision: cal.precision,
|
|
2008
|
+
sample_size: cal.sampleSize,
|
|
2009
|
+
evidence: [],
|
|
2010
|
+
ratio
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/memory/signals/coverage-decline.ts
|
|
2015
|
+
var FALLBACK_PRECISION4 = 0.3;
|
|
2016
|
+
function computeCoverageDecline(db, repoPath, filePath) {
|
|
2017
|
+
const cal = getCalibration(db, "coverage_decline", FALLBACK_PRECISION4);
|
|
2018
|
+
let strength = 0;
|
|
2019
|
+
try {
|
|
2020
|
+
const entries = getGitLog(repoPath, 200);
|
|
2021
|
+
const trends = {
|
|
2022
|
+
hotspots: detectHotspots(entries, { threshold: 10, fixRatioThreshold: 0.5 }),
|
|
2023
|
+
decaySignals: detectDecay(entries),
|
|
2024
|
+
inconsistencies: detectInconsistencies(entries)
|
|
2025
|
+
};
|
|
2026
|
+
const health = computeHealthFromTrends(filePath, trends);
|
|
2027
|
+
if (health.coverageTrend === "down") strength = 1;
|
|
2028
|
+
} catch {
|
|
2029
|
+
strength = 0;
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
type: "coverage_decline",
|
|
2033
|
+
strength,
|
|
2034
|
+
precision: cal.precision,
|
|
2035
|
+
sample_size: cal.sampleSize,
|
|
2036
|
+
evidence: []
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/memory/signals/author-churn.ts
|
|
2041
|
+
var WINDOW_SECONDS2 = 90 * 86400;
|
|
2042
|
+
var INACTIVE_THRESHOLD = 5;
|
|
2043
|
+
var FALLBACK_PRECISION5 = 0.3;
|
|
2044
|
+
function computeAuthorChurn(db, filePath) {
|
|
2045
|
+
const cal = getCalibration(db, "author_churn", FALLBACK_PRECISION5);
|
|
2046
|
+
const base = {
|
|
2047
|
+
type: "author_churn",
|
|
2048
|
+
precision: cal.precision,
|
|
2049
|
+
sample_size: cal.sampleSize,
|
|
2050
|
+
evidence: []
|
|
2051
|
+
};
|
|
2052
|
+
const lastTouch = db.prepare(`
|
|
2053
|
+
SELECT c.author, c.timestamp
|
|
2054
|
+
FROM file_touches ft
|
|
2055
|
+
JOIN commits c ON c.sha = ft.commit_sha
|
|
2056
|
+
WHERE ft.file_path = ?
|
|
2057
|
+
ORDER BY c.timestamp DESC
|
|
2058
|
+
LIMIT 1
|
|
2059
|
+
`).get(filePath);
|
|
2060
|
+
if (!lastTouch) return { ...base, strength: 0 };
|
|
2061
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2062
|
+
const lowerBound = now - WINDOW_SECONDS2;
|
|
2063
|
+
const activity = db.prepare(`SELECT COUNT(*) AS n FROM commits WHERE author = ? AND timestamp >= ?`).get(lastTouch.author, lowerBound);
|
|
2064
|
+
let strength = 0;
|
|
2065
|
+
if (activity.n === 0) strength = 1;
|
|
2066
|
+
else if (activity.n < INACTIVE_THRESHOLD) strength = 0.5;
|
|
2067
|
+
return { ...base, strength };
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/memory/signals/index.ts
|
|
2071
|
+
function collectSignals(db, repoPath, filePath) {
|
|
2072
|
+
return [
|
|
2073
|
+
computeRevertMatch(db, filePath),
|
|
2074
|
+
computeHotspot(db, filePath),
|
|
2075
|
+
computeFixRatio(db, filePath),
|
|
2076
|
+
computeCoverageDecline(db, repoPath, filePath),
|
|
2077
|
+
computeAuthorChurn(db, filePath)
|
|
2078
|
+
];
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/memory/confidence.ts
|
|
2082
|
+
var USABLE_SAMPLE_THRESHOLD = 20;
|
|
2083
|
+
function coverageFactor(signals) {
|
|
2084
|
+
const usable = signals.filter(
|
|
2085
|
+
(s) => s.strength > 0 && s.sample_size >= USABLE_SAMPLE_THRESHOLD
|
|
2086
|
+
).length;
|
|
2087
|
+
return Math.min(1, usable / 3);
|
|
2088
|
+
}
|
|
2089
|
+
function calibrationFactor(signals) {
|
|
2090
|
+
const firing = signals.filter((s) => s.strength > 0);
|
|
2091
|
+
if (firing.length === 0) return 1;
|
|
2092
|
+
const avg = firing.reduce((acc, s) => acc + s.sample_size, 0) / firing.length;
|
|
2093
|
+
if (avg < 20) return 0.3;
|
|
2094
|
+
if (avg < 100) return 0.6;
|
|
2095
|
+
return 1;
|
|
2096
|
+
}
|
|
2097
|
+
function freshnessFactor(ctx) {
|
|
2098
|
+
if (ctx.partial) return 0.4;
|
|
2099
|
+
switch (ctx.tazelik) {
|
|
2100
|
+
case "fresh":
|
|
2101
|
+
return 1;
|
|
2102
|
+
case "catching_up":
|
|
2103
|
+
return 0.8;
|
|
2104
|
+
case "partial":
|
|
2105
|
+
return 0.4;
|
|
2106
|
+
case "bootstrapping":
|
|
2107
|
+
return 0.2;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function historyFactor(totalCommits) {
|
|
2111
|
+
if (totalCommits < 50) return 0.2;
|
|
2112
|
+
if (totalCommits < 200) return 0.5;
|
|
2113
|
+
if (totalCommits < 1e3) return 0.8;
|
|
2114
|
+
return 1;
|
|
2115
|
+
}
|
|
2116
|
+
function computeScoreAndConfidence(signals, ctx) {
|
|
2117
|
+
let num = 0;
|
|
2118
|
+
let den = 0;
|
|
2119
|
+
for (const s of signals) {
|
|
2120
|
+
if (s.strength <= 0 || s.precision <= 0) continue;
|
|
2121
|
+
num += s.strength * s.precision;
|
|
2122
|
+
den += s.precision;
|
|
2123
|
+
}
|
|
2124
|
+
const score = den === 0 ? 0 : num / den;
|
|
2125
|
+
const confidence = Math.min(
|
|
2126
|
+
coverageFactor(signals),
|
|
2127
|
+
calibrationFactor(signals),
|
|
2128
|
+
freshnessFactor(ctx),
|
|
2129
|
+
historyFactor(ctx.totalCommits)
|
|
2130
|
+
);
|
|
2131
|
+
return { score, confidence };
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/memory/verdict.ts
|
|
2135
|
+
function mapVerdict(score, confidence) {
|
|
2136
|
+
if (confidence < 0.3) return "unknown";
|
|
2137
|
+
if (score < 0.3) return "low";
|
|
2138
|
+
if (score < 0.6) return "medium";
|
|
2139
|
+
return "high";
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// src/memory/envelope.ts
|
|
2143
|
+
var CONFIDENCE_CAP = {
|
|
2144
|
+
ok: 1,
|
|
2145
|
+
empty_repo: 0,
|
|
2146
|
+
insufficient_history: 0.3,
|
|
2147
|
+
shallow_clone: 0,
|
|
2148
|
+
indexing: 0.4,
|
|
2149
|
+
squashed_history: 0.5,
|
|
2150
|
+
reindexing: 0,
|
|
2151
|
+
internal_error: 0,
|
|
2152
|
+
disabled: 0
|
|
2153
|
+
};
|
|
2154
|
+
var USABLE_SAMPLE_THRESHOLD2 = 20;
|
|
2155
|
+
function inferCalibrationSource(signals) {
|
|
2156
|
+
return signals.some((s) => s.sample_size > 0) ? "repo-calibrated" : "heuristic";
|
|
2157
|
+
}
|
|
2158
|
+
function buildEnvelope(args2) {
|
|
2159
|
+
const cap = CONFIDENCE_CAP[args2.status];
|
|
2160
|
+
const cappedConfidence = Math.min(args2.confidence, cap);
|
|
2161
|
+
const verdict = mapVerdict(args2.score, cappedConfidence);
|
|
2162
|
+
const usable = args2.signals.filter(
|
|
2163
|
+
(s) => s.strength > 0 && s.sample_size >= USABLE_SAMPLE_THRESHOLD2
|
|
2164
|
+
).length;
|
|
2165
|
+
return {
|
|
2166
|
+
status: args2.status,
|
|
2167
|
+
reason: args2.reason,
|
|
2168
|
+
verdict,
|
|
2169
|
+
score: args2.score,
|
|
2170
|
+
confidence: cappedConfidence,
|
|
2171
|
+
signals: args2.signals,
|
|
2172
|
+
calibration: inferCalibrationSource(args2.signals),
|
|
2173
|
+
retry_hint_ms: args2.retry_hint_ms,
|
|
2174
|
+
confidence_cap: args2.status === "ok" ? void 0 : cap,
|
|
2175
|
+
metadata: {
|
|
2176
|
+
tazelik: args2.tazelik,
|
|
2177
|
+
index_version: 1,
|
|
2178
|
+
indexed_commits_through: args2.indexedThrough,
|
|
2179
|
+
indexed_commits_total: args2.indexedTotal,
|
|
2180
|
+
query_ms: args2.queryMs,
|
|
2181
|
+
signal_coverage: `${usable}/${args2.signals.length}`
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/memory/pool.ts
|
|
2187
|
+
import { Worker } from "worker_threads";
|
|
2188
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2189
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2190
|
+
function resolveWorkerPath() {
|
|
2191
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
2192
|
+
if (here.endsWith("/memory") || here.endsWith("\\memory")) {
|
|
2193
|
+
return join3(here, "worker.js");
|
|
2194
|
+
}
|
|
2195
|
+
return join3(here, "memory", "worker.js");
|
|
2196
|
+
}
|
|
2197
|
+
var WorkerPool = class {
|
|
2198
|
+
workers = [];
|
|
2199
|
+
nextJobId = 1;
|
|
2200
|
+
pending = /* @__PURE__ */ new Map();
|
|
2201
|
+
constructor(opts = {}) {
|
|
2202
|
+
const size = Math.max(1, opts.size ?? 1);
|
|
2203
|
+
for (let i = 0; i < size; i++) this.spawn();
|
|
2204
|
+
}
|
|
2205
|
+
spawn() {
|
|
2206
|
+
const worker = new Worker(resolveWorkerPath());
|
|
2207
|
+
worker.on("message", (msg) => {
|
|
2208
|
+
const job = this.pending.get(msg.jobId);
|
|
2209
|
+
if (!job) return;
|
|
2210
|
+
this.pending.delete(msg.jobId);
|
|
2211
|
+
if (msg.type === "ingest_done") {
|
|
2212
|
+
job.resolve({ status: "done", commits: msg.commits });
|
|
2213
|
+
} else if (msg.type === "ingest_error") {
|
|
2214
|
+
job.reject(new Error(msg.message));
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
worker.on("error", (err) => {
|
|
2218
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2219
|
+
for (const job of this.pending.values()) job.reject(error);
|
|
2220
|
+
this.pending.clear();
|
|
2221
|
+
});
|
|
2222
|
+
this.workers.push(worker);
|
|
2223
|
+
}
|
|
2224
|
+
runIngest(args2) {
|
|
2225
|
+
const jobId = this.nextJobId++;
|
|
2226
|
+
const worker = this.workers[jobId % this.workers.length];
|
|
2227
|
+
return new Promise((resolve3, reject) => {
|
|
2228
|
+
this.pending.set(jobId, { resolve: resolve3, reject });
|
|
2229
|
+
worker.postMessage({ type: "ingest", jobId, ...args2 });
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
async close() {
|
|
2233
|
+
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
2234
|
+
this.workers = [];
|
|
2235
|
+
this.pending.clear();
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
// src/memory/detectors.ts
|
|
2240
|
+
function detectSquashed(db) {
|
|
2241
|
+
const row = db.prepare(`
|
|
2242
|
+
SELECT author, COUNT(*) AS n, MIN(timestamp) AS t0, MAX(timestamp) AS t1
|
|
2243
|
+
FROM commits
|
|
2244
|
+
GROUP BY author
|
|
2245
|
+
ORDER BY n DESC
|
|
2246
|
+
LIMIT 1
|
|
2247
|
+
`).get();
|
|
2248
|
+
if (!row || row.n < 50) return false;
|
|
2249
|
+
const span = row.t1 - row.t0;
|
|
2250
|
+
return span < 86400;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// src/memory/failure-tracker.ts
|
|
2254
|
+
import { readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2255
|
+
import { join as join4 } from "path";
|
|
2256
|
+
var STRIKE_THRESHOLD = 3;
|
|
2257
|
+
var WINDOW_SECONDS3 = 300;
|
|
2258
|
+
function createFailureTracker(composto_dir) {
|
|
2259
|
+
const path = join4(composto_dir, "failures.json");
|
|
2260
|
+
try {
|
|
2261
|
+
mkdirSync2(composto_dir, { recursive: true });
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
function load() {
|
|
2265
|
+
try {
|
|
2266
|
+
const raw = readFileSync3(path, "utf-8");
|
|
2267
|
+
return JSON.parse(raw);
|
|
2268
|
+
} catch {
|
|
2269
|
+
return { failures: [], disabled: false };
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function save(s) {
|
|
2273
|
+
try {
|
|
2274
|
+
writeFileSync(path, JSON.stringify(s), "utf-8");
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
function now() {
|
|
2279
|
+
return Math.floor(Date.now() / 1e3);
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
recordFailure: (failureClass) => {
|
|
2283
|
+
const s = load();
|
|
2284
|
+
s.failures.push({ class: failureClass, t: now() });
|
|
2285
|
+
s.failures = s.failures.filter((f) => now() - f.t <= WINDOW_SECONDS3);
|
|
2286
|
+
const sameClass = s.failures.filter((f) => f.class === failureClass);
|
|
2287
|
+
if (sameClass.length >= STRIKE_THRESHOLD) s.disabled = true;
|
|
2288
|
+
save(s);
|
|
2289
|
+
},
|
|
2290
|
+
recordSuccess: () => {
|
|
2291
|
+
save({ failures: [], disabled: false });
|
|
2292
|
+
},
|
|
2293
|
+
isDisabled: () => {
|
|
2294
|
+
return load().disabled;
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// src/memory/log.ts
|
|
2300
|
+
import { appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync, statSync, unlinkSync } from "fs";
|
|
2301
|
+
import { join as join5 } from "path";
|
|
2302
|
+
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2303
|
+
var RETENTION_DAYS = 7;
|
|
2304
|
+
function currentThreshold() {
|
|
2305
|
+
const raw = (process.env.COMPOSTO_LOG ?? "info").toLowerCase();
|
|
2306
|
+
if (raw === "debug" || raw === "info" || raw === "warn" || raw === "error") return raw;
|
|
2307
|
+
return "info";
|
|
2308
|
+
}
|
|
2309
|
+
function rotateIfNeeded(dir) {
|
|
2310
|
+
const logPath = join5(dir, "index.log");
|
|
2311
|
+
try {
|
|
2312
|
+
const s = statSync(logPath);
|
|
2313
|
+
const age = (Date.now() - s.mtimeMs) / 864e5;
|
|
2314
|
+
if (age < 1) return;
|
|
2315
|
+
const files = readdirSync2(dir).filter((f) => /^index\.log(\.\d+)?$/.test(f));
|
|
2316
|
+
const numbered = files.map((f) => {
|
|
2317
|
+
const m = f.match(/^index\.log\.(\d+)$/);
|
|
2318
|
+
return { name: f, n: m ? parseInt(m[1], 10) : 0 };
|
|
2319
|
+
}).sort((a, b) => b.n - a.n);
|
|
2320
|
+
for (const f of numbered) {
|
|
2321
|
+
if (f.n >= RETENTION_DAYS) {
|
|
2322
|
+
unlinkSync(join5(dir, f.name));
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (f.n === 0) {
|
|
2326
|
+
renameSync(join5(dir, f.name), join5(dir, "index.log.1"));
|
|
2327
|
+
} else {
|
|
2328
|
+
renameSync(join5(dir, f.name), join5(dir, `index.log.${f.n + 1}`));
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
} catch {
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function createLogger(composto_dir) {
|
|
2335
|
+
let disabled = false;
|
|
2336
|
+
try {
|
|
2337
|
+
mkdirSync3(composto_dir, { recursive: true });
|
|
2338
|
+
rotateIfNeeded(composto_dir);
|
|
2339
|
+
} catch {
|
|
2340
|
+
disabled = true;
|
|
2341
|
+
}
|
|
2342
|
+
const path = join5(composto_dir, "index.log");
|
|
2343
|
+
const threshold = currentThreshold();
|
|
2344
|
+
function write(level, evt, extras) {
|
|
2345
|
+
if (disabled) return;
|
|
2346
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[threshold]) return;
|
|
2347
|
+
const line = JSON.stringify({
|
|
2348
|
+
t: Math.floor(Date.now() / 1e3),
|
|
2349
|
+
lvl: level,
|
|
2350
|
+
evt,
|
|
2351
|
+
...extras ?? {}
|
|
2352
|
+
});
|
|
2353
|
+
try {
|
|
2354
|
+
appendFileSync(path, line + "\n", "utf-8");
|
|
2355
|
+
} catch {
|
|
2356
|
+
disabled = true;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return {
|
|
2360
|
+
debug: (evt, extras) => write("debug", evt, extras),
|
|
2361
|
+
info: (evt, extras) => write("info", evt, extras),
|
|
2362
|
+
warn: (evt, extras) => write("warn", evt, extras),
|
|
2363
|
+
error: (evt, extras) => write("error", evt, extras),
|
|
2364
|
+
close: () => {
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/memory/api.ts
|
|
2370
|
+
import { dirname as dirname4 } from "path";
|
|
2371
|
+
var EMPTY_REPO_THRESHOLD = 10;
|
|
2372
|
+
var MemoryAPI = class {
|
|
2373
|
+
db;
|
|
2374
|
+
pool;
|
|
2375
|
+
dbPath;
|
|
2376
|
+
repoPath;
|
|
2377
|
+
compostoDir;
|
|
2378
|
+
log;
|
|
2379
|
+
failures;
|
|
2380
|
+
bootstrapPromise = null;
|
|
2381
|
+
constructor(opts) {
|
|
2382
|
+
this.dbPath = opts.dbPath;
|
|
2383
|
+
this.repoPath = opts.repoPath;
|
|
2384
|
+
this.compostoDir = dirname4(opts.dbPath);
|
|
2385
|
+
this.log = createLogger(this.compostoDir);
|
|
2386
|
+
this.failures = createFailureTracker(this.compostoDir);
|
|
2387
|
+
this.db = openDatabase(opts.dbPath);
|
|
2388
|
+
runMigrations(this.db);
|
|
2389
|
+
this.pool = new WorkerPool({ size: opts.workerPoolSize ?? 1 });
|
|
2390
|
+
this.log.info("api_open", { dbPath: opts.dbPath });
|
|
2391
|
+
}
|
|
2392
|
+
async bootstrapIfNeeded() {
|
|
2393
|
+
if (this.bootstrapPromise) return this.bootstrapPromise;
|
|
2394
|
+
const fresh = ensureFresh(this.db, this.repoPath);
|
|
2395
|
+
if (fresh.tazelik === "fresh" || !fresh.delta) return;
|
|
2396
|
+
this.bootstrapPromise = this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range: fresh.delta }).then(() => {
|
|
2397
|
+
this.log.info("bootstrap_done", { through: fresh.delta?.to });
|
|
2398
|
+
}).catch((err) => {
|
|
2399
|
+
this.log.error("bootstrap_failed", { message: err.message });
|
|
2400
|
+
this.failures.recordFailure("ingest_failure");
|
|
2401
|
+
throw err;
|
|
2402
|
+
}).finally(() => {
|
|
2403
|
+
this.bootstrapPromise = null;
|
|
2404
|
+
});
|
|
2405
|
+
return this.bootstrapPromise;
|
|
2406
|
+
}
|
|
2407
|
+
async blastradius(input) {
|
|
2408
|
+
const start = Date.now();
|
|
2409
|
+
if (this.failures.isDisabled()) {
|
|
2410
|
+
this.log.warn("call_on_disabled", { file: input.file });
|
|
2411
|
+
return buildEnvelope({
|
|
2412
|
+
status: "disabled",
|
|
2413
|
+
signals: [],
|
|
2414
|
+
score: 0,
|
|
2415
|
+
confidence: 0,
|
|
2416
|
+
tazelik: "fresh",
|
|
2417
|
+
indexedThrough: "",
|
|
2418
|
+
indexedTotal: 0,
|
|
2419
|
+
queryMs: Date.now() - start,
|
|
2420
|
+
reason: "tool disabled after repeated failures; clear .composto/failures.json to re-enable"
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
try {
|
|
2424
|
+
return await this.runQuery(input, start);
|
|
2425
|
+
} catch (err) {
|
|
2426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2427
|
+
this.log.error("internal_error", { file: input.file, message });
|
|
2428
|
+
this.failures.recordFailure("internal_error");
|
|
2429
|
+
return buildEnvelope({
|
|
2430
|
+
status: "internal_error",
|
|
2431
|
+
signals: [],
|
|
2432
|
+
score: 0,
|
|
2433
|
+
confidence: 0,
|
|
2434
|
+
tazelik: "fresh",
|
|
2435
|
+
indexedThrough: "",
|
|
2436
|
+
indexedTotal: 0,
|
|
2437
|
+
queryMs: Date.now() - start,
|
|
2438
|
+
reason: `internal error: ${message}; see .composto/index.log`
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
async runQuery(input, start) {
|
|
2443
|
+
if (isShallowRepo(this.repoPath)) {
|
|
2444
|
+
return buildEnvelope({
|
|
2445
|
+
status: "shallow_clone",
|
|
2446
|
+
signals: [],
|
|
2447
|
+
score: 0,
|
|
2448
|
+
confidence: 0,
|
|
2449
|
+
tazelik: "fresh",
|
|
2450
|
+
indexedThrough: "",
|
|
2451
|
+
indexedTotal: 0,
|
|
2452
|
+
queryMs: Date.now() - start,
|
|
2453
|
+
reason: "shallow clone detected; run `git fetch --unshallow` or `composto index --deepen`"
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
const totalCommits = countCommits(this.repoPath);
|
|
2457
|
+
if (totalCommits < EMPTY_REPO_THRESHOLD) {
|
|
2458
|
+
return buildEnvelope({
|
|
2459
|
+
status: "empty_repo",
|
|
2460
|
+
signals: [],
|
|
2461
|
+
score: 0,
|
|
2462
|
+
confidence: 0,
|
|
2463
|
+
tazelik: "fresh",
|
|
2464
|
+
indexedThrough: "",
|
|
2465
|
+
indexedTotal: totalCommits,
|
|
2466
|
+
queryMs: Date.now() - start,
|
|
2467
|
+
reason: `repo has ${totalCommits} commits; blastradius requires >= ${EMPTY_REPO_THRESHOLD}`
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
const fresh = ensureFresh(this.db, this.repoPath);
|
|
2471
|
+
let status = "ok";
|
|
2472
|
+
if (fresh.rewritten) {
|
|
2473
|
+
status = "reindexing";
|
|
2474
|
+
this.log.warn("history_rewritten", { last_indexed: fresh.head });
|
|
2475
|
+
}
|
|
2476
|
+
if (fresh.tazelik === "bootstrapping") {
|
|
2477
|
+
await this.bootstrapIfNeeded();
|
|
2478
|
+
} else if (fresh.tazelik === "catching_up" && fresh.delta) {
|
|
2479
|
+
this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range: fresh.delta }).catch((err) => {
|
|
2480
|
+
this.log.error("delta_ingest_failed", { message: err.message });
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
if (status === "ok" && detectSquashed(this.db)) {
|
|
2484
|
+
status = "squashed_history";
|
|
2485
|
+
}
|
|
2486
|
+
const indexedTotalRow = this.db.prepare("SELECT value FROM index_state WHERE key='indexed_commits_total'").get();
|
|
2487
|
+
const indexedTotal = indexedTotalRow ? parseInt(indexedTotalRow.value, 10) : 0;
|
|
2488
|
+
const indexedThrough = this.db.prepare("SELECT value FROM index_state WHERE key='last_indexed_sha'").get()?.value ?? "";
|
|
2489
|
+
const signals = collectSignals(this.db, this.repoPath, input.file);
|
|
2490
|
+
const tazelik = fresh.tazelik === "bootstrapping" ? "fresh" : fresh.tazelik;
|
|
2491
|
+
const { score, confidence } = computeScoreAndConfidence(signals, {
|
|
2492
|
+
tazelik,
|
|
2493
|
+
partial: false,
|
|
2494
|
+
totalCommits: indexedTotal
|
|
2495
|
+
});
|
|
2496
|
+
const response = buildEnvelope({
|
|
2497
|
+
status,
|
|
2498
|
+
signals,
|
|
2499
|
+
score,
|
|
2500
|
+
confidence,
|
|
2501
|
+
tazelik,
|
|
2502
|
+
indexedThrough,
|
|
2503
|
+
indexedTotal,
|
|
2504
|
+
queryMs: Date.now() - start
|
|
2505
|
+
});
|
|
2506
|
+
this.log.info("query", {
|
|
2507
|
+
file: input.file,
|
|
2508
|
+
status: response.status,
|
|
2509
|
+
verdict: response.verdict,
|
|
2510
|
+
confidence: response.confidence,
|
|
2511
|
+
query_ms: response.metadata.query_ms
|
|
2512
|
+
});
|
|
2513
|
+
this.failures.recordSuccess();
|
|
2514
|
+
return response;
|
|
2515
|
+
}
|
|
2516
|
+
async close() {
|
|
2517
|
+
this.log.info("api_close", {});
|
|
2518
|
+
this.log.close();
|
|
2519
|
+
this.db.close();
|
|
2520
|
+
await this.pool.close();
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
|
|
2524
|
+
// src/memory/status.ts
|
|
2525
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
2526
|
+
function collectStatus(dbPath) {
|
|
2527
|
+
const db = openDatabase(dbPath);
|
|
2528
|
+
try {
|
|
2529
|
+
const schemaVersion = db.pragma("user_version", { simple: true });
|
|
2530
|
+
const totalRow = db.prepare(
|
|
2531
|
+
"SELECT value FROM index_state WHERE key='indexed_commits_total'"
|
|
2532
|
+
).get();
|
|
2533
|
+
const headRow = db.prepare(
|
|
2534
|
+
"SELECT value FROM index_state WHERE key='last_indexed_sha'"
|
|
2535
|
+
).get();
|
|
2536
|
+
const calRefreshRow = db.prepare(
|
|
2537
|
+
"SELECT value FROM index_state WHERE key='calibration_last_refreshed_at'"
|
|
2538
|
+
).get();
|
|
2539
|
+
const filesWithDeepIndex = db.prepare("SELECT COUNT(*) AS n FROM file_index_state").get().n;
|
|
2540
|
+
const calibrationRows = db.prepare("SELECT COUNT(*) AS n FROM signal_calibration").get().n;
|
|
2541
|
+
const storageBytes = statFileSize(dbPath) + statFileSize(dbPath + "-wal") + statFileSize(dbPath + "-shm");
|
|
2542
|
+
const integrityOk = db.prepare("PRAGMA integrity_check").get().integrity_check === "ok";
|
|
2543
|
+
return {
|
|
2544
|
+
schemaVersion,
|
|
2545
|
+
bootstrapped: !!headRow,
|
|
2546
|
+
indexedCommitsThrough: headRow?.value ?? "",
|
|
2547
|
+
indexedCommitsTotal: totalRow ? parseInt(totalRow.value, 10) : 0,
|
|
2548
|
+
filesWithDeepIndex,
|
|
2549
|
+
calibrationLastRefreshedAt: calRefreshRow ? parseInt(calRefreshRow.value, 10) : null,
|
|
2550
|
+
calibrationRows,
|
|
2551
|
+
storageBytes,
|
|
2552
|
+
integrityOk
|
|
2553
|
+
};
|
|
2554
|
+
} finally {
|
|
2555
|
+
db.close();
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function statFileSize(path) {
|
|
2559
|
+
try {
|
|
2560
|
+
return existsSync3(path) ? statSync2(path).size : 0;
|
|
2561
|
+
} catch {
|
|
2562
|
+
return 0;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/cli/commands.ts
|
|
201
2567
|
function runScan(projectPath) {
|
|
202
2568
|
const adapter = new CLIAdapter();
|
|
203
2569
|
const config = loadConfig(projectPath);
|
|
204
|
-
console.log("composto v0.
|
|
2570
|
+
console.log("composto v0.4.1 \u2014 scanning...\n");
|
|
205
2571
|
const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
|
|
206
2572
|
console.log(` Found ${files.length} files
|
|
207
2573
|
`);
|
|
208
2574
|
const allFindings = [];
|
|
209
2575
|
for (const file of files) {
|
|
210
|
-
const code =
|
|
211
|
-
const relPath =
|
|
2576
|
+
const code = readFileSync4(file, "utf-8");
|
|
2577
|
+
const relPath = relative2(projectPath, file);
|
|
212
2578
|
const findings = runDetector(code, relPath, config.watchers);
|
|
213
2579
|
allFindings.push(...findings);
|
|
214
2580
|
}
|
|
@@ -228,7 +2594,7 @@ function runScan(projectPath) {
|
|
|
228
2594
|
function runTrends(projectPath) {
|
|
229
2595
|
const adapter = new CLIAdapter();
|
|
230
2596
|
const config = loadConfig(projectPath);
|
|
231
|
-
console.log("composto v0.
|
|
2597
|
+
console.log("composto v0.4.1 \u2014 trend analysis...\n");
|
|
232
2598
|
const entries = getGitLog(projectPath, 100);
|
|
233
2599
|
if (entries.length === 0) {
|
|
234
2600
|
console.log(" No git history found.\n");
|
|
@@ -248,8 +2614,8 @@ function runTrends(projectPath) {
|
|
|
248
2614
|
}
|
|
249
2615
|
async function runIR(projectPath, filePath, layer) {
|
|
250
2616
|
const config = loadConfig(projectPath);
|
|
251
|
-
const code =
|
|
252
|
-
const relPath =
|
|
2617
|
+
const code = readFileSync4(filePath, "utf-8");
|
|
2618
|
+
const relPath = relative2(projectPath, filePath);
|
|
253
2619
|
const entries = getGitLog(projectPath, 100);
|
|
254
2620
|
const trends = {
|
|
255
2621
|
hotspots: detectHotspots(entries, {
|
|
@@ -270,14 +2636,14 @@ async function runIR(projectPath, filePath, layer) {
|
|
|
270
2636
|
}
|
|
271
2637
|
var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
|
|
272
2638
|
async function runBenchmark(projectPath) {
|
|
273
|
-
console.log("composto v0.
|
|
2639
|
+
console.log("composto v0.4.1 \u2014 benchmark\n");
|
|
274
2640
|
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
275
2641
|
console.log(` ${files.length} files
|
|
276
2642
|
`);
|
|
277
2643
|
const results = [];
|
|
278
2644
|
for (const file of files) {
|
|
279
|
-
const code =
|
|
280
|
-
const relPath =
|
|
2645
|
+
const code = readFileSync4(file, "utf-8");
|
|
2646
|
+
const relPath = relative2(projectPath, file);
|
|
281
2647
|
results.push(await benchmarkFile(code, relPath));
|
|
282
2648
|
}
|
|
283
2649
|
results.sort((a, b) => b.savedPercent - a.savedPercent);
|
|
@@ -315,9 +2681,9 @@ async function runBenchmarkQuality(projectPath, filePath) {
|
|
|
315
2681
|
console.error(" Error: ANTHROPIC_API_KEY environment variable is required.");
|
|
316
2682
|
process.exit(1);
|
|
317
2683
|
}
|
|
318
|
-
const code =
|
|
319
|
-
const relPath =
|
|
320
|
-
console.log("composto v0.
|
|
2684
|
+
const code = readFileSync4(filePath, "utf-8");
|
|
2685
|
+
const relPath = relative2(projectPath, filePath);
|
|
2686
|
+
console.log("composto v0.4.1 \u2014 quality benchmark\n");
|
|
321
2687
|
console.log(` File: ${relPath}
|
|
322
2688
|
`);
|
|
323
2689
|
console.log(" Sending to Claude Haiku...\n");
|
|
@@ -347,8 +2713,8 @@ ${result.ir.response}
|
|
|
347
2713
|
}
|
|
348
2714
|
}
|
|
349
2715
|
async function runContext(projectPath, budget, target) {
|
|
350
|
-
const header = target ? `composto v0.
|
|
351
|
-
` : `composto v0.
|
|
2716
|
+
const header = target ? `composto v0.4.1 \u2014 context (target: ${target}, budget: ${budget} tokens)
|
|
2717
|
+
` : `composto v0.4.1 \u2014 context (budget: ${budget} tokens)
|
|
352
2718
|
`;
|
|
353
2719
|
console.log(header);
|
|
354
2720
|
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
@@ -361,8 +2727,8 @@ async function runContext(projectPath, budget, target) {
|
|
|
361
2727
|
fixRatioThreshold: config.trends.bugFixRatioThreshold
|
|
362
2728
|
});
|
|
363
2729
|
const fileInputs = files.map((file) => {
|
|
364
|
-
const code =
|
|
365
|
-
const relPath =
|
|
2730
|
+
const code = readFileSync4(file, "utf-8");
|
|
2731
|
+
const relPath = relative2(projectPath, file);
|
|
366
2732
|
return { path: relPath, code, rawTokens: estimateTokens(code) };
|
|
367
2733
|
});
|
|
368
2734
|
const result = await packContext(fileInputs, { budget, hotspots, target });
|
|
@@ -416,19 +2782,88 @@ async function runContext(projectPath, budget, target) {
|
|
|
416
2782
|
console.log(` Budget: ${result.totalTokens}/${result.budget} tokens`);
|
|
417
2783
|
console.log(` Files: ${parts.join(", ")}`);
|
|
418
2784
|
}
|
|
2785
|
+
async function runImpact(projectPath, file, opts = {}) {
|
|
2786
|
+
const dbPath = join6(projectPath, ".composto", "memory.db");
|
|
2787
|
+
const api = new MemoryAPI({ dbPath, repoPath: projectPath });
|
|
2788
|
+
try {
|
|
2789
|
+
await api.bootstrapIfNeeded();
|
|
2790
|
+
const res = await api.blastradius({
|
|
2791
|
+
file,
|
|
2792
|
+
intent: opts.intent,
|
|
2793
|
+
level: opts.level
|
|
2794
|
+
});
|
|
2795
|
+
if (res.status !== "ok") {
|
|
2796
|
+
console.log(`status: ${res.status}`);
|
|
2797
|
+
if (res.reason) console.log(`reason: ${res.reason}`);
|
|
2798
|
+
console.log(`verdict: ${res.verdict}`);
|
|
2799
|
+
console.log(`confidence: ${res.confidence.toFixed(2)}`);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
console.log(`verdict: ${res.verdict}`);
|
|
2803
|
+
console.log(`score: ${res.score.toFixed(2)}`);
|
|
2804
|
+
console.log(`confidence: ${res.confidence.toFixed(2)}`);
|
|
2805
|
+
console.log(`tazelik: ${res.metadata.tazelik}`);
|
|
2806
|
+
console.log(`signals:`);
|
|
2807
|
+
for (const s of res.signals) {
|
|
2808
|
+
const bar = s.strength > 0 ? "\u25A0".repeat(Math.max(1, Math.round(s.strength * 10))) : "\xB7";
|
|
2809
|
+
console.log(` ${s.type.padEnd(18)} ${bar.padEnd(10)} strength=${s.strength.toFixed(2)} precision=${s.precision.toFixed(2)}`);
|
|
2810
|
+
}
|
|
2811
|
+
} finally {
|
|
2812
|
+
await api.close();
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
async function runIndex(projectPath) {
|
|
2816
|
+
const dbPath = join6(projectPath, ".composto", "memory.db");
|
|
2817
|
+
const api = new MemoryAPI({ dbPath, repoPath: projectPath });
|
|
2818
|
+
try {
|
|
2819
|
+
console.log("composto: bootstrapping memory index...");
|
|
2820
|
+
const start = Date.now();
|
|
2821
|
+
await api.bootstrapIfNeeded();
|
|
2822
|
+
console.log(`composto: index ready (${Date.now() - start} ms)`);
|
|
2823
|
+
} finally {
|
|
2824
|
+
await api.close();
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
async function runIndexStatus(projectPath) {
|
|
2828
|
+
const dbPath = join6(projectPath, ".composto", "memory.db");
|
|
2829
|
+
const s = collectStatus(dbPath);
|
|
2830
|
+
console.log(`Composto Memory \u2014 ${projectPath}
|
|
2831
|
+
`);
|
|
2832
|
+
console.log("Index state");
|
|
2833
|
+
console.log(` Schema version: ${s.schemaVersion}`);
|
|
2834
|
+
console.log(` Bootstrapped: ${s.bootstrapped ? "yes" : "no"}`);
|
|
2835
|
+
console.log(` Indexed through: ${s.indexedCommitsThrough || "(none)"}`);
|
|
2836
|
+
console.log(` Indexed commits total: ${s.indexedCommitsTotal}`);
|
|
2837
|
+
console.log(` Files w/ deep index: ${s.filesWithDeepIndex}`);
|
|
2838
|
+
console.log();
|
|
2839
|
+
console.log("Calibration");
|
|
2840
|
+
if (s.calibrationLastRefreshedAt) {
|
|
2841
|
+
const dt = new Date(s.calibrationLastRefreshedAt * 1e3).toISOString();
|
|
2842
|
+
console.log(` Last refreshed: ${dt}`);
|
|
2843
|
+
} else {
|
|
2844
|
+
console.log(` Last refreshed: (never)`);
|
|
2845
|
+
}
|
|
2846
|
+
console.log(` Rows populated: ${s.calibrationRows} / 5`);
|
|
2847
|
+
console.log();
|
|
2848
|
+
console.log("Storage");
|
|
2849
|
+
console.log(` DB + WAL + SHM: ${(s.storageBytes / 1024).toFixed(1)} KB`);
|
|
2850
|
+
console.log();
|
|
2851
|
+
console.log("Health");
|
|
2852
|
+
console.log(` Integrity check: ${s.integrityOk ? "OK" : "FAIL"}`);
|
|
2853
|
+
}
|
|
419
2854
|
|
|
420
2855
|
// src/index.ts
|
|
421
|
-
import { resolve } from "path";
|
|
2856
|
+
import { resolve as resolve2 } from "path";
|
|
422
2857
|
var args = process.argv.slice(2);
|
|
423
2858
|
var command = args[0];
|
|
424
2859
|
switch (command) {
|
|
425
2860
|
case "scan": {
|
|
426
|
-
const projectPath =
|
|
2861
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
427
2862
|
runScan(projectPath);
|
|
428
2863
|
break;
|
|
429
2864
|
}
|
|
430
2865
|
case "trends": {
|
|
431
|
-
const projectPath =
|
|
2866
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
432
2867
|
runTrends(projectPath);
|
|
433
2868
|
break;
|
|
434
2869
|
}
|
|
@@ -439,11 +2874,11 @@ switch (command) {
|
|
|
439
2874
|
console.error("Usage: composto ir <file> [L0|L1|L2|L3]");
|
|
440
2875
|
process.exit(1);
|
|
441
2876
|
}
|
|
442
|
-
await runIR(
|
|
2877
|
+
await runIR(resolve2("."), resolve2(filePath), layer);
|
|
443
2878
|
break;
|
|
444
2879
|
}
|
|
445
2880
|
case "benchmark": {
|
|
446
|
-
const projectPath =
|
|
2881
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
447
2882
|
await runBenchmark(projectPath);
|
|
448
2883
|
break;
|
|
449
2884
|
}
|
|
@@ -453,7 +2888,7 @@ switch (command) {
|
|
|
453
2888
|
console.error("Usage: composto benchmark-quality <file>");
|
|
454
2889
|
process.exit(1);
|
|
455
2890
|
}
|
|
456
|
-
await runBenchmarkQuality(
|
|
2891
|
+
await runBenchmarkQuality(resolve2("."), resolve2(filePath));
|
|
457
2892
|
break;
|
|
458
2893
|
}
|
|
459
2894
|
case "context": {
|
|
@@ -465,18 +2900,40 @@ switch (command) {
|
|
|
465
2900
|
return void 0;
|
|
466
2901
|
};
|
|
467
2902
|
parseFlag2 = parseFlag;
|
|
468
|
-
const projectPath =
|
|
2903
|
+
const projectPath = resolve2(args[1] && !args[1].startsWith("--") ? args[1] : ".");
|
|
469
2904
|
const budgetStr = parseFlag("budget");
|
|
470
2905
|
const budget = budgetStr ? parseInt(budgetStr, 10) : 4e3;
|
|
471
2906
|
const target = parseFlag("target");
|
|
472
2907
|
await runContext(projectPath, budget, target);
|
|
473
2908
|
break;
|
|
474
2909
|
}
|
|
2910
|
+
case "impact": {
|
|
2911
|
+
const filePath = args[1];
|
|
2912
|
+
if (!filePath) {
|
|
2913
|
+
console.error("Usage: composto impact <file> [--intent=bugfix] [--level=detail]");
|
|
2914
|
+
process.exit(1);
|
|
2915
|
+
}
|
|
2916
|
+
const intentArg = args.find((a) => a.startsWith("--intent="));
|
|
2917
|
+
const levelArg = args.find((a) => a.startsWith("--level="));
|
|
2918
|
+
await runImpact(resolve2("."), filePath, {
|
|
2919
|
+
intent: intentArg?.slice("--intent=".length),
|
|
2920
|
+
level: levelArg?.slice("--level=".length)
|
|
2921
|
+
});
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
case "index": {
|
|
2925
|
+
if (args.includes("--status")) {
|
|
2926
|
+
await runIndexStatus(resolve2("."));
|
|
2927
|
+
} else {
|
|
2928
|
+
await runIndex(resolve2("."));
|
|
2929
|
+
}
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
475
2932
|
case "version":
|
|
476
|
-
console.log("composto v0.
|
|
2933
|
+
console.log("composto v0.4.1");
|
|
477
2934
|
break;
|
|
478
2935
|
default:
|
|
479
|
-
console.log("composto v0.
|
|
2936
|
+
console.log("composto v0.4.1 \u2014 less tokens, more insight\n");
|
|
480
2937
|
console.log("Commands:");
|
|
481
2938
|
console.log(" scan [path] Scan codebase for issues");
|
|
482
2939
|
console.log(" trends [path] Analyze codebase health trends");
|
|
@@ -485,6 +2942,9 @@ switch (command) {
|
|
|
485
2942
|
console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
|
|
486
2943
|
console.log(" context [path] --budget N Smart context within token budget");
|
|
487
2944
|
console.log(" context [path] --target <symbol> Target file as raw, surrounding as IR");
|
|
2945
|
+
console.log(" impact <file> Show historical blast radius for a file");
|
|
2946
|
+
console.log(" index Build or refresh the memory index");
|
|
2947
|
+
console.log(" index --status Show memory index diagnostics");
|
|
488
2948
|
console.log(" version Show version");
|
|
489
2949
|
break;
|
|
490
2950
|
}
|