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.
@@ -1,1531 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/config/loader.ts
4
- import { readFileSync, existsSync } from "fs";
5
- import { join } from "path";
6
- import { parse } from "yaml";
7
- var DEFAULT_CONFIG = {
8
- watchers: {
9
- security: {
10
- enabled: true,
11
- severity: { "src/**": "warning", "tests/**": "info" }
12
- },
13
- deadCode: { enabled: true, trigger: "on-commit" },
14
- consoleLog: { enabled: true, severity: { "src/**": "warning", "tests/**": "info" } }
15
- },
16
- agents: {
17
- fixer: { enabled: true, model: "haiku" },
18
- reviewer: { enabled: false, model: "sonnet" }
19
- },
20
- ir: {
21
- deltaContextLines: 3,
22
- confidenceThreshold: 0.6,
23
- genericPatterns: "default"
24
- },
25
- trends: {
26
- enabled: true,
27
- hotspotThreshold: 10,
28
- bugFixRatioThreshold: 0.5,
29
- decayCheckTrigger: "on-commit",
30
- fullReportSchedule: "weekly"
31
- }
32
- };
33
- function deepMerge(target, source) {
34
- const result = { ...target };
35
- for (const key of Object.keys(source)) {
36
- if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
37
- result[key] = deepMerge(target[key] ?? {}, source[key]);
38
- } else {
39
- result[key] = source[key];
40
- }
41
- }
42
- return result;
43
- }
44
- function parseConfig(yamlContent) {
45
- if (!yamlContent.trim()) return { ...DEFAULT_CONFIG };
46
- const parsed = parse(yamlContent) ?? {};
47
- return deepMerge(DEFAULT_CONFIG, parsed);
48
- }
49
- function loadConfig(projectPath) {
50
- const configPath = join(projectPath, ".composto", "config.yaml");
51
- if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
52
- const content = readFileSync(configPath, "utf-8");
53
- return parseConfig(content);
54
- }
55
-
56
- // src/watcher/detector.ts
57
- import picomatch from "picomatch";
58
- function getSeverity(filePath, severityMap) {
59
- for (const [glob, severity] of Object.entries(severityMap)) {
60
- if (picomatch.isMatch(filePath, glob)) return severity;
61
- }
62
- return "info";
63
- }
64
- var SECRET_PATTERNS = [
65
- /["'](sk-[a-zA-Z0-9-]{20,})["']/,
66
- /["'](AKIA[0-9A-Z]{16})["']/,
67
- /["'](ghp_[a-zA-Z0-9]{36})["']/,
68
- /(?:password|secret|token|api_?key)\s*[:=]\s*["']([^"']{8,})["']/i
69
- ];
70
- function securityRule(code, filePath, severityMap) {
71
- const findings = [];
72
- const severity = getSeverity(filePath, severityMap);
73
- const lines = code.split("\n");
74
- for (let i = 0; i < lines.length; i++) {
75
- const line = lines[i];
76
- if (line.trim().startsWith("//") || line.trim().startsWith("/*")) continue;
77
- for (const pattern of SECRET_PATTERNS) {
78
- if (pattern.test(line)) {
79
- findings.push({
80
- watcherId: "security",
81
- severity,
82
- file: filePath,
83
- line: i + 1,
84
- message: "Potential hardcoded secret detected",
85
- action: {
86
- type: "agent-required",
87
- agentHint: { role: "fixer", model: "haiku", contextFiles: [filePath] }
88
- }
89
- });
90
- break;
91
- }
92
- }
93
- }
94
- return findings;
95
- }
96
- function consoleLogRule(code, filePath, severityMap) {
97
- const findings = [];
98
- const severity = getSeverity(filePath, severityMap);
99
- const lines = code.split("\n");
100
- for (let i = 0; i < lines.length; i++) {
101
- if (/\bconsole\.(log|debug|info|warn)\b/.test(lines[i])) {
102
- findings.push({
103
- watcherId: "consoleLog",
104
- severity,
105
- file: filePath,
106
- line: i + 1,
107
- message: "console.log detected \u2014 likely debug artifact",
108
- action: { type: "auto-fix", autoFix: "remove-line" }
109
- });
110
- }
111
- }
112
- return findings;
113
- }
114
- var RULES = {
115
- security: securityRule,
116
- consoleLog: consoleLogRule
117
- };
118
- function runDetector(code, filePath, watcherConfigs) {
119
- const findings = [];
120
- for (const [name, config] of Object.entries(watcherConfigs)) {
121
- if (!config.enabled) continue;
122
- const rule = RULES[name];
123
- if (rule && config.severity) {
124
- findings.push(...rule(code, filePath, config.severity));
125
- }
126
- }
127
- return findings;
128
- }
129
-
130
- // src/ir/health.ts
131
- var CHURN_THRESHOLD = 10;
132
- var FIX_RATIO_THRESHOLD = 0.5;
133
- function buildHealthTag(health) {
134
- const parts = [];
135
- if (health.churn > CHURN_THRESHOLD) parts.push(`HOT:${health.churn}/30`);
136
- if (health.fixRatio > FIX_RATIO_THRESHOLD) parts.push(`FIX:${Math.round(health.fixRatio * 100)}%`);
137
- if (health.coverageTrend === "down") parts.push("COV:\u2193");
138
- if (health.consistency === "low") parts.push("INCON");
139
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
140
- }
141
- function annotateIR(ir, health) {
142
- const tag = buildHealthTag(health);
143
- if (!tag) return ir;
144
- const lines = ir.split("\n");
145
- lines[0] = `${lines[0]} ${tag}`;
146
- return lines.join("\n");
147
- }
148
- function computeHealthFromTrends(file, trends) {
149
- const hotspot = trends.hotspots.find((h) => h.file === file);
150
- const decay = trends.decaySignals.find((d) => d.file === file);
151
- const inconsistency = trends.inconsistencies.find((i) => i.file === file);
152
- return {
153
- churn: hotspot?.changesInLast30Commits ?? 0,
154
- fixRatio: hotspot?.bugFixRatio ?? 0,
155
- coverageTrend: decay?.trend === "declining" ? "down" : decay?.trend === "improving" ? "up" : "stable",
156
- staleness: "",
157
- authorCount: hotspot?.authorCount ?? 0,
158
- consistency: inconsistency ? "low" : "high"
159
- };
160
- }
161
-
162
- // src/ir/structure.ts
163
- var CLASSIFIERS = [
164
- [/^(function|def|fn|func)\b/, "function-start"],
165
- [/^(class|struct|interface)\b/, "type-start"],
166
- [/^(if|else|elif|switch|match|case)\b/, "branch"],
167
- [/^(for|while|loop|do)\b/, "loop"],
168
- [/^(return|yield)\b/, "exit"],
169
- [/^(import|require|use|from)\b/, "import"],
170
- [/^(export|pub|public)\b/, "export"],
171
- [/^(const|let|var|val|mut)\b/, "assignment"],
172
- [/^(try|catch|except|finally)\b/, "error-handling"],
173
- [/^(async|await)\b/, "async"],
174
- [/^(\/\/|\/\*|#)/, "comment"]
175
- ];
176
- function classifyLine(firstToken) {
177
- if (firstToken === "") return "blank";
178
- for (const [pattern, type] of CLASSIFIERS) {
179
- if (pattern.test(firstToken)) return type;
180
- }
181
- return "unknown";
182
- }
183
- function extractStructure(code) {
184
- const lines = code.split("\n");
185
- return lines.map((raw, i) => {
186
- const indent = raw.search(/\S/);
187
- const trimmed = raw.trim();
188
- const firstToken = trimmed.split(/[\s({<]/)[0];
189
- const type = classifyLine(firstToken);
190
- return {
191
- line: i + 1,
192
- indent: indent === -1 ? -1 : indent,
193
- type,
194
- raw
195
- };
196
- });
197
- }
198
-
199
- // src/ir/fingerprint.ts
200
- var PATTERNS = [
201
- // import type { x, y } from "module"
202
- {
203
- match: /^import\s+type\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
204
- transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
205
- confidence: 0.95
206
- },
207
- // import { x, y } from "module"
208
- {
209
- match: /^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
210
- transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
211
- confidence: 0.95
212
- },
213
- // import type x from "module"
214
- {
215
- match: /^import\s+type\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
216
- transform: (m) => `USE:${m[2]}{${m[1]}}`,
217
- confidence: 0.95
218
- },
219
- // import x from "module"
220
- {
221
- match: /^import\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
222
- transform: (m) => `USE:${m[2]}{${m[1]}}`,
223
- confidence: 0.95
224
- },
225
- // const x = require("module")
226
- {
227
- match: /^(?:const|let|var)\s+(\w+)\s*=\s*require\(["']([^"']+)["']\);?\s*$/,
228
- transform: (m) => `USE:${m[2]}{${m[1]}}`,
229
- confidence: 0.95
230
- },
231
- // export function name(params) {
232
- {
233
- match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
234
- transform: (m) => `OUT FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
235
- confidence: 0.95
236
- },
237
- // function name(params) {
238
- {
239
- match: /^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
240
- transform: (m) => `FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
241
- confidence: 0.95
242
- },
243
- // export class Name extends Base {
244
- {
245
- match: /^export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
246
- transform: (m) => `OUT CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
247
- confidence: 0.95
248
- },
249
- // class Name extends Base {
250
- {
251
- match: /^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
252
- transform: (m) => `CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
253
- confidence: 0.95
254
- },
255
- // if (cond) return expr;
256
- {
257
- match: /^if\s*\(([^)]+)\)\s*return\s+(.+);?\s*$/,
258
- transform: (m) => `IF:${m[1].trim()} -> RET ${m[2].trim().replace(/;$/, "")}`,
259
- confidence: 0.95
260
- },
261
- // if (cond) {
262
- {
263
- match: /^if\s*\(([^)]+)\)\s*\{?\s*$/,
264
- transform: (m) => `IF:${m[1].trim()}`,
265
- confidence: 0.9
266
- },
267
- // for (... of/in ...) {
268
- {
269
- match: /^for\s*\((?:const|let|var)\s+(\w+)\s+(?:of|in)\s+(\w+)\)\s*\{?\s*$/,
270
- transform: (m) => `LOOP:${m[2]} -> ${m[1]}`,
271
- confidence: 0.9
272
- },
273
- // return expr
274
- {
275
- match: /^return\s+(.+);?\s*$/,
276
- transform: (m) => `RET ${m[1].trim().replace(/;$/, "")}`,
277
- confidence: 0.95
278
- },
279
- // return;
280
- {
281
- match: /^return;?\s*$/,
282
- transform: () => "RET",
283
- confidence: 0.95
284
- },
285
- // try {
286
- {
287
- match: /^try\s*\{\s*$/,
288
- transform: () => "TRY:",
289
- confidence: 0.9
290
- },
291
- // catch (e) {
292
- {
293
- match: /^(?:\}\s*)?catch\s*\((\w+)\)\s*\{?\s*$/,
294
- transform: (m) => `CATCH:${m[1]}`,
295
- confidence: 0.9
296
- },
297
- // switch (expr) {
298
- {
299
- match: /^switch\s*\(([^)]+)\)\s*\{?\s*$/,
300
- transform: (m) => `SWITCH:${m[1].trim()}`,
301
- confidence: 0.9
302
- },
303
- // case "value": / case value:
304
- {
305
- match: /^case\s+(.+)\s*:\s*$/,
306
- transform: (m) => `CASE:${m[1].trim()}`,
307
- confidence: 0.9
308
- },
309
- // default:
310
- {
311
- match: /^default\s*:\s*$/,
312
- transform: () => "DEFAULT:",
313
- confidence: 0.9
314
- },
315
- // export type Name = ...
316
- {
317
- match: /^export\s+type\s+(\w+)(?:<[^>]+>)?\s*=\s*(.+);?\s*$/,
318
- transform: (m) => `OUT TYPE:${m[1]}`,
319
- confidence: 0.9
320
- },
321
- // if (cond) expr; (inline if with method call)
322
- {
323
- match: /^if\s*\(([^)]+)\)\s+(\w+.+);?\s*$/,
324
- transform: (m) => `IF:${m[1].trim()} -> ${m[2].replace(/;$/, "").trim().slice(0, 50)}`,
325
- confidence: 0.9
326
- },
327
- // export async function name( (multiline signature)
328
- {
329
- match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(\s*$/,
330
- transform: (m) => `OUT FN:${m[1]}(`,
331
- confidence: 0.95
332
- },
333
- // interface/type property — name: type;
334
- {
335
- match: /^\s*(\w+)\??\s*:\s*(.+);?\s*$/,
336
- transform: (m) => `PROP:${m[1]}: ${m[2].replace(/;$/, "").trim()}`,
337
- confidence: 0.75
338
- },
339
- // const x = await expr;
340
- {
341
- match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*await\s+(.+);?\s*$/,
342
- transform: (m) => {
343
- const prefix = m[0].startsWith("export") ? "OUT " : "";
344
- return `${prefix}AWAIT:VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
345
- },
346
- confidence: 0.85
347
- },
348
- // export const name = async (params) => { OR export const name = (params) => expr;
349
- {
350
- match: /^export\s+(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
351
- transform: (m) => {
352
- const asyncPrefix = m[2] ? "ASYNC " : "";
353
- const body = m[4].replace(/[{;]\s*$/, "").trim();
354
- return `OUT ${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
355
- },
356
- confidence: 0.9
357
- },
358
- // const name = async (params) => { OR const name = (params) => expr;
359
- {
360
- match: /^(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
361
- transform: (m) => {
362
- const asyncPrefix = m[2] ? "ASYNC " : "";
363
- const body = m[4].replace(/[{;]\s*$/, "").trim();
364
- return `${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
365
- },
366
- confidence: 0.9
367
- },
368
- // get name() {
369
- {
370
- match: /^\s*get\s+(\w+)\s*\(\)\s*(?::\s*\S+\s*)?\{?\s*$/,
371
- transform: (m) => `GET:${m[1]}()`,
372
- confidence: 0.9
373
- },
374
- // set name(value) {
375
- {
376
- match: /^\s*set\s+(\w+)\s*\(([^)]*)\)\s*\{?\s*$/,
377
- transform: (m) => `SET:${m[1]}(${m[2].replace(/\s/g, "")})`,
378
- confidence: 0.9
379
- },
380
- // methodName(params) { (inside class body, indented)
381
- {
382
- match: /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{\s*$/,
383
- transform: (m) => {
384
- const name = m[1];
385
- if (["if", "for", "while", "switch", "catch", "function"].includes(name)) return `${name}`;
386
- return `METHOD:${name}(${m[2].replace(/\s/g, "")})`;
387
- },
388
- confidence: 0.9
389
- },
390
- // const { a, b } = expr (object destructuring — before regular assignment)
391
- {
392
- match: /^(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(.+);?\s*$/,
393
- transform: (m) => `VAR:{${m[1].replace(/\s/g, "")}} = ${m[2].replace(/;$/, "").trim()}`,
394
- confidence: 0.9
395
- },
396
- // const [a, b] = expr (destructuring — before regular assignment)
397
- {
398
- match: /^(?:const|let|var)\s+\[([^\]]+)\]\s*=\s*(.+);?\s*$/,
399
- transform: (m) => `VAR:[${m[1].replace(/\s/g, "")}] = ${m[2].replace(/;$/, "").trim()}`,
400
- confidence: 0.9
401
- },
402
- // const name = value;
403
- {
404
- match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+);?\s*$/,
405
- transform: (m) => {
406
- const prefix = m[0].startsWith("export") ? "OUT " : "";
407
- return `${prefix}VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
408
- },
409
- confidence: 0.85
410
- }
411
- ];
412
- function fingerprintLine(line) {
413
- const trimmed = line.trim();
414
- if (trimmed === "" || trimmed === "{" || trimmed === "}" || trimmed === "});" || trimmed === ");") {
415
- return { type: "fingerprint", ir: "", confidence: 1 };
416
- }
417
- if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
418
- return { type: "fingerprint", ir: "", confidence: 1 };
419
- }
420
- for (const pattern of PATTERNS) {
421
- const match = trimmed.match(pattern.match);
422
- if (match) {
423
- const ir = pattern.transform(match);
424
- if (pattern.confidence > 0.9) {
425
- return { type: "fingerprint", ir, confidence: pattern.confidence };
426
- }
427
- return {
428
- type: "fingerprint+hint",
429
- ir,
430
- hint: trimmed,
431
- confidence: pattern.confidence
432
- };
433
- }
434
- }
435
- return { type: "raw", ir: trimmed, confidence: 0.1 };
436
- }
437
- function fingerprintFile(code, confidenceThreshold = 0.6) {
438
- const lines = code.split("\n");
439
- const irLines = [];
440
- for (const line of lines) {
441
- const indent = line.search(/\S/);
442
- const indentStr = indent > 0 ? " ".repeat(Math.floor(indent / 2)) : "";
443
- const result = fingerprintLine(line);
444
- if (result.ir === "") continue;
445
- if (result.confidence >= confidenceThreshold) {
446
- irLines.push(`${indentStr}${result.ir}`);
447
- }
448
- }
449
- return irLines.join("\n");
450
- }
451
-
452
- // src/parser/init.ts
453
- import { Parser, Language } from "web-tree-sitter";
454
- import { resolve, dirname } from "path";
455
- import { existsSync as existsSync2 } from "fs";
456
- import { fileURLToPath } from "url";
457
- var __dirname = dirname(fileURLToPath(import.meta.url));
458
- var initialized = false;
459
- var cache = /* @__PURE__ */ new Map();
460
- function grammarPath(lang) {
461
- const distPath = resolve(__dirname, "grammars", `tree-sitter-${lang}.wasm`);
462
- if (existsSync2(distPath)) return distPath;
463
- const devPath = resolve(__dirname, "../../grammars", `tree-sitter-${lang}.wasm`);
464
- if (existsSync2(devPath)) return devPath;
465
- throw new Error(`Grammar not found for ${lang}`);
466
- }
467
- async function getParser(lang) {
468
- if (!initialized) {
469
- await Parser.init();
470
- initialized = true;
471
- }
472
- const cached = cache.get(lang);
473
- if (cached) return cached;
474
- const parser = new Parser();
475
- const language = await Language.load(grammarPath(lang));
476
- parser.setLanguage(language);
477
- const result = { parser, language };
478
- cache.set(lang, result);
479
- return result;
480
- }
481
-
482
- // src/parser/languages.ts
483
- import { extname } from "path";
484
- var EXT_MAP = {
485
- ".ts": "typescript",
486
- ".tsx": "typescript",
487
- ".js": "javascript",
488
- ".jsx": "javascript",
489
- ".mjs": "javascript",
490
- ".py": "python",
491
- ".go": "go",
492
- ".rs": "rust"
493
- };
494
- var SUPPORTED_EXTENSIONS = Object.keys(EXT_MAP);
495
- function detectLanguage(filePath) {
496
- const ext = extname(filePath);
497
- return EXT_MAP[ext] ?? null;
498
- }
499
-
500
- // src/ir/ast-walker.ts
501
- var TIER_MAP = {
502
- // Tier 1 — structural declarations (JS/TS)
503
- import_statement: "T1_KEEP",
504
- function_declaration: "T1_KEEP",
505
- class_declaration: "T1_KEEP",
506
- interface_declaration: "T1_KEEP",
507
- type_alias_declaration: "T1_KEEP",
508
- enum_declaration: "T1_KEEP",
509
- // Tier 1 — Python
510
- function_definition: "T1_KEEP",
511
- class_definition: "T1_KEEP",
512
- import_from_statement: "T1_KEEP",
513
- decorated_definition: "T1_KEEP",
514
- // Tier 1 — class members (qualified methods)
515
- method_definition: "T1_KEEP",
516
- // JS/TS class method
517
- public_field_definition: "T1_KEEP",
518
- // TS class property
519
- // Tier 1 — Go
520
- function_item: "T1_KEEP",
521
- // Rust
522
- method_declaration: "T1_KEEP",
523
- // Go
524
- type_declaration: "T1_KEEP",
525
- // Go
526
- import_declaration: "T1_KEEP",
527
- // Go
528
- use_declaration: "T1_KEEP",
529
- // Rust
530
- struct_item: "T1_KEEP",
531
- // Rust
532
- enum_item: "T1_KEEP",
533
- // Rust
534
- trait_item: "T1_KEEP",
535
- // Rust
536
- impl_item: "T1_KEEP",
537
- // Rust
538
- // Tier 2 — control flow (universal)
539
- if_statement: "T2_CONTROL",
540
- if_expression: "T2_CONTROL",
541
- // Rust
542
- else_clause: "WALK_ONLY",
543
- elif_clause: "T2_CONTROL",
544
- // Python
545
- for_statement: "T2_CONTROL",
546
- for_in_statement: "T2_CONTROL",
547
- for_expression: "T2_CONTROL",
548
- // Rust
549
- while_statement: "T2_CONTROL",
550
- do_statement: "T2_CONTROL",
551
- switch_statement: "T2_CONTROL",
552
- switch_case: "T2_CONTROL",
553
- switch_default: "T2_CONTROL",
554
- match_expression: "T2_CONTROL",
555
- // Rust
556
- return_statement: "T2_CONTROL",
557
- return_expression: "T2_CONTROL",
558
- // Rust
559
- throw_statement: "T2_CONTROL",
560
- raise_statement: "T2_CONTROL",
561
- // Python
562
- try_statement: "T2_CONTROL",
563
- catch_clause: "T2_CONTROL",
564
- except_clause: "T2_CONTROL",
565
- // Python
566
- with_statement: "T2_CONTROL",
567
- // Python
568
- defer_statement: "T2_CONTROL",
569
- // Go
570
- // Tier 3 — compressible expressions
571
- lexical_declaration: "T3_COMPRESS",
572
- expression_statement: "T3_COMPRESS",
573
- assignment: "T3_COMPRESS",
574
- // Python
575
- short_var_declaration: "T3_COMPRESS",
576
- // Go
577
- // Walk-only — containers
578
- program: "WALK_ONLY",
579
- module: "WALK_ONLY",
580
- // Python
581
- statement_block: "WALK_ONLY",
582
- block: "WALK_ONLY",
583
- // Python/Go/Rust
584
- class_body: "WALK_ONLY",
585
- // JS/TS
586
- switch_body: "WALK_ONLY",
587
- export_statement: "WALK_ONLY",
588
- source_file: "WALK_ONLY"
589
- // Go/Rust
590
- };
591
- function tierOf(nodeType) {
592
- return TIER_MAP[nodeType] ?? "T4_DROP";
593
- }
594
- function collapseText(text, maxLen) {
595
- const collapsed = text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
596
- if (collapsed.length <= maxLen) return collapsed;
597
- return collapsed.slice(0, maxLen - 3) + "...";
598
- }
599
- function getTypeParams(node) {
600
- for (let i = 0; i < node.childCount; i++) {
601
- const child = node.child(i);
602
- if (child.type === "type_parameters") {
603
- return child.text;
604
- }
605
- }
606
- return "";
607
- }
608
- function isExported(node) {
609
- return node.parent?.type === "export_statement";
610
- }
611
- function isAsync(node) {
612
- return node.text.trimStart().startsWith("async");
613
- }
614
- function extractDocComment(node) {
615
- let prev = node.previousNamedSibling;
616
- if (!prev && node.parent?.type === "export_statement") {
617
- prev = node.parent.previousNamedSibling;
618
- }
619
- if (!prev || prev.type !== "comment") return null;
620
- const text = prev.text;
621
- if (!text.startsWith("/**")) return null;
622
- const body = text.replace(/^\/\*\*|\*\/$/g, "").replace(/^\s*\*\s?/gm, "").trim();
623
- const tags = [];
624
- const tagMatches = body.matchAll(/@(\w+)(?:\s+([^\n@]+))?/g);
625
- for (const m of tagMatches) {
626
- const tag = m[1];
627
- const val = (m[2] ?? "").trim();
628
- if (tag === "deprecated") tags.push("@deprecated");
629
- else if (tag === "internal") tags.push("@internal");
630
- else if (tag === "throws" && val) tags.push(`@throws:${val.length > 30 ? val.slice(0, 27) + "..." : val}`);
631
- }
632
- const beforeTags = body.split(/@\w+/)[0].trim();
633
- const desc = beforeTags.split(/[.\n]/)[0].trim();
634
- const parts = [];
635
- if (tags.length > 0) parts.push(tags.join(" "));
636
- if (desc && desc.length > 3) {
637
- parts.push(`"${desc.length > 50 ? desc.slice(0, 47) + "..." : desc}"`);
638
- }
639
- return parts.length > 0 ? parts.join(" ") : null;
640
- }
641
- function extractPythonDocstring(bodyNode) {
642
- if (!bodyNode) return null;
643
- for (let i = 0; i < bodyNode.childCount; i++) {
644
- const child = bodyNode.child(i);
645
- if (child.type === "expression_statement" && child.childCount > 0) {
646
- const expr = child.child(0);
647
- if (expr.type === "string") {
648
- const text = expr.text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
649
- const firstLine = text.split("\n")[0].trim();
650
- return firstLine.length > 3 ? `"${firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine}"` : null;
651
- }
652
- break;
653
- }
654
- }
655
- return null;
656
- }
657
- function extractCondition(node) {
658
- const condNode = node.childForFieldName("condition") ?? (() => {
659
- for (let i = 0; i < node.childCount; i++) {
660
- const c = node.child(i);
661
- if (c.type === "parenthesized_expression") return c;
662
- }
663
- return null;
664
- })();
665
- if (!condNode) return "...";
666
- const text = condNode.text.replace(/^\(/, "").replace(/\)$/, "").trim();
667
- return text.length > 60 ? text.slice(0, 57) + "..." : text;
668
- }
669
- function emitTier2(node) {
670
- switch (node.type) {
671
- case "if_statement": {
672
- const cond = extractCondition(node);
673
- return `IF:${cond}`;
674
- }
675
- case "else_clause":
676
- return "ELSE:";
677
- case "for_statement":
678
- case "for_in_statement":
679
- return "LOOP";
680
- case "while_statement": {
681
- const cond = extractCondition(node);
682
- return `WHILE:${cond}`;
683
- }
684
- case "do_statement": {
685
- const cond = extractCondition(node);
686
- return `WHILE:${cond}`;
687
- }
688
- case "switch_statement": {
689
- const expr = node.childForFieldName("value") ?? node.childForFieldName("condition") ?? (() => {
690
- for (let i = 0; i < node.childCount; i++) {
691
- const c = node.child(i);
692
- if (c.type === "parenthesized_expression") return c;
693
- }
694
- return null;
695
- })();
696
- const text = expr ? expr.text.replace(/^\(/, "").replace(/\)$/, "").trim() : "...";
697
- return `SWITCH:${text.length > 60 ? text.slice(0, 57) + "..." : text}`;
698
- }
699
- case "switch_case": {
700
- let value = null;
701
- const valNode = node.childForFieldName("value");
702
- if (valNode) {
703
- value = valNode.text;
704
- } else {
705
- for (let i = 0; i < node.childCount; i++) {
706
- const c = node.child(i);
707
- if (c.type !== "case" && c.type !== ":" && c.childCount === 0 && c.text === "case") continue;
708
- if (c.type !== "case" && c.text !== "case" && c.text !== ":") {
709
- value = c.text;
710
- break;
711
- }
712
- }
713
- }
714
- return `CASE:${value ?? "..."}`;
715
- }
716
- case "switch_default":
717
- return "DEFAULT:";
718
- case "return_statement": {
719
- let retText = "";
720
- for (let i = 0; i < node.childCount; i++) {
721
- const c = node.child(i);
722
- if (c.text !== "return" && c.text !== ";") {
723
- retText += (retText ? " " : "") + c.text;
724
- }
725
- }
726
- retText = retText.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
727
- if (!retText) return "RET";
728
- return `RET ${retText.length > 60 ? retText.slice(0, 57) + "..." : retText}`;
729
- }
730
- case "throw_statement": {
731
- let throwText = "";
732
- for (let i = 0; i < node.childCount; i++) {
733
- const c = node.child(i);
734
- if (c.text !== "throw" && c.text !== ";") {
735
- throwText += (throwText ? " " : "") + c.text;
736
- }
737
- }
738
- throwText = throwText.trim();
739
- return `THROW:${throwText.length > 60 ? throwText.slice(0, 57) + "..." : throwText}`;
740
- }
741
- case "try_statement":
742
- return "TRY";
743
- case "catch_clause": {
744
- const param = node.childForFieldName("parameter");
745
- const paramText = param ? param.text : "...";
746
- return `CATCH:${paramText}`;
747
- }
748
- // Python
749
- case "raise_statement": {
750
- const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
751
- return `RAISE:${val.length > 50 ? val.slice(0, 47) + "..." : val}`;
752
- }
753
- case "except_clause":
754
- return "EXCEPT";
755
- case "elif_clause": {
756
- const cond = extractCondition(node);
757
- return `ELIF:${cond}`;
758
- }
759
- case "with_statement":
760
- return "WITH";
761
- // Rust
762
- case "if_expression": {
763
- const cond = extractCondition(node);
764
- return `IF:${cond}`;
765
- }
766
- case "for_expression":
767
- return "LOOP";
768
- case "match_expression":
769
- return "MATCH";
770
- case "return_expression": {
771
- const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
772
- return `RET ${val.length > 60 ? val.slice(0, 57) + "..." : val}`.trimEnd();
773
- }
774
- // Go
775
- case "defer_statement":
776
- return "DEFER";
777
- default:
778
- return null;
779
- }
780
- }
781
- function emitTier1(node) {
782
- const exported = isExported(node);
783
- const outPrefix = exported ? "OUT " : "";
784
- switch (node.type) {
785
- case "import_statement": {
786
- const text = collapseText(node.text, 80);
787
- return `USE:${text}`;
788
- }
789
- case "function_declaration": {
790
- const name = node.childForFieldName("name")?.text ?? "anonymous";
791
- const rawParams = node.childForFieldName("parameters")?.text ?? "()";
792
- const params = collapseText(rawParams, 60);
793
- const asyncPrefix = isAsync(node) ? "ASYNC " : "";
794
- const doc = extractDocComment(node);
795
- const docPrefix = doc ? `${doc} ` : "";
796
- return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
797
- }
798
- case "class_declaration": {
799
- const name = node.childForFieldName("name")?.text ?? "Anonymous";
800
- const typeParams = getTypeParams(node);
801
- const doc = extractDocComment(node);
802
- const docPrefix = doc ? `${doc} ` : "";
803
- return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
804
- }
805
- case "interface_declaration": {
806
- const name = node.childForFieldName("name")?.text ?? "Anonymous";
807
- const typeParams = getTypeParams(node);
808
- const doc = extractDocComment(node);
809
- const docPrefix = doc ? `${doc} ` : "";
810
- return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
811
- }
812
- case "type_alias_declaration": {
813
- const name = node.childForFieldName("name")?.text ?? "Anonymous";
814
- const doc = extractDocComment(node);
815
- const docPrefix = doc ? `${doc} ` : "";
816
- return `${docPrefix}${outPrefix}TYPE:${name}`;
817
- }
818
- case "enum_declaration": {
819
- const name = node.childForFieldName("name")?.text ?? "Anonymous";
820
- const doc = extractDocComment(node);
821
- const docPrefix = doc ? `${doc} ` : "";
822
- return `${docPrefix}${outPrefix}ENUM:${name}`;
823
- }
824
- case "method_definition": {
825
- let enclosingClass = null;
826
- let parent = node.parent;
827
- while (parent) {
828
- if (parent.type === "class_declaration" || parent.type === "class_definition") {
829
- enclosingClass = parent.childForFieldName("name")?.text ?? null;
830
- break;
831
- }
832
- parent = parent.parent;
833
- }
834
- const name = node.childForFieldName("name")?.text ?? "anonymous";
835
- const params = node.childForFieldName("parameters")?.text ?? "()";
836
- const asyncPrefix = isAsync(node) ? "ASYNC " : "";
837
- const doc = extractDocComment(node);
838
- const docPrefix = doc ? `${doc} ` : "";
839
- const qualifiedName = enclosingClass ? `${enclosingClass}.${name}` : name;
840
- return `${docPrefix}${asyncPrefix}METHOD:${qualifiedName}${collapseText(params, 60)}`;
841
- }
842
- case "public_field_definition": {
843
- const name = node.childForFieldName("name")?.text ?? "field";
844
- return `FIELD:${name}`;
845
- }
846
- // Python
847
- case "function_definition": {
848
- const name = node.childForFieldName("name")?.text ?? "anonymous";
849
- const params = node.childForFieldName("parameters")?.text ?? "()";
850
- const returnType = node.childForFieldName("return_type")?.text ?? "";
851
- const rt = returnType ? ` -> ${returnType}` : "";
852
- const body = node.childForFieldName("body");
853
- const doc = extractPythonDocstring(body);
854
- const docPrefix = doc ? `${doc} ` : "";
855
- return `${docPrefix}FN:${name}${collapseText(params, 60)}${rt}`;
856
- }
857
- case "class_definition": {
858
- const name = node.childForFieldName("name")?.text ?? "Anonymous";
859
- const superclass = node.childForFieldName("superclasses")?.text ?? "";
860
- const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
861
- const body = node.childForFieldName("body");
862
- const doc = extractPythonDocstring(body);
863
- const docPrefix = doc ? `${doc} ` : "";
864
- return `${docPrefix}CLASS:${name}${sc}`;
865
- }
866
- case "import_from_statement": {
867
- return `USE:${collapseText(node.text, 80)}`;
868
- }
869
- case "decorated_definition": {
870
- return null;
871
- }
872
- // Go
873
- case "method_declaration":
874
- case "type_declaration":
875
- case "import_declaration": {
876
- return `${collapseText(node.text, 80)}`;
877
- }
878
- // Rust
879
- case "function_item": {
880
- const name = node.childForFieldName("name")?.text ?? "anonymous";
881
- const params = node.childForFieldName("parameters")?.text ?? "()";
882
- return `FN:${name}${collapseText(params, 60)}`;
883
- }
884
- case "struct_item":
885
- case "enum_item":
886
- case "trait_item":
887
- case "impl_item":
888
- case "use_declaration": {
889
- const firstLine = node.text.split("\n")[0];
890
- return collapseText(firstLine, 80);
891
- }
892
- default:
893
- return null;
894
- }
895
- }
896
- function emitTier3(node) {
897
- switch (node.type) {
898
- case "lexical_declaration": {
899
- let declarator = null;
900
- for (let i = 0; i < node.childCount; i++) {
901
- const c = node.child(i);
902
- if (c.type === "variable_declarator") {
903
- declarator = c;
904
- break;
905
- }
906
- }
907
- if (!declarator) return null;
908
- const name = declarator.childForFieldName("name")?.text ?? "?";
909
- const value = declarator.childForFieldName("value");
910
- if (value) {
911
- if (value.type === "arrow_function") {
912
- const asyncPrefix = isAsync(value) ? "ASYNC " : "";
913
- const params = value.childForFieldName("parameters")?.text ?? "()";
914
- return `${asyncPrefix}FN:${name}${collapseText(params, 60)} => ...`;
915
- }
916
- if (value.type === "await_expression") {
917
- const callee = value.childCount > 1 ? value.child(1).text : "...";
918
- return `AWAIT:${name}=${collapseText(callee, 40)}`;
919
- }
920
- if (node.parent?.type === "statement_block") return null;
921
- const vt = value.type;
922
- if (vt === "number" || vt === "true" || vt === "false") return null;
923
- if (vt === "object" || vt === "array") return null;
924
- if (vt === "new_expression" || vt === "call_expression") return null;
925
- const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
926
- return `VAR:${name} = ${collapseText(valText, 50)}`;
927
- }
928
- return null;
929
- }
930
- case "expression_statement": {
931
- const expr = node.child(0);
932
- if (!expr) return null;
933
- if (expr.type === "await_expression") {
934
- return null;
935
- }
936
- if (expr.type === "call_expression") {
937
- const callee = expr.child(0)?.text ?? "";
938
- if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
939
- const args = expr.child(1);
940
- if (args && args.childCount >= 4) {
941
- const child = args.child(1)?.text ?? "?";
942
- const parent = args.child(3)?.text ?? "?";
943
- const shortChild = child.length > 30 ? child.slice(0, 27) + "..." : child;
944
- const shortParent = parent.length > 30 ? parent.slice(0, 27) + "..." : parent;
945
- return `EXTENDS:${shortChild} < ${shortParent}`;
946
- }
947
- }
948
- return null;
949
- }
950
- return null;
951
- }
952
- default:
953
- return null;
954
- }
955
- }
956
- function walkNode(node, depth, lines) {
957
- const tier = tierOf(node.type);
958
- switch (tier) {
959
- case "T1_KEEP": {
960
- const ir = emitTier1(node);
961
- if (ir) lines.push(ir);
962
- for (let i = 0; i < node.childCount; i++) {
963
- const child = node.child(i);
964
- const childType = child.type;
965
- if (childType === "statement_block" || childType === "class_body" || childType === "block" || // Python/Go/Rust
966
- childType === "body" || // Python class/function body
967
- childType === "declaration_list") {
968
- walkNode(child, depth + 1, lines);
969
- }
970
- }
971
- break;
972
- }
973
- case "T2_CONTROL": {
974
- if (depth > 4 && node.type !== "return_statement" && node.type !== "throw_statement" && node.type !== "switch_case" && node.type !== "switch_default") break;
975
- if (node.type === "if_statement") {
976
- let hasElse = false;
977
- for (let i = 0; i < node.childCount; i++) {
978
- if (node.child(i).type === "else_clause") {
979
- hasElse = true;
980
- break;
981
- }
982
- }
983
- if (!hasElse) {
984
- const body = node.childForFieldName("consequence") ?? (() => {
985
- for (let i = 0; i < node.childCount; i++) {
986
- const c = node.child(i);
987
- if (c.type === "statement_block") return c;
988
- }
989
- return null;
990
- })();
991
- if (body) {
992
- let singleStmt = null;
993
- if (body.type === "statement_block") {
994
- const stmts = [];
995
- for (let i = 0; i < body.childCount; i++) {
996
- const c = body.child(i);
997
- if (c.type !== "{" && c.type !== "}") stmts.push(c);
998
- }
999
- if (stmts.length === 1) singleStmt = stmts[0];
1000
- } else if (body.type === "return_statement" || body.type === "throw_statement") {
1001
- singleStmt = body;
1002
- }
1003
- if (singleStmt && (singleStmt.type === "return_statement" || singleStmt.type === "throw_statement")) {
1004
- const cond = extractCondition(node);
1005
- const retLine = emitTier2(singleStmt);
1006
- if (retLine) {
1007
- const indent2 = " ".repeat(depth);
1008
- lines.push(`${indent2}IF:${cond} \u2192 ${retLine}`);
1009
- break;
1010
- }
1011
- }
1012
- }
1013
- }
1014
- }
1015
- const line = emitTier2(node);
1016
- const indent = " ".repeat(depth);
1017
- if (line) lines.push(indent + line);
1018
- for (let i = 0; i < node.childCount; i++) {
1019
- walkNode(node.child(i), depth + 1, lines);
1020
- }
1021
- break;
1022
- }
1023
- case "T3_COMPRESS": {
1024
- if (depth > 4) break;
1025
- const line = emitTier3(node);
1026
- const indent = " ".repeat(depth);
1027
- if (line) lines.push(indent + line);
1028
- break;
1029
- }
1030
- case "WALK_ONLY": {
1031
- for (let i = 0; i < node.childCount; i++) {
1032
- const child = node.child(i);
1033
- if (node.type === "export_statement") {
1034
- if (child.type === "export" || child.type === "default" || child.text === "export" || child.text === "default") {
1035
- if (child.childCount === 0 && (child.text === "export" || child.text === "default")) {
1036
- continue;
1037
- }
1038
- }
1039
- }
1040
- walkNode(child, depth + 1, lines);
1041
- }
1042
- break;
1043
- }
1044
- case "T4_DROP":
1045
- default:
1046
- break;
1047
- }
1048
- }
1049
- async function astWalkIR(code, filePath) {
1050
- const lang = detectLanguage(filePath);
1051
- if (!lang) return null;
1052
- const { parser } = await getParser(lang);
1053
- const tree = parser.parse(code);
1054
- const root = tree.rootNode;
1055
- const lines = [];
1056
- walkNode(root, 0, lines);
1057
- if (lines.length === 0) return null;
1058
- const pass1 = [];
1059
- let useBlock = [];
1060
- for (const line of lines) {
1061
- if (line.startsWith("USE:")) {
1062
- const m = line.match(/from\s+["']([^"']+)["']/);
1063
- useBlock.push(m ? m[1] : line.slice(4));
1064
- } else {
1065
- if (useBlock.length > 0) {
1066
- if (useBlock.length <= 3) {
1067
- for (const mod of useBlock) pass1.push(`USE:${mod}`);
1068
- } else {
1069
- pass1.push(`USE:[${useBlock.join(", ")}]`);
1070
- }
1071
- useBlock = [];
1072
- }
1073
- pass1.push(line);
1074
- }
1075
- }
1076
- if (useBlock.length > 0) {
1077
- if (useBlock.length <= 3) {
1078
- for (const mod of useBlock) pass1.push(`USE:${mod}`);
1079
- } else {
1080
- pass1.push(`USE:[${useBlock.join(", ")}]`);
1081
- }
1082
- }
1083
- const merged = [];
1084
- let guardBlock = [];
1085
- for (const line of pass1) {
1086
- const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
1087
- if (guardMatch) {
1088
- guardBlock.push(guardMatch[2].trim());
1089
- continue;
1090
- }
1091
- if (guardBlock.length > 0) {
1092
- if (guardBlock.length < 3) {
1093
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1094
- } else {
1095
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
1096
- }
1097
- guardBlock = [];
1098
- }
1099
- merged.push(line);
1100
- }
1101
- if (guardBlock.length > 0) {
1102
- if (guardBlock.length < 3) {
1103
- for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1104
- } else {
1105
- merged.push(` GUARD:[${guardBlock.join(", ")}]`);
1106
- }
1107
- }
1108
- return merged.join("\n");
1109
- }
1110
-
1111
- // src/ir/layers.ts
1112
- function generateL0(code, filePath) {
1113
- const structure = extractStructure(code);
1114
- const topLevel = structure.filter(
1115
- (s) => s.indent === 0 && ["function-start", "type-start", "export"].includes(s.type)
1116
- );
1117
- const declarations = topLevel.map((s) => {
1118
- const name = s.raw.match(
1119
- /(?:function|class|interface|const|let|var|export\s+(?:default\s+)?(?:function|class|async\s+function))\s+(\w+)/
1120
- )?.[1] ?? "unknown";
1121
- return ` ${s.type === "type-start" ? "CLASS" : "FN"}:${name} L${s.line}`;
1122
- });
1123
- return `${filePath}
1124
- ${declarations.join("\n")}`;
1125
- }
1126
- async function generateL1(code, filePath, health) {
1127
- const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
1128
- const result = ir.length < code.length ? ir : code;
1129
- if (health) {
1130
- return annotateIR(result, health);
1131
- }
1132
- return result;
1133
- }
1134
- function generateL2(delta, health) {
1135
- const parts = [`FILE: ${delta.file}`];
1136
- for (const hunk of delta.hunks) {
1137
- if (hunk.functionScope) parts.push(`SCOPE: ${hunk.functionScope}`);
1138
- parts.push(`CHANGED: ${hunk.changed.join("\n ")}`);
1139
- if (hunk.surroundingIR) parts.push(`CONTEXT: ${hunk.surroundingIR}`);
1140
- if (hunk.blame) {
1141
- parts.push(`BLAME: ${hunk.blame.author}, ${hunk.blame.date}, commit:"${hunk.blame.commitMessage}"`);
1142
- }
1143
- }
1144
- const ir = parts.join("\n");
1145
- if (health) return annotateIR(ir, health);
1146
- return ir;
1147
- }
1148
- function generateL3(code, startLine, endLine) {
1149
- const lines = code.split("\n");
1150
- return lines.slice(startLine - 1, endLine).join("\n");
1151
- }
1152
- async function generateLayer(layer, options) {
1153
- switch (layer) {
1154
- case "L0":
1155
- return generateL0(options.code, options.filePath);
1156
- case "L1":
1157
- return generateL1(options.code, options.filePath, options.health);
1158
- case "L2":
1159
- if (!options.delta) return generateL1(options.code, options.filePath, options.health);
1160
- return generateL2(options.delta, options.health);
1161
- case "L3":
1162
- if (options.lineRange) {
1163
- return generateL3(options.code, options.lineRange.start, options.lineRange.end);
1164
- }
1165
- return options.code;
1166
- }
1167
- }
1168
-
1169
- // src/trends/git-log-parser.ts
1170
- import { execSync } from "child_process";
1171
- var BUG_FIX_PATTERNS = [
1172
- /\bfix\b/i,
1173
- /\bbugfix\b/i,
1174
- /\bhotfix\b/i,
1175
- /\bpatch\b/i,
1176
- /\bresolve\b/i,
1177
- /\bbug\b/i
1178
- ];
1179
- function isBugFixCommit(message) {
1180
- return BUG_FIX_PATTERNS.some((p) => p.test(message));
1181
- }
1182
- function parseGitLogOutput(output) {
1183
- const entries = [];
1184
- const lines = output.split("\n");
1185
- let i = 0;
1186
- while (i < lines.length) {
1187
- const line = lines[i].trim();
1188
- if (!line || !line.includes("|")) {
1189
- i++;
1190
- continue;
1191
- }
1192
- const [hash, author, date, ...messageParts] = line.split("|");
1193
- const message = messageParts.join("|");
1194
- const files = [];
1195
- i++;
1196
- while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
1197
- const fileLine = lines[i].trim();
1198
- if (fileLine) files.push(fileLine);
1199
- i++;
1200
- }
1201
- entries.push({ hash, author, date, message, files });
1202
- }
1203
- return entries;
1204
- }
1205
- function getGitLog(repoPath, count = 100) {
1206
- try {
1207
- const output = execSync(
1208
- `git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
1209
- { cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
1210
- );
1211
- return parseGitLogOutput(output);
1212
- } catch {
1213
- return [];
1214
- }
1215
- }
1216
-
1217
- // src/trends/hotspot.ts
1218
- function detectHotspots(entries, options) {
1219
- const fileStats = /* @__PURE__ */ new Map();
1220
- for (const entry of entries) {
1221
- const isFix = isBugFixCommit(entry.message);
1222
- for (const file of entry.files) {
1223
- const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
1224
- stats.changes++;
1225
- if (isFix) stats.fixes++;
1226
- stats.authors.add(entry.author);
1227
- fileStats.set(file, stats);
1228
- }
1229
- }
1230
- const hotspots = [];
1231
- for (const [file, stats] of fileStats) {
1232
- const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
1233
- if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
1234
- hotspots.push({
1235
- file,
1236
- changesInLast30Commits: stats.changes,
1237
- bugFixRatio: fixRatio,
1238
- authorCount: stats.authors.size
1239
- });
1240
- }
1241
- }
1242
- return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
1243
- }
1244
-
1245
- // src/trends/decay.ts
1246
- function detectDecay(entries) {
1247
- const fileChanges = /* @__PURE__ */ new Map();
1248
- for (const entry of entries) {
1249
- for (const file of entry.files) {
1250
- const changes = fileChanges.get(file) ?? [];
1251
- changes.push({ date: entry.date });
1252
- fileChanges.set(file, changes);
1253
- }
1254
- }
1255
- const signals = [];
1256
- for (const [file, changes] of fileChanges) {
1257
- if (changes.length < 4) continue;
1258
- const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
1259
- const firstDate = new Date(sorted[0].date).getTime();
1260
- const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
1261
- const midDate = firstDate + (lastDate - firstDate) / 2;
1262
- const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
1263
- const secondHalfCount = sorted.length - firstHalfCount;
1264
- if (secondHalfCount > firstHalfCount) {
1265
- signals.push({
1266
- file,
1267
- metric: "churn",
1268
- trend: "declining",
1269
- dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
1270
- });
1271
- }
1272
- }
1273
- return signals;
1274
- }
1275
-
1276
- // src/trends/inconsistency.ts
1277
- function detectInconsistencies(entries, minAuthors = 3) {
1278
- const fileAuthors = /* @__PURE__ */ new Map();
1279
- for (const entry of entries) {
1280
- for (const file of entry.files) {
1281
- const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
1282
- const commits = authors.get(entry.author) ?? [];
1283
- commits.push(entry.message);
1284
- authors.set(entry.author, commits);
1285
- fileAuthors.set(file, authors);
1286
- }
1287
- }
1288
- const inconsistencies = [];
1289
- for (const [file, authors] of fileAuthors) {
1290
- if (authors.size >= minAuthors) {
1291
- const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
1292
- author,
1293
- style: categorizeStyle(commits)
1294
- }));
1295
- inconsistencies.push({ file, patterns });
1296
- }
1297
- }
1298
- return inconsistencies;
1299
- }
1300
- function categorizeStyle(commits) {
1301
- const types = commits.map((m) => {
1302
- if (m.match(/^fix/i)) return "fix";
1303
- if (m.match(/^feat/i)) return "feature";
1304
- if (m.match(/^refactor/i)) return "refactor";
1305
- return "other";
1306
- });
1307
- const primary = mode(types);
1308
- return `primarily ${primary} (${commits.length} commits)`;
1309
- }
1310
- function mode(arr) {
1311
- const counts = /* @__PURE__ */ new Map();
1312
- for (const item of arr) {
1313
- counts.set(item, (counts.get(item) ?? 0) + 1);
1314
- }
1315
- return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
1316
- }
1317
-
1318
- // src/benchmark/tokenizer.ts
1319
- function estimateTokens(text) {
1320
- if (!text) return 0;
1321
- const tokens = text.split(/[\s]+|(?<=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])|(?=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])/).filter(Boolean);
1322
- return tokens.length;
1323
- }
1324
-
1325
- // src/benchmark/runner.ts
1326
- async function benchmarkFile(code, filePath) {
1327
- const rawTokens = estimateTokens(code);
1328
- const irL0 = await generateLayer("L0", { code, filePath, health: null });
1329
- const irL1 = await generateLayer("L1", { code, filePath, health: null });
1330
- const irL0Tokens = estimateTokens(irL0);
1331
- const irL1Tokens = estimateTokens(irL1);
1332
- const astResult = await astWalkIR(code, filePath);
1333
- const engine = astResult !== null ? "AST" : "FP";
1334
- const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
1335
- return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, engine };
1336
- }
1337
- function summarize(results) {
1338
- const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
1339
- const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
1340
- const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
1341
- const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
1342
- const astCount = results.filter((r) => r.engine === "AST").length;
1343
- const fpCount = results.filter((r) => r.engine === "FP").length;
1344
- return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, astCount, fpCount };
1345
- }
1346
-
1347
- // src/context/packer.ts
1348
- function escapeRegex(s) {
1349
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1350
- }
1351
- function findTargetFile(files, target) {
1352
- const t = escapeRegex(target);
1353
- const declarationPatterns = [
1354
- // JS/TS declarations
1355
- new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${t}\\b`),
1356
- new RegExp(`(?:export\\s+)?class\\s+${t}\\b`),
1357
- new RegExp(`(?:export\\s+)?interface\\s+${t}\\b`),
1358
- new RegExp(`(?:export\\s+)?type\\s+${t}\\b`),
1359
- new RegExp(`(?:export\\s+)?enum\\s+${t}\\b`),
1360
- new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${t}\\b`),
1361
- // Python
1362
- new RegExp(`\\bdef\\s+${t}\\b`),
1363
- // Rust
1364
- new RegExp(`\\bfn\\s+${t}\\b`),
1365
- new RegExp(`\\bstruct\\s+${t}\\b`),
1366
- new RegExp(`\\btrait\\s+${t}\\b`),
1367
- // Go
1368
- new RegExp(`\\bfunc\\s+${t}\\b`),
1369
- new RegExp(`\\btype\\s+${t}\\b`),
1370
- // Object method shorthand
1371
- new RegExp(`\\b${t}\\s*:\\s*(?:async\\s+)?function\\b`),
1372
- new RegExp(`\\b${t}\\s*\\([^)]*\\)\\s*\\{`)
1373
- ];
1374
- for (const pattern of declarationPatterns) {
1375
- const match2 = files.find((f) => pattern.test(f.code));
1376
- if (match2) return match2.path;
1377
- }
1378
- const fallback = new RegExp(`\\b${t}\\s*\\(`);
1379
- const match = files.find((f) => fallback.test(f.code));
1380
- return match ? match.path : null;
1381
- }
1382
- function findRelatedFiles(files, targetPath) {
1383
- const related = /* @__PURE__ */ new Set();
1384
- const targetFile = files.find((f) => f.path === targetPath);
1385
- if (!targetFile) return related;
1386
- const importPattern = /(?:import|require)\s*(?:\([^)]*|\{[^}]*\}|\w+)?\s*(?:from)?\s*["']([^"']+)["']/g;
1387
- const imports = [...targetFile.code.matchAll(importPattern)].map((m) => m[1]);
1388
- for (const imp of imports) {
1389
- const match = files.find((f) => {
1390
- const basename = f.path.replace(/\.[^.]+$/, "");
1391
- return imp.includes(basename) || basename.endsWith(imp.replace(/^\.\.?\//, "").replace(/\.[^.]+$/, ""));
1392
- });
1393
- if (match) related.add(match.path);
1394
- }
1395
- const targetBasename = targetPath.replace(/\.[^.]+$/, "").split("/").pop() ?? "";
1396
- for (const file of files) {
1397
- if (file.path === targetPath) continue;
1398
- if (file.code.includes(targetBasename)) {
1399
- related.add(file.path);
1400
- }
1401
- }
1402
- return related;
1403
- }
1404
- async function packContext(files, options) {
1405
- const { budget, hotspots, target } = options;
1406
- const hotspotSet = new Set(hotspots.map((h) => h.file));
1407
- let targetPath = null;
1408
- let relatedFiles = /* @__PURE__ */ new Set();
1409
- if (target) {
1410
- targetPath = findTargetFile(files, target);
1411
- if (targetPath) {
1412
- relatedFiles = findRelatedFiles(files, targetPath);
1413
- }
1414
- }
1415
- const entries = [];
1416
- let totalTokens = 0;
1417
- let filesAtL3 = 0;
1418
- let targetDowngraded = false;
1419
- if (targetPath) {
1420
- const targetFile = files.find((f) => f.path === targetPath);
1421
- const rawTokens = estimateTokens(targetFile.code);
1422
- if (rawTokens <= budget * 0.6) {
1423
- entries.push({
1424
- path: targetPath,
1425
- layer: "L3",
1426
- ir: targetFile.code,
1427
- tokens: rawTokens,
1428
- isTarget: true
1429
- });
1430
- totalTokens += rawTokens;
1431
- filesAtL3 = 1;
1432
- } else {
1433
- targetDowngraded = true;
1434
- const l1 = await generateLayer("L1", { code: targetFile.code, filePath: targetFile.path, health: null });
1435
- const l1Tokens = estimateTokens(l1);
1436
- entries.push({
1437
- path: targetPath,
1438
- layer: "L1",
1439
- ir: l1,
1440
- tokens: l1Tokens,
1441
- isTarget: true
1442
- });
1443
- totalTokens += l1Tokens;
1444
- }
1445
- }
1446
- for (const file of files) {
1447
- if (file.path === targetPath) continue;
1448
- const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
1449
- const l0Tokens = estimateTokens(l0);
1450
- entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
1451
- totalTokens += l0Tokens;
1452
- }
1453
- if (totalTokens > budget) {
1454
- const truncated = entries.filter((e) => e.isTarget);
1455
- let used = truncated.reduce((s, e) => s + e.tokens, 0);
1456
- for (const entry of entries) {
1457
- if (entry.isTarget) continue;
1458
- if (used + entry.tokens <= budget) {
1459
- truncated.push(entry);
1460
- used += entry.tokens;
1461
- }
1462
- }
1463
- return {
1464
- entries: truncated,
1465
- totalTokens: used,
1466
- budget,
1467
- filesAtL0: truncated.filter((e) => e.layer === "L0").length,
1468
- filesAtL1: truncated.filter((e) => e.layer === "L1").length,
1469
- filesAtL3,
1470
- targetFile: targetPath ?? void 0,
1471
- targetDowngraded
1472
- };
1473
- }
1474
- const upgradeOrder = entries.map((e, i) => ({
1475
- index: i,
1476
- path: e.path,
1477
- rawTokens: files.find((f) => f.path === e.path)?.rawTokens ?? 0,
1478
- isHotspot: hotspotSet.has(e.path),
1479
- isRelated: relatedFiles.has(e.path),
1480
- isTarget: e.isTarget ?? false
1481
- })).filter((x) => x.isTarget === false && entries[x.index].layer === "L0").sort((a, b) => {
1482
- if (a.isRelated && !b.isRelated) return -1;
1483
- if (!a.isRelated && b.isRelated) return 1;
1484
- if (a.isHotspot && !b.isHotspot) return -1;
1485
- if (!a.isHotspot && b.isHotspot) return 1;
1486
- return b.rawTokens - a.rawTokens;
1487
- });
1488
- let filesAtL1 = 0;
1489
- for (const item of upgradeOrder) {
1490
- const file = files.find((f) => f.path === item.path);
1491
- const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
1492
- const l1Tokens = estimateTokens(l1);
1493
- const currentTokens = entries[item.index].tokens;
1494
- const additional = l1Tokens - currentTokens;
1495
- if (totalTokens + additional <= budget) {
1496
- entries[item.index] = {
1497
- path: item.path,
1498
- layer: "L1",
1499
- ir: l1,
1500
- tokens: l1Tokens
1501
- };
1502
- totalTokens += additional;
1503
- filesAtL1++;
1504
- }
1505
- }
1506
- return {
1507
- entries,
1508
- totalTokens,
1509
- budget,
1510
- filesAtL0: entries.filter((e) => e.layer === "L0").length,
1511
- filesAtL1,
1512
- filesAtL3,
1513
- targetFile: targetPath ?? void 0,
1514
- targetDowngraded
1515
- };
1516
- }
1517
-
1518
- export {
1519
- loadConfig,
1520
- runDetector,
1521
- computeHealthFromTrends,
1522
- generateLayer,
1523
- getGitLog,
1524
- detectHotspots,
1525
- detectDecay,
1526
- detectInconsistencies,
1527
- estimateTokens,
1528
- benchmarkFile,
1529
- summarize,
1530
- packContext
1531
- };