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/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, readdirSync } from "fs";
19
- import { join, relative } from "path";
4
+ import { readFileSync as readFileSync4 } from "fs";
5
+ import { relative as relative2, join as join6 } from "path";
20
6
 
21
- // src/router/router.ts
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 (!picomatch.isMatch(finding.file, rule.pattern)) continue;
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/cli/commands.ts
184
- function collectFiles(dir, extensions) {
185
- const files = [];
186
- try {
187
- const entries = readdirSync(dir, { withFileTypes: true });
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
- const fullPath = join(dir, entry.name);
190
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
191
- if (entry.isDirectory()) {
192
- files.push(...collectFiles(fullPath, extensions));
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.2.3 \u2014 scanning...\n");
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 = readFileSync(file, "utf-8");
211
- const relPath = relative(projectPath, file);
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.2.3 \u2014 trend analysis...\n");
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 = readFileSync(filePath, "utf-8");
252
- const relPath = relative(projectPath, filePath);
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.2.3 \u2014 benchmark\n");
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 = readFileSync(file, "utf-8");
280
- const relPath = relative(projectPath, file);
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 = readFileSync(filePath, "utf-8");
319
- const relPath = relative(projectPath, filePath);
320
- console.log("composto v0.2.3 \u2014 quality benchmark\n");
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.2.3 \u2014 context (target: ${target}, budget: ${budget} tokens)
351
- ` : `composto v0.2.3 \u2014 context (budget: ${budget} tokens)
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 = readFileSync(file, "utf-8");
365
- const relPath = relative(projectPath, file);
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 = resolve(args[1] ?? ".");
2861
+ const projectPath = resolve2(args[1] ?? ".");
427
2862
  runScan(projectPath);
428
2863
  break;
429
2864
  }
430
2865
  case "trends": {
431
- const projectPath = resolve(args[1] ?? ".");
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(resolve("."), resolve(filePath), layer);
2877
+ await runIR(resolve2("."), resolve2(filePath), layer);
443
2878
  break;
444
2879
  }
445
2880
  case "benchmark": {
446
- const projectPath = resolve(args[1] ?? ".");
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(resolve("."), resolve(filePath));
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 = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
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.2.3");
2933
+ console.log("composto v0.4.1");
477
2934
  break;
478
2935
  default:
479
- console.log("composto v0.2.3 \u2014 less tokens, more insight\n");
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
  }