composto-ai 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,46 +1,2372 @@
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/mcp/server.ts
18
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
6
  import { z } from "zod";
21
- import { readFileSync, readdirSync } from "fs";
22
- import { resolve, relative, join } from "path";
23
- var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
24
- function collectFiles(dir, extensions) {
25
- const files = [];
7
+ import { readFileSync as readFileSync4 } from "fs";
8
+ import { resolve as resolve2, relative as relative2, join as join6 } from "path";
9
+
10
+ // src/ir/structure.ts
11
+ var CLASSIFIERS = [
12
+ [/^(function|def|fn|func)\b/, "function-start"],
13
+ [/^(class|struct|interface)\b/, "type-start"],
14
+ [/^(if|else|elif|switch|match|case)\b/, "branch"],
15
+ [/^(for|while|loop|do)\b/, "loop"],
16
+ [/^(return|yield)\b/, "exit"],
17
+ [/^(import|require|use|from)\b/, "import"],
18
+ [/^(export|pub|public)\b/, "export"],
19
+ [/^(const|let|var|val|mut)\b/, "assignment"],
20
+ [/^(try|catch|except|finally)\b/, "error-handling"],
21
+ [/^(async|await)\b/, "async"],
22
+ [/^(\/\/|\/\*|#)/, "comment"]
23
+ ];
24
+ function classifyLine(firstToken) {
25
+ if (firstToken === "") return "blank";
26
+ for (const [pattern, type] of CLASSIFIERS) {
27
+ if (pattern.test(firstToken)) return type;
28
+ }
29
+ return "unknown";
30
+ }
31
+ function extractStructure(code) {
32
+ const lines = code.split("\n");
33
+ return lines.map((raw, i) => {
34
+ const indent = raw.search(/\S/);
35
+ const trimmed = raw.trim();
36
+ const firstToken = trimmed.split(/[\s({<]/)[0];
37
+ const type = classifyLine(firstToken);
38
+ return {
39
+ line: i + 1,
40
+ indent: indent === -1 ? -1 : indent,
41
+ type,
42
+ raw
43
+ };
44
+ });
45
+ }
46
+
47
+ // src/ir/fingerprint.ts
48
+ var PATTERNS = [
49
+ // import type { x, y } from "module"
50
+ {
51
+ match: /^import\s+type\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
52
+ transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
53
+ confidence: 0.95
54
+ },
55
+ // import { x, y } from "module"
56
+ {
57
+ match: /^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
58
+ transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
59
+ confidence: 0.95
60
+ },
61
+ // import type x from "module"
62
+ {
63
+ match: /^import\s+type\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
64
+ transform: (m) => `USE:${m[2]}{${m[1]}}`,
65
+ confidence: 0.95
66
+ },
67
+ // import x from "module"
68
+ {
69
+ match: /^import\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
70
+ transform: (m) => `USE:${m[2]}{${m[1]}}`,
71
+ confidence: 0.95
72
+ },
73
+ // const x = require("module")
74
+ {
75
+ match: /^(?:const|let|var)\s+(\w+)\s*=\s*require\(["']([^"']+)["']\);?\s*$/,
76
+ transform: (m) => `USE:${m[2]}{${m[1]}}`,
77
+ confidence: 0.95
78
+ },
79
+ // export function name(params) {
80
+ {
81
+ match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
82
+ transform: (m) => `OUT FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
83
+ confidence: 0.95
84
+ },
85
+ // function name(params) {
86
+ {
87
+ match: /^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{?\s*$/,
88
+ transform: (m) => `FN:${m[1]}(${m[2].replace(/\s/g, "")})`,
89
+ confidence: 0.95
90
+ },
91
+ // export class Name extends Base {
92
+ {
93
+ match: /^export\s+class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
94
+ transform: (m) => `OUT CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
95
+ confidence: 0.95
96
+ },
97
+ // class Name extends Base {
98
+ {
99
+ match: /^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*(?:implements\s+\S+\s*)?\{?\s*$/,
100
+ transform: (m) => `CLASS:${m[1]}${m[2] ? ` < ${m[2]}` : ""}`,
101
+ confidence: 0.95
102
+ },
103
+ // if (cond) return expr;
104
+ {
105
+ match: /^if\s*\(([^)]+)\)\s*return\s+(.+);?\s*$/,
106
+ transform: (m) => `IF:${m[1].trim()} -> RET ${m[2].trim().replace(/;$/, "")}`,
107
+ confidence: 0.95
108
+ },
109
+ // if (cond) {
110
+ {
111
+ match: /^if\s*\(([^)]+)\)\s*\{?\s*$/,
112
+ transform: (m) => `IF:${m[1].trim()}`,
113
+ confidence: 0.9
114
+ },
115
+ // for (... of/in ...) {
116
+ {
117
+ match: /^for\s*\((?:const|let|var)\s+(\w+)\s+(?:of|in)\s+(\w+)\)\s*\{?\s*$/,
118
+ transform: (m) => `LOOP:${m[2]} -> ${m[1]}`,
119
+ confidence: 0.9
120
+ },
121
+ // return expr
122
+ {
123
+ match: /^return\s+(.+);?\s*$/,
124
+ transform: (m) => `RET ${m[1].trim().replace(/;$/, "")}`,
125
+ confidence: 0.95
126
+ },
127
+ // return;
128
+ {
129
+ match: /^return;?\s*$/,
130
+ transform: () => "RET",
131
+ confidence: 0.95
132
+ },
133
+ // try {
134
+ {
135
+ match: /^try\s*\{\s*$/,
136
+ transform: () => "TRY:",
137
+ confidence: 0.9
138
+ },
139
+ // catch (e) {
140
+ {
141
+ match: /^(?:\}\s*)?catch\s*\((\w+)\)\s*\{?\s*$/,
142
+ transform: (m) => `CATCH:${m[1]}`,
143
+ confidence: 0.9
144
+ },
145
+ // switch (expr) {
146
+ {
147
+ match: /^switch\s*\(([^)]+)\)\s*\{?\s*$/,
148
+ transform: (m) => `SWITCH:${m[1].trim()}`,
149
+ confidence: 0.9
150
+ },
151
+ // case "value": / case value:
152
+ {
153
+ match: /^case\s+(.+)\s*:\s*$/,
154
+ transform: (m) => `CASE:${m[1].trim()}`,
155
+ confidence: 0.9
156
+ },
157
+ // default:
158
+ {
159
+ match: /^default\s*:\s*$/,
160
+ transform: () => "DEFAULT:",
161
+ confidence: 0.9
162
+ },
163
+ // export type Name = ...
164
+ {
165
+ match: /^export\s+type\s+(\w+)(?:<[^>]+>)?\s*=\s*(.+);?\s*$/,
166
+ transform: (m) => `OUT TYPE:${m[1]}`,
167
+ confidence: 0.9
168
+ },
169
+ // if (cond) expr; (inline if with method call)
170
+ {
171
+ match: /^if\s*\(([^)]+)\)\s+(\w+.+);?\s*$/,
172
+ transform: (m) => `IF:${m[1].trim()} -> ${m[2].replace(/;$/, "").trim().slice(0, 50)}`,
173
+ confidence: 0.9
174
+ },
175
+ // export async function name( (multiline signature)
176
+ {
177
+ match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(\s*$/,
178
+ transform: (m) => `OUT FN:${m[1]}(`,
179
+ confidence: 0.95
180
+ },
181
+ // interface/type property — name: type;
182
+ {
183
+ match: /^\s*(\w+)\??\s*:\s*(.+);?\s*$/,
184
+ transform: (m) => `PROP:${m[1]}: ${m[2].replace(/;$/, "").trim()}`,
185
+ confidence: 0.75
186
+ },
187
+ // const x = await expr;
188
+ {
189
+ match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*await\s+(.+);?\s*$/,
190
+ transform: (m) => {
191
+ const prefix = m[0].startsWith("export") ? "OUT " : "";
192
+ return `${prefix}AWAIT:VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
193
+ },
194
+ confidence: 0.85
195
+ },
196
+ // export const name = async (params) => { OR export const name = (params) => expr;
197
+ {
198
+ match: /^export\s+(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
199
+ transform: (m) => {
200
+ const asyncPrefix = m[2] ? "ASYNC " : "";
201
+ const body = m[4].replace(/[{;]\s*$/, "").trim();
202
+ return `OUT ${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
203
+ },
204
+ confidence: 0.9
205
+ },
206
+ // const name = async (params) => { OR const name = (params) => expr;
207
+ {
208
+ match: /^(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
209
+ transform: (m) => {
210
+ const asyncPrefix = m[2] ? "ASYNC " : "";
211
+ const body = m[4].replace(/[{;]\s*$/, "").trim();
212
+ return `${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
213
+ },
214
+ confidence: 0.9
215
+ },
216
+ // get name() {
217
+ {
218
+ match: /^\s*get\s+(\w+)\s*\(\)\s*(?::\s*\S+\s*)?\{?\s*$/,
219
+ transform: (m) => `GET:${m[1]}()`,
220
+ confidence: 0.9
221
+ },
222
+ // set name(value) {
223
+ {
224
+ match: /^\s*set\s+(\w+)\s*\(([^)]*)\)\s*\{?\s*$/,
225
+ transform: (m) => `SET:${m[1]}(${m[2].replace(/\s/g, "")})`,
226
+ confidence: 0.9
227
+ },
228
+ // methodName(params) { (inside class body, indented)
229
+ {
230
+ match: /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{\s*$/,
231
+ transform: (m) => {
232
+ const name = m[1];
233
+ if (["if", "for", "while", "switch", "catch", "function"].includes(name)) return `${name}`;
234
+ return `METHOD:${name}(${m[2].replace(/\s/g, "")})`;
235
+ },
236
+ confidence: 0.9
237
+ },
238
+ // const { a, b } = expr (object destructuring — before regular assignment)
239
+ {
240
+ match: /^(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(.+);?\s*$/,
241
+ transform: (m) => `VAR:{${m[1].replace(/\s/g, "")}} = ${m[2].replace(/;$/, "").trim()}`,
242
+ confidence: 0.9
243
+ },
244
+ // const [a, b] = expr (destructuring — before regular assignment)
245
+ {
246
+ match: /^(?:const|let|var)\s+\[([^\]]+)\]\s*=\s*(.+);?\s*$/,
247
+ transform: (m) => `VAR:[${m[1].replace(/\s/g, "")}] = ${m[2].replace(/;$/, "").trim()}`,
248
+ confidence: 0.9
249
+ },
250
+ // const name = value;
251
+ {
252
+ match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+);?\s*$/,
253
+ transform: (m) => {
254
+ const prefix = m[0].startsWith("export") ? "OUT " : "";
255
+ return `${prefix}VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
256
+ },
257
+ confidence: 0.85
258
+ }
259
+ ];
260
+ function fingerprintLine(line) {
261
+ const trimmed = line.trim();
262
+ if (trimmed === "" || trimmed === "{" || trimmed === "}" || trimmed === "});" || trimmed === ");") {
263
+ return { type: "fingerprint", ir: "", confidence: 1 };
264
+ }
265
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
266
+ return { type: "fingerprint", ir: "", confidence: 1 };
267
+ }
268
+ for (const pattern of PATTERNS) {
269
+ const match = trimmed.match(pattern.match);
270
+ if (match) {
271
+ const ir = pattern.transform(match);
272
+ if (pattern.confidence > 0.9) {
273
+ return { type: "fingerprint", ir, confidence: pattern.confidence };
274
+ }
275
+ return {
276
+ type: "fingerprint+hint",
277
+ ir,
278
+ hint: trimmed,
279
+ confidence: pattern.confidence
280
+ };
281
+ }
282
+ }
283
+ return { type: "raw", ir: trimmed, confidence: 0.1 };
284
+ }
285
+ function fingerprintFile(code, confidenceThreshold = 0.6) {
286
+ const lines = code.split("\n");
287
+ const irLines = [];
288
+ for (const line of lines) {
289
+ const indent = line.search(/\S/);
290
+ const indentStr = indent > 0 ? " ".repeat(Math.floor(indent / 2)) : "";
291
+ const result = fingerprintLine(line);
292
+ if (result.ir === "") continue;
293
+ if (result.confidence >= confidenceThreshold) {
294
+ irLines.push(`${indentStr}${result.ir}`);
295
+ }
296
+ }
297
+ return irLines.join("\n");
298
+ }
299
+
300
+ // src/ir/health.ts
301
+ var CHURN_THRESHOLD = 10;
302
+ var FIX_RATIO_THRESHOLD = 0.5;
303
+ function buildHealthTag(health) {
304
+ const parts = [];
305
+ if (health.churn > CHURN_THRESHOLD) parts.push(`HOT:${health.churn}/30`);
306
+ if (health.fixRatio > FIX_RATIO_THRESHOLD) parts.push(`FIX:${Math.round(health.fixRatio * 100)}%`);
307
+ if (health.coverageTrend === "down") parts.push("COV:\u2193");
308
+ if (health.consistency === "low") parts.push("INCON");
309
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
310
+ }
311
+ function annotateIR(ir, health) {
312
+ const tag = buildHealthTag(health);
313
+ if (!tag) return ir;
314
+ const lines = ir.split("\n");
315
+ lines[0] = `${lines[0]} ${tag}`;
316
+ return lines.join("\n");
317
+ }
318
+ function computeHealthFromTrends(file, trends) {
319
+ const hotspot = trends.hotspots.find((h) => h.file === file);
320
+ const decay = trends.decaySignals.find((d) => d.file === file);
321
+ const inconsistency = trends.inconsistencies.find((i) => i.file === file);
322
+ return {
323
+ churn: hotspot?.changesInLast30Commits ?? 0,
324
+ fixRatio: hotspot?.bugFixRatio ?? 0,
325
+ coverageTrend: decay?.trend === "declining" ? "down" : decay?.trend === "improving" ? "up" : "stable",
326
+ staleness: "",
327
+ authorCount: hotspot?.authorCount ?? 0,
328
+ consistency: inconsistency ? "low" : "high"
329
+ };
330
+ }
331
+
332
+ // src/parser/init.ts
333
+ import { Parser, Language } from "web-tree-sitter";
334
+ import { resolve, dirname } from "path";
335
+ import { existsSync } from "fs";
336
+ import { fileURLToPath } from "url";
337
+ var __dirname = dirname(fileURLToPath(import.meta.url));
338
+ var initialized = false;
339
+ var cache = /* @__PURE__ */ new Map();
340
+ function grammarPath(lang) {
341
+ const distPath = resolve(__dirname, "grammars", `tree-sitter-${lang}.wasm`);
342
+ if (existsSync(distPath)) return distPath;
343
+ const devPath = resolve(__dirname, "../../grammars", `tree-sitter-${lang}.wasm`);
344
+ if (existsSync(devPath)) return devPath;
345
+ throw new Error(`Grammar not found for ${lang}`);
346
+ }
347
+ async function getParser(lang) {
348
+ if (!initialized) {
349
+ await Parser.init();
350
+ initialized = true;
351
+ }
352
+ const cached = cache.get(lang);
353
+ if (cached) return cached;
354
+ const parser = new Parser();
355
+ const language = await Language.load(grammarPath(lang));
356
+ parser.setLanguage(language);
357
+ const result = { parser, language };
358
+ cache.set(lang, result);
359
+ return result;
360
+ }
361
+
362
+ // src/parser/languages.ts
363
+ import { extname } from "path";
364
+ var EXT_MAP = {
365
+ ".ts": "typescript",
366
+ ".tsx": "typescript",
367
+ ".js": "javascript",
368
+ ".jsx": "javascript",
369
+ ".mjs": "javascript",
370
+ ".py": "python",
371
+ ".go": "go",
372
+ ".rs": "rust"
373
+ };
374
+ var SUPPORTED_EXTENSIONS = Object.keys(EXT_MAP);
375
+ function detectLanguage(filePath) {
376
+ const ext = extname(filePath);
377
+ return EXT_MAP[ext] ?? null;
378
+ }
379
+
380
+ // src/ir/ast-walker.ts
381
+ var TIER_MAP = {
382
+ // Tier 1 — structural declarations (JS/TS)
383
+ import_statement: "T1_KEEP",
384
+ function_declaration: "T1_KEEP",
385
+ class_declaration: "T1_KEEP",
386
+ interface_declaration: "T1_KEEP",
387
+ type_alias_declaration: "T1_KEEP",
388
+ enum_declaration: "T1_KEEP",
389
+ // Tier 1 — Python
390
+ function_definition: "T1_KEEP",
391
+ class_definition: "T1_KEEP",
392
+ import_from_statement: "T1_KEEP",
393
+ decorated_definition: "T1_KEEP",
394
+ // Tier 1 — class members (qualified methods only, fields dropped as noise)
395
+ method_definition: "T1_KEEP",
396
+ // JS/TS class method
397
+ // Tier 1 — Go
398
+ function_item: "T1_KEEP",
399
+ // Rust
400
+ method_declaration: "T1_KEEP",
401
+ // Go
402
+ type_declaration: "T1_KEEP",
403
+ // Go
404
+ import_declaration: "T1_KEEP",
405
+ // Go
406
+ use_declaration: "T1_KEEP",
407
+ // Rust
408
+ struct_item: "T1_KEEP",
409
+ // Rust
410
+ enum_item: "T1_KEEP",
411
+ // Rust
412
+ trait_item: "T1_KEEP",
413
+ // Rust
414
+ impl_item: "T1_KEEP",
415
+ // Rust
416
+ // Tier 2 — control flow (universal)
417
+ if_statement: "T2_CONTROL",
418
+ if_expression: "T2_CONTROL",
419
+ // Rust
420
+ else_clause: "WALK_ONLY",
421
+ elif_clause: "T2_CONTROL",
422
+ // Python
423
+ for_statement: "T2_CONTROL",
424
+ for_in_statement: "T2_CONTROL",
425
+ for_expression: "T2_CONTROL",
426
+ // Rust
427
+ while_statement: "T2_CONTROL",
428
+ do_statement: "T2_CONTROL",
429
+ switch_statement: "T2_CONTROL",
430
+ switch_case: "T2_CONTROL",
431
+ switch_default: "T2_CONTROL",
432
+ match_expression: "T2_CONTROL",
433
+ // Rust
434
+ return_statement: "T2_CONTROL",
435
+ return_expression: "T2_CONTROL",
436
+ // Rust
437
+ throw_statement: "T2_CONTROL",
438
+ raise_statement: "T2_CONTROL",
439
+ // Python
440
+ try_statement: "T2_CONTROL",
441
+ catch_clause: "T2_CONTROL",
442
+ except_clause: "T2_CONTROL",
443
+ // Python
444
+ with_statement: "T2_CONTROL",
445
+ // Python
446
+ defer_statement: "T2_CONTROL",
447
+ // Go
448
+ // Tier 3 — compressible expressions
449
+ lexical_declaration: "T3_COMPRESS",
450
+ expression_statement: "T3_COMPRESS",
451
+ assignment: "T3_COMPRESS",
452
+ // Python
453
+ short_var_declaration: "T3_COMPRESS",
454
+ // Go
455
+ // Walk-only — containers
456
+ program: "WALK_ONLY",
457
+ module: "WALK_ONLY",
458
+ // Python
459
+ statement_block: "WALK_ONLY",
460
+ block: "WALK_ONLY",
461
+ // Python/Go/Rust
462
+ class_body: "WALK_ONLY",
463
+ // JS/TS
464
+ switch_body: "WALK_ONLY",
465
+ export_statement: "WALK_ONLY",
466
+ source_file: "WALK_ONLY"
467
+ // Go/Rust
468
+ };
469
+ function tierOf(nodeType) {
470
+ return TIER_MAP[nodeType] ?? "T4_DROP";
471
+ }
472
+ function collapseText(text, maxLen) {
473
+ const collapsed = text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
474
+ if (collapsed.length <= maxLen) return collapsed;
475
+ return collapsed.slice(0, maxLen - 3) + "...";
476
+ }
477
+ function getTypeParams(node) {
478
+ for (let i = 0; i < node.childCount; i++) {
479
+ const child = node.child(i);
480
+ if (child.type === "type_parameters") {
481
+ return child.text;
482
+ }
483
+ }
484
+ return "";
485
+ }
486
+ function isExported(node) {
487
+ return node.parent?.type === "export_statement";
488
+ }
489
+ function isAsync(node) {
490
+ return node.text.trimStart().startsWith("async");
491
+ }
492
+ function extractDocComment(node) {
493
+ const exported = node.parent?.type === "export_statement";
494
+ if (!exported) return null;
495
+ const prev = node.parent?.previousNamedSibling;
496
+ if (!prev || prev.type !== "comment") return null;
497
+ const text = prev.text;
498
+ if (!text.startsWith("/**")) return null;
499
+ const body = text.replace(/^\/\*\*|\*\/$/g, "").replace(/^\s*\*\s?/gm, "").trim();
500
+ const hasDeprecated = /@deprecated\b/.test(body);
501
+ if (hasDeprecated) return "@deprecated";
502
+ const beforeTags = body.split(/@\w+/)[0].trim();
503
+ const firstLine = beforeTags.split("\n")[0].trim();
504
+ if (!firstLine || firstLine.length < 5) return null;
505
+ return `"${firstLine.length > 30 ? firstLine.slice(0, 27) + "..." : firstLine}"`;
506
+ }
507
+ function extractPythonDocstring(bodyNode) {
508
+ if (!bodyNode) return null;
509
+ for (let i = 0; i < bodyNode.childCount; i++) {
510
+ const child = bodyNode.child(i);
511
+ if (child.type === "expression_statement" && child.childCount > 0) {
512
+ const expr = child.child(0);
513
+ if (expr.type === "string") {
514
+ const text = expr.text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
515
+ const firstLine = text.split("\n")[0].trim();
516
+ if (firstLine.length < 5) return null;
517
+ return `"${firstLine.length > 30 ? firstLine.slice(0, 27) + "..." : firstLine}"`;
518
+ }
519
+ break;
520
+ }
521
+ }
522
+ return null;
523
+ }
524
+ function extractCondition(node) {
525
+ const condNode = node.childForFieldName("condition") ?? (() => {
526
+ for (let i = 0; i < node.childCount; i++) {
527
+ const c = node.child(i);
528
+ if (c.type === "parenthesized_expression") return c;
529
+ }
530
+ return null;
531
+ })();
532
+ if (!condNode) return "...";
533
+ const text = condNode.text.replace(/^\(/, "").replace(/\)$/, "").trim();
534
+ return text.length > 60 ? text.slice(0, 57) + "..." : text;
535
+ }
536
+ function emitTier2(node) {
537
+ switch (node.type) {
538
+ case "if_statement": {
539
+ const cond = extractCondition(node);
540
+ return `IF:${cond}`;
541
+ }
542
+ case "else_clause":
543
+ return "ELSE:";
544
+ case "for_statement":
545
+ case "for_in_statement":
546
+ return "LOOP";
547
+ case "while_statement": {
548
+ const cond = extractCondition(node);
549
+ return `WHILE:${cond}`;
550
+ }
551
+ case "do_statement": {
552
+ const cond = extractCondition(node);
553
+ return `WHILE:${cond}`;
554
+ }
555
+ case "switch_statement": {
556
+ const expr = node.childForFieldName("value") ?? node.childForFieldName("condition") ?? (() => {
557
+ for (let i = 0; i < node.childCount; i++) {
558
+ const c = node.child(i);
559
+ if (c.type === "parenthesized_expression") return c;
560
+ }
561
+ return null;
562
+ })();
563
+ const text = expr ? expr.text.replace(/^\(/, "").replace(/\)$/, "").trim() : "...";
564
+ return `SWITCH:${text.length > 60 ? text.slice(0, 57) + "..." : text}`;
565
+ }
566
+ case "switch_case": {
567
+ let value = null;
568
+ const valNode = node.childForFieldName("value");
569
+ if (valNode) {
570
+ value = valNode.text;
571
+ } else {
572
+ for (let i = 0; i < node.childCount; i++) {
573
+ const c = node.child(i);
574
+ if (c.type !== "case" && c.type !== ":" && c.childCount === 0 && c.text === "case") continue;
575
+ if (c.type !== "case" && c.text !== "case" && c.text !== ":") {
576
+ value = c.text;
577
+ break;
578
+ }
579
+ }
580
+ }
581
+ return `CASE:${value ?? "..."}`;
582
+ }
583
+ case "switch_default":
584
+ return "DEFAULT:";
585
+ case "return_statement": {
586
+ let retText = "";
587
+ for (let i = 0; i < node.childCount; i++) {
588
+ const c = node.child(i);
589
+ if (c.text !== "return" && c.text !== ";") {
590
+ retText += (retText ? " " : "") + c.text;
591
+ }
592
+ }
593
+ retText = retText.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
594
+ if (!retText) return "RET";
595
+ return `RET ${retText.length > 60 ? retText.slice(0, 57) + "..." : retText}`;
596
+ }
597
+ case "throw_statement": {
598
+ let throwText = "";
599
+ for (let i = 0; i < node.childCount; i++) {
600
+ const c = node.child(i);
601
+ if (c.text !== "throw" && c.text !== ";") {
602
+ throwText += (throwText ? " " : "") + c.text;
603
+ }
604
+ }
605
+ throwText = throwText.trim();
606
+ return `THROW:${throwText.length > 60 ? throwText.slice(0, 57) + "..." : throwText}`;
607
+ }
608
+ case "try_statement":
609
+ return "TRY";
610
+ case "catch_clause": {
611
+ const param = node.childForFieldName("parameter");
612
+ const paramText = param ? param.text : "...";
613
+ return `CATCH:${paramText}`;
614
+ }
615
+ // Python
616
+ case "raise_statement": {
617
+ const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
618
+ return `RAISE:${val.length > 50 ? val.slice(0, 47) + "..." : val}`;
619
+ }
620
+ case "except_clause":
621
+ return "EXCEPT";
622
+ case "elif_clause": {
623
+ const cond = extractCondition(node);
624
+ return `ELIF:${cond}`;
625
+ }
626
+ case "with_statement":
627
+ return "WITH";
628
+ // Rust
629
+ case "if_expression": {
630
+ const cond = extractCondition(node);
631
+ return `IF:${cond}`;
632
+ }
633
+ case "for_expression":
634
+ return "LOOP";
635
+ case "match_expression":
636
+ return "MATCH";
637
+ case "return_expression": {
638
+ const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
639
+ return `RET ${val.length > 60 ? val.slice(0, 57) + "..." : val}`.trimEnd();
640
+ }
641
+ // Go
642
+ case "defer_statement":
643
+ return "DEFER";
644
+ default:
645
+ return null;
646
+ }
647
+ }
648
+ function emitTier1(node) {
649
+ const exported = isExported(node);
650
+ const outPrefix = exported ? "OUT " : "";
651
+ switch (node.type) {
652
+ case "import_statement": {
653
+ const text = collapseText(node.text, 80);
654
+ return `USE:${text}`;
655
+ }
656
+ case "function_declaration": {
657
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
658
+ const rawParams = node.childForFieldName("parameters")?.text ?? "()";
659
+ const params = collapseText(rawParams, 60);
660
+ const asyncPrefix = isAsync(node) ? "ASYNC " : "";
661
+ const doc = extractDocComment(node);
662
+ const docPrefix = doc ? `${doc} ` : "";
663
+ return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
664
+ }
665
+ case "class_declaration": {
666
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
667
+ const typeParams = getTypeParams(node);
668
+ const doc = extractDocComment(node);
669
+ const docPrefix = doc ? `${doc} ` : "";
670
+ return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
671
+ }
672
+ case "interface_declaration": {
673
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
674
+ const typeParams = getTypeParams(node);
675
+ const doc = extractDocComment(node);
676
+ const docPrefix = doc ? `${doc} ` : "";
677
+ return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
678
+ }
679
+ case "type_alias_declaration": {
680
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
681
+ const doc = extractDocComment(node);
682
+ const docPrefix = doc ? `${doc} ` : "";
683
+ return `${docPrefix}${outPrefix}TYPE:${name}`;
684
+ }
685
+ case "enum_declaration": {
686
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
687
+ const doc = extractDocComment(node);
688
+ const docPrefix = doc ? `${doc} ` : "";
689
+ return `${docPrefix}${outPrefix}ENUM:${name}`;
690
+ }
691
+ case "method_definition": {
692
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
693
+ if (name.startsWith("#") || name.startsWith("_")) return null;
694
+ let enclosingClass = null;
695
+ let parent = node.parent;
696
+ while (parent) {
697
+ if (parent.type === "class_declaration" || parent.type === "class_definition") {
698
+ enclosingClass = parent.childForFieldName("name")?.text ?? null;
699
+ break;
700
+ }
701
+ parent = parent.parent;
702
+ }
703
+ const params = node.childForFieldName("parameters")?.text ?? "()";
704
+ const asyncPrefix = isAsync(node) ? "ASYNC " : "";
705
+ const qualifiedName = enclosingClass ? `${enclosingClass}.${name}` : name;
706
+ return `${asyncPrefix}METHOD:${qualifiedName}${collapseText(params, 40)}`;
707
+ }
708
+ // Python
709
+ case "function_definition": {
710
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
711
+ const params = node.childForFieldName("parameters")?.text ?? "()";
712
+ const returnType = node.childForFieldName("return_type")?.text ?? "";
713
+ const rt = returnType ? ` -> ${returnType}` : "";
714
+ const body = node.childForFieldName("body");
715
+ const doc = extractPythonDocstring(body);
716
+ const docPrefix = doc ? `${doc} ` : "";
717
+ return `${docPrefix}FN:${name}${collapseText(params, 60)}${rt}`;
718
+ }
719
+ case "class_definition": {
720
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
721
+ const superclass = node.childForFieldName("superclasses")?.text ?? "";
722
+ const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
723
+ const body = node.childForFieldName("body");
724
+ const doc = extractPythonDocstring(body);
725
+ const docPrefix = doc ? `${doc} ` : "";
726
+ return `${docPrefix}CLASS:${name}${sc}`;
727
+ }
728
+ case "import_from_statement": {
729
+ return `USE:${collapseText(node.text, 80)}`;
730
+ }
731
+ case "decorated_definition": {
732
+ return null;
733
+ }
734
+ // Go
735
+ case "method_declaration":
736
+ case "type_declaration":
737
+ case "import_declaration": {
738
+ return `${collapseText(node.text, 80)}`;
739
+ }
740
+ // Rust
741
+ case "function_item": {
742
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
743
+ const params = node.childForFieldName("parameters")?.text ?? "()";
744
+ return `FN:${name}${collapseText(params, 60)}`;
745
+ }
746
+ case "struct_item":
747
+ case "enum_item":
748
+ case "trait_item":
749
+ case "impl_item":
750
+ case "use_declaration": {
751
+ const firstLine = node.text.split("\n")[0];
752
+ return collapseText(firstLine, 80);
753
+ }
754
+ default:
755
+ return null;
756
+ }
757
+ }
758
+ function emitTier3(node) {
759
+ switch (node.type) {
760
+ case "lexical_declaration": {
761
+ let declarator = null;
762
+ for (let i = 0; i < node.childCount; i++) {
763
+ const c = node.child(i);
764
+ if (c.type === "variable_declarator") {
765
+ declarator = c;
766
+ break;
767
+ }
768
+ }
769
+ if (!declarator) return null;
770
+ const name = declarator.childForFieldName("name")?.text ?? "?";
771
+ const value = declarator.childForFieldName("value");
772
+ if (value) {
773
+ if (value.type === "arrow_function") {
774
+ const asyncPrefix = isAsync(value) ? "ASYNC " : "";
775
+ const params = value.childForFieldName("parameters")?.text ?? "()";
776
+ return `${asyncPrefix}FN:${name}${collapseText(params, 60)} => ...`;
777
+ }
778
+ if (value.type === "await_expression") {
779
+ const callee = value.childCount > 1 ? value.child(1).text : "...";
780
+ return `AWAIT:${name}=${collapseText(callee, 40)}`;
781
+ }
782
+ if (node.parent?.type === "statement_block") return null;
783
+ const vt = value.type;
784
+ if (vt === "number" || vt === "true" || vt === "false") return null;
785
+ if (vt === "object" || vt === "array") return null;
786
+ if (vt === "new_expression" || vt === "call_expression") return null;
787
+ const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
788
+ return `VAR:${name} = ${collapseText(valText, 50)}`;
789
+ }
790
+ return null;
791
+ }
792
+ case "expression_statement": {
793
+ const expr = node.child(0);
794
+ if (!expr) return null;
795
+ if (expr.type === "await_expression") {
796
+ return null;
797
+ }
798
+ if (expr.type === "call_expression") {
799
+ const callee = expr.child(0)?.text ?? "";
800
+ if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
801
+ const args = expr.child(1);
802
+ if (args && args.childCount >= 4) {
803
+ const child = args.child(1)?.text ?? "?";
804
+ const parent = args.child(3)?.text ?? "?";
805
+ const shortChild = child.length > 30 ? child.slice(0, 27) + "..." : child;
806
+ const shortParent = parent.length > 30 ? parent.slice(0, 27) + "..." : parent;
807
+ return `EXTENDS:${shortChild} < ${shortParent}`;
808
+ }
809
+ }
810
+ return null;
811
+ }
812
+ return null;
813
+ }
814
+ default:
815
+ return null;
816
+ }
817
+ }
818
+ function walkNode(node, depth, lines) {
819
+ const tier = tierOf(node.type);
820
+ switch (tier) {
821
+ case "T1_KEEP": {
822
+ const ir = emitTier1(node);
823
+ if (ir) lines.push(ir);
824
+ for (let i = 0; i < node.childCount; i++) {
825
+ const child = node.child(i);
826
+ const childType = child.type;
827
+ if (childType === "statement_block" || childType === "class_body" || childType === "block" || // Python/Go/Rust
828
+ childType === "body" || // Python class/function body
829
+ childType === "declaration_list") {
830
+ walkNode(child, depth + 1, lines);
831
+ }
832
+ }
833
+ break;
834
+ }
835
+ case "T2_CONTROL": {
836
+ if (depth > 4 && node.type !== "return_statement" && node.type !== "throw_statement" && node.type !== "switch_case" && node.type !== "switch_default") break;
837
+ if (node.type === "if_statement") {
838
+ let hasElse = false;
839
+ for (let i = 0; i < node.childCount; i++) {
840
+ if (node.child(i).type === "else_clause") {
841
+ hasElse = true;
842
+ break;
843
+ }
844
+ }
845
+ if (!hasElse) {
846
+ const body = node.childForFieldName("consequence") ?? (() => {
847
+ for (let i = 0; i < node.childCount; i++) {
848
+ const c = node.child(i);
849
+ if (c.type === "statement_block") return c;
850
+ }
851
+ return null;
852
+ })();
853
+ if (body) {
854
+ let singleStmt = null;
855
+ if (body.type === "statement_block") {
856
+ const stmts = [];
857
+ for (let i = 0; i < body.childCount; i++) {
858
+ const c = body.child(i);
859
+ if (c.type !== "{" && c.type !== "}") stmts.push(c);
860
+ }
861
+ if (stmts.length === 1) singleStmt = stmts[0];
862
+ } else if (body.type === "return_statement" || body.type === "throw_statement") {
863
+ singleStmt = body;
864
+ }
865
+ if (singleStmt && (singleStmt.type === "return_statement" || singleStmt.type === "throw_statement")) {
866
+ const cond = extractCondition(node);
867
+ const retLine = emitTier2(singleStmt);
868
+ if (retLine) {
869
+ const indent2 = " ".repeat(depth);
870
+ lines.push(`${indent2}IF:${cond} \u2192 ${retLine}`);
871
+ break;
872
+ }
873
+ }
874
+ }
875
+ }
876
+ }
877
+ const line = emitTier2(node);
878
+ const indent = " ".repeat(depth);
879
+ if (line) lines.push(indent + line);
880
+ for (let i = 0; i < node.childCount; i++) {
881
+ walkNode(node.child(i), depth + 1, lines);
882
+ }
883
+ break;
884
+ }
885
+ case "T3_COMPRESS": {
886
+ if (depth > 4) break;
887
+ const line = emitTier3(node);
888
+ const indent = " ".repeat(depth);
889
+ if (line) lines.push(indent + line);
890
+ break;
891
+ }
892
+ case "WALK_ONLY": {
893
+ for (let i = 0; i < node.childCount; i++) {
894
+ const child = node.child(i);
895
+ if (node.type === "export_statement") {
896
+ if (child.type === "export" || child.type === "default" || child.text === "export" || child.text === "default") {
897
+ if (child.childCount === 0 && (child.text === "export" || child.text === "default")) {
898
+ continue;
899
+ }
900
+ }
901
+ }
902
+ walkNode(child, depth + 1, lines);
903
+ }
904
+ break;
905
+ }
906
+ case "T4_DROP":
907
+ default:
908
+ break;
909
+ }
910
+ }
911
+ async function astWalkIR(code, filePath) {
912
+ const lang = detectLanguage(filePath);
913
+ if (!lang) return null;
914
+ const { parser } = await getParser(lang);
915
+ const tree = parser.parse(code);
916
+ const root = tree.rootNode;
917
+ const lines = [];
918
+ walkNode(root, 0, lines);
919
+ if (lines.length === 0) return null;
920
+ const pass1 = [];
921
+ let useBlock = [];
922
+ for (const line of lines) {
923
+ if (line.startsWith("USE:")) {
924
+ const m = line.match(/from\s+["']([^"']+)["']/);
925
+ useBlock.push(m ? m[1] : line.slice(4));
926
+ } else {
927
+ if (useBlock.length > 0) {
928
+ if (useBlock.length <= 3) {
929
+ for (const mod of useBlock) pass1.push(`USE:${mod}`);
930
+ } else {
931
+ pass1.push(`USE:[${useBlock.join(", ")}]`);
932
+ }
933
+ useBlock = [];
934
+ }
935
+ pass1.push(line);
936
+ }
937
+ }
938
+ if (useBlock.length > 0) {
939
+ if (useBlock.length <= 3) {
940
+ for (const mod of useBlock) pass1.push(`USE:${mod}`);
941
+ } else {
942
+ pass1.push(`USE:[${useBlock.join(", ")}]`);
943
+ }
944
+ }
945
+ const merged = [];
946
+ let guardBlock = [];
947
+ for (const line of pass1) {
948
+ const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
949
+ if (guardMatch) {
950
+ guardBlock.push(guardMatch[2].trim());
951
+ continue;
952
+ }
953
+ if (guardBlock.length > 0) {
954
+ if (guardBlock.length < 3) {
955
+ for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
956
+ } else {
957
+ merged.push(` GUARD:[${guardBlock.join(", ")}]`);
958
+ }
959
+ guardBlock = [];
960
+ }
961
+ merged.push(line);
962
+ }
963
+ if (guardBlock.length > 0) {
964
+ if (guardBlock.length < 3) {
965
+ for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
966
+ } else {
967
+ merged.push(` GUARD:[${guardBlock.join(", ")}]`);
968
+ }
969
+ }
970
+ return merged.join("\n");
971
+ }
972
+
973
+ // src/ir/layers.ts
974
+ function generateL0(code, filePath) {
975
+ const structure = extractStructure(code);
976
+ const topLevel = structure.filter(
977
+ (s) => s.indent === 0 && ["function-start", "type-start", "export"].includes(s.type)
978
+ );
979
+ const declarations = topLevel.map((s) => {
980
+ const name = s.raw.match(
981
+ /(?:function|class|interface|const|let|var|export\s+(?:default\s+)?(?:function|class|async\s+function))\s+(\w+)/
982
+ )?.[1] ?? "unknown";
983
+ return ` ${s.type === "type-start" ? "CLASS" : "FN"}:${name} L${s.line}`;
984
+ });
985
+ return `${filePath}
986
+ ${declarations.join("\n")}`;
987
+ }
988
+ async function generateL1(code, filePath, health) {
989
+ const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
990
+ const result = ir.length < code.length ? ir : code;
991
+ if (health) {
992
+ return annotateIR(result, health);
993
+ }
994
+ return result;
995
+ }
996
+ function generateL2(delta, health) {
997
+ const parts = [`FILE: ${delta.file}`];
998
+ for (const hunk of delta.hunks) {
999
+ if (hunk.functionScope) parts.push(`SCOPE: ${hunk.functionScope}`);
1000
+ parts.push(`CHANGED: ${hunk.changed.join("\n ")}`);
1001
+ if (hunk.surroundingIR) parts.push(`CONTEXT: ${hunk.surroundingIR}`);
1002
+ if (hunk.blame) {
1003
+ parts.push(`BLAME: ${hunk.blame.author}, ${hunk.blame.date}, commit:"${hunk.blame.commitMessage}"`);
1004
+ }
1005
+ }
1006
+ const ir = parts.join("\n");
1007
+ if (health) return annotateIR(ir, health);
1008
+ return ir;
1009
+ }
1010
+ function generateL3(code, startLine, endLine) {
1011
+ const lines = code.split("\n");
1012
+ return lines.slice(startLine - 1, endLine).join("\n");
1013
+ }
1014
+ async function generateLayer(layer, options) {
1015
+ switch (layer) {
1016
+ case "L0":
1017
+ return generateL0(options.code, options.filePath);
1018
+ case "L1":
1019
+ return generateL1(options.code, options.filePath, options.health);
1020
+ case "L2":
1021
+ if (!options.delta) return generateL1(options.code, options.filePath, options.health);
1022
+ return generateL2(options.delta, options.health);
1023
+ case "L3":
1024
+ if (options.lineRange) {
1025
+ return generateL3(options.code, options.lineRange.start, options.lineRange.end);
1026
+ }
1027
+ return options.code;
1028
+ }
1029
+ }
1030
+
1031
+ // src/trends/git-log-parser.ts
1032
+ import { execSync } from "child_process";
1033
+ var BUG_FIX_PATTERNS = [
1034
+ /\bfix\b/i,
1035
+ /\bbugfix\b/i,
1036
+ /\bhotfix\b/i,
1037
+ /\bpatch\b/i,
1038
+ /\bresolve\b/i,
1039
+ /\bbug\b/i
1040
+ ];
1041
+ function isBugFixCommit(message) {
1042
+ return BUG_FIX_PATTERNS.some((p) => p.test(message));
1043
+ }
1044
+ function parseGitLogOutput(output) {
1045
+ const entries = [];
1046
+ const lines = output.split("\n");
1047
+ let i = 0;
1048
+ while (i < lines.length) {
1049
+ const line = lines[i].trim();
1050
+ if (!line || !line.includes("|")) {
1051
+ i++;
1052
+ continue;
1053
+ }
1054
+ const [hash, author, date, ...messageParts] = line.split("|");
1055
+ const message = messageParts.join("|");
1056
+ const files = [];
1057
+ i++;
1058
+ while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
1059
+ const fileLine = lines[i].trim();
1060
+ if (fileLine) files.push(fileLine);
1061
+ i++;
1062
+ }
1063
+ entries.push({ hash, author, date, message, files });
1064
+ }
1065
+ return entries;
1066
+ }
1067
+ function getGitLog(repoPath, count = 100) {
26
1068
  try {
27
- const entries = readdirSync(dir, { withFileTypes: true });
1069
+ const output = execSync(
1070
+ `git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
1071
+ { cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
1072
+ );
1073
+ return parseGitLogOutput(output);
1074
+ } catch {
1075
+ return [];
1076
+ }
1077
+ }
1078
+
1079
+ // src/trends/hotspot.ts
1080
+ function detectHotspots(entries, options) {
1081
+ const fileStats = /* @__PURE__ */ new Map();
1082
+ for (const entry of entries) {
1083
+ const isFix = isBugFixCommit(entry.message);
1084
+ for (const file of entry.files) {
1085
+ const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
1086
+ stats.changes++;
1087
+ if (isFix) stats.fixes++;
1088
+ stats.authors.add(entry.author);
1089
+ fileStats.set(file, stats);
1090
+ }
1091
+ }
1092
+ const hotspots = [];
1093
+ for (const [file, stats] of fileStats) {
1094
+ const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
1095
+ if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
1096
+ hotspots.push({
1097
+ file,
1098
+ changesInLast30Commits: stats.changes,
1099
+ bugFixRatio: fixRatio,
1100
+ authorCount: stats.authors.size
1101
+ });
1102
+ }
1103
+ }
1104
+ return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
1105
+ }
1106
+
1107
+ // src/trends/decay.ts
1108
+ function detectDecay(entries) {
1109
+ const fileChanges = /* @__PURE__ */ new Map();
1110
+ for (const entry of entries) {
1111
+ for (const file of entry.files) {
1112
+ const changes = fileChanges.get(file) ?? [];
1113
+ changes.push({ date: entry.date });
1114
+ fileChanges.set(file, changes);
1115
+ }
1116
+ }
1117
+ const signals = [];
1118
+ for (const [file, changes] of fileChanges) {
1119
+ if (changes.length < 4) continue;
1120
+ const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
1121
+ const firstDate = new Date(sorted[0].date).getTime();
1122
+ const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
1123
+ const midDate = firstDate + (lastDate - firstDate) / 2;
1124
+ const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
1125
+ const secondHalfCount = sorted.length - firstHalfCount;
1126
+ if (secondHalfCount > firstHalfCount) {
1127
+ signals.push({
1128
+ file,
1129
+ metric: "churn",
1130
+ trend: "declining",
1131
+ dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
1132
+ });
1133
+ }
1134
+ }
1135
+ return signals;
1136
+ }
1137
+
1138
+ // src/trends/inconsistency.ts
1139
+ function detectInconsistencies(entries, minAuthors = 3) {
1140
+ const fileAuthors = /* @__PURE__ */ new Map();
1141
+ for (const entry of entries) {
1142
+ for (const file of entry.files) {
1143
+ const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
1144
+ const commits = authors.get(entry.author) ?? [];
1145
+ commits.push(entry.message);
1146
+ authors.set(entry.author, commits);
1147
+ fileAuthors.set(file, authors);
1148
+ }
1149
+ }
1150
+ const inconsistencies = [];
1151
+ for (const [file, authors] of fileAuthors) {
1152
+ if (authors.size >= minAuthors) {
1153
+ const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
1154
+ author,
1155
+ style: categorizeStyle(commits)
1156
+ }));
1157
+ inconsistencies.push({ file, patterns });
1158
+ }
1159
+ }
1160
+ return inconsistencies;
1161
+ }
1162
+ function categorizeStyle(commits) {
1163
+ const types = commits.map((m) => {
1164
+ if (m.match(/^fix/i)) return "fix";
1165
+ if (m.match(/^feat/i)) return "feature";
1166
+ if (m.match(/^refactor/i)) return "refactor";
1167
+ return "other";
1168
+ });
1169
+ const primary = mode(types);
1170
+ return `primarily ${primary} (${commits.length} commits)`;
1171
+ }
1172
+ function mode(arr) {
1173
+ const counts = /* @__PURE__ */ new Map();
1174
+ for (const item of arr) {
1175
+ counts.set(item, (counts.get(item) ?? 0) + 1);
1176
+ }
1177
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
1178
+ }
1179
+
1180
+ // src/config/loader.ts
1181
+ import { readFileSync, existsSync as existsSync2 } from "fs";
1182
+ import { join } from "path";
1183
+ import { parse } from "yaml";
1184
+ var DEFAULT_CONFIG = {
1185
+ watchers: {
1186
+ security: {
1187
+ enabled: true,
1188
+ severity: { "src/**": "warning", "tests/**": "info" }
1189
+ },
1190
+ deadCode: { enabled: true, trigger: "on-commit" },
1191
+ consoleLog: { enabled: true, severity: { "src/**": "warning", "tests/**": "info" } }
1192
+ },
1193
+ agents: {
1194
+ fixer: { enabled: true, model: "haiku" },
1195
+ reviewer: { enabled: false, model: "sonnet" }
1196
+ },
1197
+ ir: {
1198
+ deltaContextLines: 3,
1199
+ confidenceThreshold: 0.6,
1200
+ genericPatterns: "default"
1201
+ },
1202
+ trends: {
1203
+ enabled: true,
1204
+ hotspotThreshold: 10,
1205
+ bugFixRatioThreshold: 0.5,
1206
+ decayCheckTrigger: "on-commit",
1207
+ fullReportSchedule: "weekly"
1208
+ }
1209
+ };
1210
+ function deepMerge(target, source) {
1211
+ const result = { ...target };
1212
+ for (const key of Object.keys(source)) {
1213
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
1214
+ result[key] = deepMerge(target[key] ?? {}, source[key]);
1215
+ } else {
1216
+ result[key] = source[key];
1217
+ }
1218
+ }
1219
+ return result;
1220
+ }
1221
+ function parseConfig(yamlContent) {
1222
+ if (!yamlContent.trim()) return { ...DEFAULT_CONFIG };
1223
+ const parsed = parse(yamlContent) ?? {};
1224
+ return deepMerge(DEFAULT_CONFIG, parsed);
1225
+ }
1226
+ function loadConfig(projectPath) {
1227
+ const configPath = join(projectPath, ".composto", "config.yaml");
1228
+ if (!existsSync2(configPath)) return { ...DEFAULT_CONFIG };
1229
+ const content = readFileSync(configPath, "utf-8");
1230
+ return parseConfig(content);
1231
+ }
1232
+
1233
+ // src/benchmark/tokenizer.ts
1234
+ function estimateTokens(text) {
1235
+ if (!text) return 0;
1236
+ const tokens = text.split(/[\s]+|(?<=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])|(?=[{}()[\];,.:=<>!&|?+\-*/^~@#$%\\])/).filter(Boolean);
1237
+ return tokens.length;
1238
+ }
1239
+
1240
+ // src/benchmark/runner.ts
1241
+ async function benchmarkFile(code, filePath) {
1242
+ const rawTokens = estimateTokens(code);
1243
+ const irL0 = await generateLayer("L0", { code, filePath, health: null });
1244
+ const irL1 = await generateLayer("L1", { code, filePath, health: null });
1245
+ const irL0Tokens = estimateTokens(irL0);
1246
+ const irL1Tokens = estimateTokens(irL1);
1247
+ const astResult = await astWalkIR(code, filePath);
1248
+ const engine = astResult !== null ? "AST" : "FP";
1249
+ const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
1250
+ return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, engine };
1251
+ }
1252
+ function summarize(results) {
1253
+ const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
1254
+ const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
1255
+ const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
1256
+ const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
1257
+ const astCount = results.filter((r) => r.engine === "AST").length;
1258
+ const fpCount = results.filter((r) => r.engine === "FP").length;
1259
+ return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, astCount, fpCount };
1260
+ }
1261
+
1262
+ // src/context/packer.ts
1263
+ function escapeRegex(s) {
1264
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1265
+ }
1266
+ function findTargetFile(files, target) {
1267
+ const t = escapeRegex(target);
1268
+ const declarationPatterns = [
1269
+ // JS/TS declarations
1270
+ new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${t}\\b`),
1271
+ new RegExp(`(?:export\\s+)?class\\s+${t}\\b`),
1272
+ new RegExp(`(?:export\\s+)?interface\\s+${t}\\b`),
1273
+ new RegExp(`(?:export\\s+)?type\\s+${t}\\b`),
1274
+ new RegExp(`(?:export\\s+)?enum\\s+${t}\\b`),
1275
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${t}\\b`),
1276
+ // Python
1277
+ new RegExp(`\\bdef\\s+${t}\\b`),
1278
+ // Rust
1279
+ new RegExp(`\\bfn\\s+${t}\\b`),
1280
+ new RegExp(`\\bstruct\\s+${t}\\b`),
1281
+ new RegExp(`\\btrait\\s+${t}\\b`),
1282
+ // Go
1283
+ new RegExp(`\\bfunc\\s+${t}\\b`),
1284
+ new RegExp(`\\btype\\s+${t}\\b`),
1285
+ // Object method shorthand
1286
+ new RegExp(`\\b${t}\\s*:\\s*(?:async\\s+)?function\\b`),
1287
+ new RegExp(`\\b${t}\\s*\\([^)]*\\)\\s*\\{`)
1288
+ ];
1289
+ for (const pattern of declarationPatterns) {
1290
+ const match2 = files.find((f) => pattern.test(f.code));
1291
+ if (match2) return match2.path;
1292
+ }
1293
+ const fallback = new RegExp(`\\b${t}\\s*\\(`);
1294
+ const match = files.find((f) => fallback.test(f.code));
1295
+ return match ? match.path : null;
1296
+ }
1297
+ function findRelatedFiles(files, targetPath) {
1298
+ const related = /* @__PURE__ */ new Set();
1299
+ const targetFile = files.find((f) => f.path === targetPath);
1300
+ if (!targetFile) return related;
1301
+ const importPattern = /(?:import|require)\s*(?:\([^)]*|\{[^}]*\}|\w+)?\s*(?:from)?\s*["']([^"']+)["']/g;
1302
+ const imports = [...targetFile.code.matchAll(importPattern)].map((m) => m[1]);
1303
+ for (const imp of imports) {
1304
+ const match = files.find((f) => {
1305
+ const basename = f.path.replace(/\.[^.]+$/, "");
1306
+ return imp.includes(basename) || basename.endsWith(imp.replace(/^\.\.?\//, "").replace(/\.[^.]+$/, ""));
1307
+ });
1308
+ if (match) related.add(match.path);
1309
+ }
1310
+ const targetBasename = targetPath.replace(/\.[^.]+$/, "").split("/").pop() ?? "";
1311
+ for (const file of files) {
1312
+ if (file.path === targetPath) continue;
1313
+ if (file.code.includes(targetBasename)) {
1314
+ related.add(file.path);
1315
+ }
1316
+ }
1317
+ return related;
1318
+ }
1319
+ async function packContext(files, options) {
1320
+ const { budget, hotspots, target } = options;
1321
+ const hotspotSet = new Set(hotspots.map((h) => h.file));
1322
+ let targetPath = null;
1323
+ let relatedFiles = /* @__PURE__ */ new Set();
1324
+ if (target) {
1325
+ targetPath = findTargetFile(files, target);
1326
+ if (targetPath) {
1327
+ relatedFiles = findRelatedFiles(files, targetPath);
1328
+ }
1329
+ }
1330
+ const entries = [];
1331
+ let totalTokens = 0;
1332
+ let filesAtL3 = 0;
1333
+ let targetDowngraded = false;
1334
+ if (targetPath) {
1335
+ const targetFile = files.find((f) => f.path === targetPath);
1336
+ const rawTokens = estimateTokens(targetFile.code);
1337
+ if (rawTokens <= budget * 0.6) {
1338
+ entries.push({
1339
+ path: targetPath,
1340
+ layer: "L3",
1341
+ ir: targetFile.code,
1342
+ tokens: rawTokens,
1343
+ isTarget: true
1344
+ });
1345
+ totalTokens += rawTokens;
1346
+ filesAtL3 = 1;
1347
+ } else {
1348
+ targetDowngraded = true;
1349
+ const l1 = await generateLayer("L1", { code: targetFile.code, filePath: targetFile.path, health: null });
1350
+ const l1Tokens = estimateTokens(l1);
1351
+ entries.push({
1352
+ path: targetPath,
1353
+ layer: "L1",
1354
+ ir: l1,
1355
+ tokens: l1Tokens,
1356
+ isTarget: true
1357
+ });
1358
+ totalTokens += l1Tokens;
1359
+ }
1360
+ }
1361
+ for (const file of files) {
1362
+ if (file.path === targetPath) continue;
1363
+ const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
1364
+ const l0Tokens = estimateTokens(l0);
1365
+ entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
1366
+ totalTokens += l0Tokens;
1367
+ }
1368
+ if (totalTokens > budget) {
1369
+ const truncated = entries.filter((e) => e.isTarget);
1370
+ let used = truncated.reduce((s, e) => s + e.tokens, 0);
28
1371
  for (const entry of entries) {
29
- const fullPath = join(dir, entry.name);
30
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
31
- if (entry.isDirectory()) {
32
- files.push(...collectFiles(fullPath, extensions));
33
- } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
34
- files.push(fullPath);
1372
+ if (entry.isTarget) continue;
1373
+ if (used + entry.tokens <= budget) {
1374
+ truncated.push(entry);
1375
+ used += entry.tokens;
1376
+ }
1377
+ }
1378
+ return {
1379
+ entries: truncated,
1380
+ totalTokens: used,
1381
+ budget,
1382
+ filesAtL0: truncated.filter((e) => e.layer === "L0").length,
1383
+ filesAtL1: truncated.filter((e) => e.layer === "L1").length,
1384
+ filesAtL3,
1385
+ targetFile: targetPath ?? void 0,
1386
+ targetDowngraded
1387
+ };
1388
+ }
1389
+ const upgradeOrder = entries.map((e, i) => ({
1390
+ index: i,
1391
+ path: e.path,
1392
+ rawTokens: files.find((f) => f.path === e.path)?.rawTokens ?? 0,
1393
+ isHotspot: hotspotSet.has(e.path),
1394
+ isRelated: relatedFiles.has(e.path),
1395
+ isTarget: e.isTarget ?? false
1396
+ })).filter((x) => x.isTarget === false && entries[x.index].layer === "L0").sort((a, b) => {
1397
+ if (a.isRelated && !b.isRelated) return -1;
1398
+ if (!a.isRelated && b.isRelated) return 1;
1399
+ if (a.isHotspot && !b.isHotspot) return -1;
1400
+ if (!a.isHotspot && b.isHotspot) return 1;
1401
+ return b.rawTokens - a.rawTokens;
1402
+ });
1403
+ let filesAtL1 = 0;
1404
+ for (const item of upgradeOrder) {
1405
+ const file = files.find((f) => f.path === item.path);
1406
+ const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
1407
+ const l1Tokens = estimateTokens(l1);
1408
+ const currentTokens = entries[item.index].tokens;
1409
+ const additional = l1Tokens - currentTokens;
1410
+ if (totalTokens + additional <= budget) {
1411
+ entries[item.index] = {
1412
+ path: item.path,
1413
+ layer: "L1",
1414
+ ir: l1,
1415
+ tokens: l1Tokens
1416
+ };
1417
+ totalTokens += additional;
1418
+ filesAtL1++;
1419
+ }
1420
+ }
1421
+ return {
1422
+ entries,
1423
+ totalTokens,
1424
+ budget,
1425
+ filesAtL0: entries.filter((e) => e.layer === "L0").length,
1426
+ filesAtL1,
1427
+ filesAtL3,
1428
+ targetFile: targetPath ?? void 0,
1429
+ targetDowngraded
1430
+ };
1431
+ }
1432
+
1433
+ // src/watcher/detector.ts
1434
+ import picomatch from "picomatch";
1435
+ function getSeverity(filePath, severityMap) {
1436
+ for (const [glob, severity] of Object.entries(severityMap)) {
1437
+ if (picomatch.isMatch(filePath, glob)) return severity;
1438
+ }
1439
+ return "info";
1440
+ }
1441
+ var SECRET_PATTERNS = [
1442
+ /["'](sk-[a-zA-Z0-9-]{20,})["']/,
1443
+ /["'](AKIA[0-9A-Z]{16})["']/,
1444
+ /["'](ghp_[a-zA-Z0-9]{36})["']/,
1445
+ /(?:password|secret|token|api_?key)\s*[:=]\s*["']([^"']{8,})["']/i
1446
+ ];
1447
+ function securityRule(code, filePath, severityMap) {
1448
+ const findings = [];
1449
+ const severity = getSeverity(filePath, severityMap);
1450
+ const lines = code.split("\n");
1451
+ for (let i = 0; i < lines.length; i++) {
1452
+ const line = lines[i];
1453
+ if (line.trim().startsWith("//") || line.trim().startsWith("/*")) continue;
1454
+ for (const pattern of SECRET_PATTERNS) {
1455
+ if (pattern.test(line)) {
1456
+ findings.push({
1457
+ watcherId: "security",
1458
+ severity,
1459
+ file: filePath,
1460
+ line: i + 1,
1461
+ message: "Potential hardcoded secret detected",
1462
+ action: {
1463
+ type: "agent-required",
1464
+ agentHint: { role: "fixer", model: "haiku", contextFiles: [filePath] }
1465
+ }
1466
+ });
1467
+ break;
35
1468
  }
36
1469
  }
1470
+ }
1471
+ return findings;
1472
+ }
1473
+ function consoleLogRule(code, filePath, severityMap) {
1474
+ const findings = [];
1475
+ const severity = getSeverity(filePath, severityMap);
1476
+ const lines = code.split("\n");
1477
+ for (let i = 0; i < lines.length; i++) {
1478
+ if (/\bconsole\.(log|debug|info|warn)\b/.test(lines[i])) {
1479
+ findings.push({
1480
+ watcherId: "consoleLog",
1481
+ severity,
1482
+ file: filePath,
1483
+ line: i + 1,
1484
+ message: "console.log detected \u2014 likely debug artifact",
1485
+ action: { type: "auto-fix", autoFix: "remove-line" }
1486
+ });
1487
+ }
1488
+ }
1489
+ return findings;
1490
+ }
1491
+ var RULES = {
1492
+ security: securityRule,
1493
+ consoleLog: consoleLogRule
1494
+ };
1495
+ function runDetector(code, filePath, watcherConfigs) {
1496
+ const findings = [];
1497
+ for (const [name, config] of Object.entries(watcherConfigs)) {
1498
+ if (!config.enabled) continue;
1499
+ const rule = RULES[name];
1500
+ if (rule && config.severity) {
1501
+ findings.push(...rule(code, filePath, config.severity));
1502
+ }
1503
+ }
1504
+ return findings;
1505
+ }
1506
+
1507
+ // src/utils/collectFiles.ts
1508
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
1509
+ import { join as join2, relative } from "path";
1510
+ import ignore from "ignore";
1511
+ var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
1512
+ function loadGitignore(dir) {
1513
+ const ig = ignore();
1514
+ try {
1515
+ const raw = readFileSync2(join2(dir, ".gitignore"), "utf-8");
1516
+ ig.add(raw);
37
1517
  } catch {
38
1518
  }
1519
+ return ig;
1520
+ }
1521
+ function collectFiles(dir, extensions, projectPath) {
1522
+ const root = projectPath ?? dir;
1523
+ const files = [];
1524
+ function walk(currentDir, parentIgnore) {
1525
+ const ig = loadGitignore(currentDir);
1526
+ const merged = ignore().add(parentIgnore).add(ig);
1527
+ try {
1528
+ const entries = readdirSync(currentDir, { withFileTypes: true });
1529
+ for (const entry of entries) {
1530
+ if (entry.name.startsWith(".") || ALWAYS_SKIP.has(entry.name)) continue;
1531
+ const fullPath = join2(currentDir, entry.name);
1532
+ const relPath = relative(root, fullPath);
1533
+ if (merged.ignores(relPath)) continue;
1534
+ if (entry.isDirectory()) {
1535
+ walk(fullPath, merged);
1536
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
1537
+ files.push(fullPath);
1538
+ }
1539
+ }
1540
+ } catch {
1541
+ }
1542
+ }
1543
+ walk(dir, ignore());
39
1544
  return files;
40
1545
  }
1546
+
1547
+ // src/memory/db.ts
1548
+ import Database from "better-sqlite3";
1549
+ import { mkdirSync } from "fs";
1550
+ import { dirname as dirname2 } from "path";
1551
+ function openDatabase(path) {
1552
+ mkdirSync(dirname2(path), { recursive: true });
1553
+ const db = new Database(path);
1554
+ db.pragma("journal_mode = WAL");
1555
+ db.pragma("synchronous = NORMAL");
1556
+ db.pragma("foreign_keys = ON");
1557
+ return db;
1558
+ }
1559
+
1560
+ // src/memory/schema.ts
1561
+ var CURRENT_VERSION = 1;
1562
+ var V1_SQL = `
1563
+ CREATE TABLE IF NOT EXISTS index_state (
1564
+ key TEXT PRIMARY KEY,
1565
+ value TEXT NOT NULL
1566
+ );
1567
+
1568
+ CREATE TABLE IF NOT EXISTS commits (
1569
+ sha TEXT PRIMARY KEY,
1570
+ parent_sha TEXT,
1571
+ author TEXT NOT NULL,
1572
+ timestamp INTEGER NOT NULL,
1573
+ subject TEXT NOT NULL,
1574
+ is_fix INTEGER NOT NULL,
1575
+ is_revert INTEGER NOT NULL,
1576
+ reverts_sha TEXT,
1577
+ FOREIGN KEY (reverts_sha) REFERENCES commits(sha)
1578
+ );
1579
+ CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
1580
+ CREATE INDEX IF NOT EXISTS idx_commits_is_fix ON commits(is_fix) WHERE is_fix = 1;
1581
+
1582
+ CREATE TABLE IF NOT EXISTS file_touches (
1583
+ commit_sha TEXT NOT NULL,
1584
+ file_path TEXT NOT NULL,
1585
+ adds INTEGER NOT NULL,
1586
+ dels INTEGER NOT NULL,
1587
+ change_type TEXT NOT NULL,
1588
+ renamed_from TEXT,
1589
+ PRIMARY KEY (commit_sha, file_path),
1590
+ FOREIGN KEY (commit_sha) REFERENCES commits(sha)
1591
+ );
1592
+ CREATE INDEX IF NOT EXISTS idx_ft_file ON file_touches(file_path);
1593
+
1594
+ CREATE TABLE IF NOT EXISTS symbols (
1595
+ id INTEGER PRIMARY KEY,
1596
+ file_path TEXT NOT NULL,
1597
+ kind TEXT NOT NULL,
1598
+ qualified_name TEXT NOT NULL,
1599
+ first_seen_sha TEXT NOT NULL,
1600
+ last_seen_sha TEXT,
1601
+ UNIQUE (file_path, kind, qualified_name)
1602
+ );
1603
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
1604
+
1605
+ CREATE TABLE IF NOT EXISTS symbol_touches (
1606
+ commit_sha TEXT NOT NULL,
1607
+ symbol_id INTEGER NOT NULL,
1608
+ change_type TEXT NOT NULL,
1609
+ PRIMARY KEY (commit_sha, symbol_id),
1610
+ FOREIGN KEY (commit_sha) REFERENCES commits(sha),
1611
+ FOREIGN KEY (symbol_id) REFERENCES symbols(id)
1612
+ );
1613
+ CREATE INDEX IF NOT EXISTS idx_st_symbol ON symbol_touches(symbol_id);
1614
+
1615
+ CREATE TABLE IF NOT EXISTS fix_links (
1616
+ fix_commit_sha TEXT NOT NULL,
1617
+ suspected_break_sha TEXT NOT NULL,
1618
+ evidence_type TEXT NOT NULL,
1619
+ confidence REAL NOT NULL,
1620
+ window_hours INTEGER,
1621
+ PRIMARY KEY (fix_commit_sha, suspected_break_sha, evidence_type),
1622
+ FOREIGN KEY (fix_commit_sha) REFERENCES commits(sha),
1623
+ FOREIGN KEY (suspected_break_sha) REFERENCES commits(sha)
1624
+ );
1625
+ CREATE INDEX IF NOT EXISTS idx_fl_break ON fix_links(suspected_break_sha);
1626
+
1627
+ CREATE TABLE IF NOT EXISTS signal_calibration (
1628
+ signal_type TEXT PRIMARY KEY,
1629
+ precision REAL NOT NULL,
1630
+ sample_size INTEGER NOT NULL,
1631
+ last_computed_sha TEXT NOT NULL,
1632
+ computed_at INTEGER NOT NULL
1633
+ );
1634
+
1635
+ CREATE TABLE IF NOT EXISTS file_index_state (
1636
+ file_path TEXT PRIMARY KEY,
1637
+ last_commit_indexed TEXT NOT NULL,
1638
+ last_blob_indexed TEXT,
1639
+ indexed_at INTEGER NOT NULL,
1640
+ parse_failed INTEGER NOT NULL DEFAULT 0,
1641
+ FOREIGN KEY (last_commit_indexed) REFERENCES commits(sha)
1642
+ );
1643
+ `;
1644
+ function runMigrations(db) {
1645
+ const current = db.pragma("user_version", { simple: true });
1646
+ if (current >= CURRENT_VERSION) return;
1647
+ db.exec("BEGIN");
1648
+ try {
1649
+ db.exec(V1_SQL);
1650
+ db.pragma(`user_version = ${CURRENT_VERSION}`);
1651
+ db.exec("COMMIT");
1652
+ } catch (err) {
1653
+ db.exec("ROLLBACK");
1654
+ throw err;
1655
+ }
1656
+ }
1657
+
1658
+ // src/memory/git.ts
1659
+ import { execSync as execSync2 } from "child_process";
1660
+ function run(cwd, cmd, timeoutMs = 1e4) {
1661
+ return execSync2(cmd, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
1662
+ }
1663
+ function revParseHead(cwd) {
1664
+ return run(cwd, "git rev-parse HEAD");
1665
+ }
1666
+ function isShallowRepo(cwd) {
1667
+ return run(cwd, "git rev-parse --is-shallow-repository") === "true";
1668
+ }
1669
+ function revListCount(cwd, from, to) {
1670
+ if (from === to) return 0;
1671
+ const out = run(cwd, `git rev-list --count ${from}..${to}`);
1672
+ return parseInt(out, 10);
1673
+ }
1674
+ function isAncestor(cwd, ancestor, descendant) {
1675
+ try {
1676
+ execSync2(`git merge-base --is-ancestor ${ancestor} ${descendant}`, {
1677
+ cwd,
1678
+ stdio: "ignore"
1679
+ });
1680
+ return true;
1681
+ } catch {
1682
+ return false;
1683
+ }
1684
+ }
1685
+ function countCommits(cwd) {
1686
+ const out = run(cwd, "git rev-list --count HEAD");
1687
+ return parseInt(out, 10);
1688
+ }
1689
+
1690
+ // src/memory/freshness.ts
1691
+ function ensureFresh(db, repoPath) {
1692
+ const head = revParseHead(repoPath);
1693
+ const row = db.prepare("SELECT value FROM index_state WHERE key = 'last_indexed_sha'").get();
1694
+ if (!row) {
1695
+ return {
1696
+ tazelik: "bootstrapping",
1697
+ head,
1698
+ delta: { from: null, to: head },
1699
+ behind_by: 0,
1700
+ rewritten: false
1701
+ };
1702
+ }
1703
+ const last = row.value;
1704
+ if (last === head) {
1705
+ return { tazelik: "fresh", head, delta: null, behind_by: 0, rewritten: false };
1706
+ }
1707
+ const reachable = isAncestor(repoPath, last, head);
1708
+ if (!reachable) {
1709
+ return {
1710
+ tazelik: "bootstrapping",
1711
+ head,
1712
+ delta: { from: null, to: head },
1713
+ behind_by: 0,
1714
+ rewritten: true
1715
+ };
1716
+ }
1717
+ const behind_by = revListCount(repoPath, last, head);
1718
+ return {
1719
+ tazelik: "catching_up",
1720
+ head,
1721
+ delta: { from: last, to: head },
1722
+ behind_by,
1723
+ rewritten: false
1724
+ };
1725
+ }
1726
+
1727
+ // src/memory/signals/calibration-lookup.ts
1728
+ function getCalibration(db, type, fallbackPrecision) {
1729
+ const row = db.prepare("SELECT precision, sample_size FROM signal_calibration WHERE signal_type = ?").get(type);
1730
+ if (!row) {
1731
+ return { precision: fallbackPrecision, sampleSize: 0, source: "heuristic" };
1732
+ }
1733
+ return {
1734
+ precision: row.precision,
1735
+ sampleSize: row.sample_size,
1736
+ source: "repo-calibrated"
1737
+ };
1738
+ }
1739
+
1740
+ // src/memory/signals/revert-match.ts
1741
+ var STRENGTH_BY_EVIDENCE = {
1742
+ revert_marker: 1,
1743
+ short_followup_fix: 0.7,
1744
+ same_region_fix_chain: 0.4
1745
+ };
1746
+ var FALLBACK_PRECISION = 0.5;
1747
+ var MAX_EVIDENCE = 5;
1748
+ function computeRevertMatch(db, filePath) {
1749
+ const rows = db.prepare(`
1750
+ SELECT fl.evidence_type, fl.confidence, fl.suspected_break_sha,
1751
+ c.subject, c.timestamp
1752
+ FROM fix_links fl
1753
+ JOIN file_touches ft ON ft.commit_sha = fl.suspected_break_sha
1754
+ JOIN commits c ON c.sha = fl.suspected_break_sha
1755
+ WHERE ft.file_path = ?
1756
+ ORDER BY c.timestamp DESC
1757
+ LIMIT ?
1758
+ `).all(filePath, MAX_EVIDENCE);
1759
+ const cal = getCalibration(db, "revert_match", FALLBACK_PRECISION);
1760
+ if (rows.length === 0) {
1761
+ return {
1762
+ type: "revert_match",
1763
+ strength: 0,
1764
+ precision: cal.precision,
1765
+ sample_size: cal.sampleSize,
1766
+ evidence: []
1767
+ };
1768
+ }
1769
+ let strength = 0;
1770
+ const evidence = [];
1771
+ const now = Math.floor(Date.now() / 1e3);
1772
+ for (const r of rows) {
1773
+ const s = STRENGTH_BY_EVIDENCE[r.evidence_type] ?? 0;
1774
+ if (s > strength) strength = s;
1775
+ evidence.push({
1776
+ commit_sha: r.suspected_break_sha,
1777
+ subject: r.subject,
1778
+ days_ago: Math.floor((now - r.timestamp) / 86400),
1779
+ evidence_type: r.evidence_type
1780
+ });
1781
+ }
1782
+ return {
1783
+ type: "revert_match",
1784
+ strength,
1785
+ precision: cal.precision,
1786
+ sample_size: cal.sampleSize,
1787
+ evidence
1788
+ };
1789
+ }
1790
+
1791
+ // src/memory/signals/hotspot.ts
1792
+ var WINDOW_SECONDS = 90 * 86400;
1793
+ var SATURATION_TOUCHES = 30;
1794
+ var FALLBACK_PRECISION2 = 0.3;
1795
+ function computeHotspot(db, filePath) {
1796
+ const now = Math.floor(Date.now() / 1e3);
1797
+ const lowerBound = now - WINDOW_SECONDS;
1798
+ const row = db.prepare(`
1799
+ SELECT COUNT(*) AS n
1800
+ FROM file_touches ft
1801
+ JOIN commits c ON c.sha = ft.commit_sha
1802
+ WHERE ft.file_path = ? AND c.timestamp >= ?
1803
+ `).get(filePath, lowerBound);
1804
+ const touches = row.n;
1805
+ const strength = Math.min(1, touches / SATURATION_TOUCHES);
1806
+ const cal = getCalibration(db, "hotspot", FALLBACK_PRECISION2);
1807
+ return {
1808
+ type: "hotspot",
1809
+ strength,
1810
+ precision: cal.precision,
1811
+ sample_size: cal.sampleSize,
1812
+ evidence: [],
1813
+ touches_90d: touches
1814
+ };
1815
+ }
1816
+
1817
+ // src/memory/signals/fix-ratio.ts
1818
+ var WINDOW_COMMITS = 30;
1819
+ var DEAD_ZONE = 0.3;
1820
+ var SATURATION_OVER_DEAD_ZONE = 0.5;
1821
+ var FALLBACK_PRECISION3 = 0.3;
1822
+ function computeFixRatio(db, filePath) {
1823
+ const rows = db.prepare(`
1824
+ SELECT c.is_fix
1825
+ FROM file_touches ft
1826
+ JOIN commits c ON c.sha = ft.commit_sha
1827
+ WHERE ft.file_path = ?
1828
+ ORDER BY c.timestamp DESC
1829
+ LIMIT ?
1830
+ `).all(filePath, WINDOW_COMMITS);
1831
+ const cal = getCalibration(db, "fix_ratio", FALLBACK_PRECISION3);
1832
+ if (rows.length === 0) {
1833
+ return {
1834
+ type: "fix_ratio",
1835
+ strength: 0,
1836
+ precision: cal.precision,
1837
+ sample_size: cal.sampleSize,
1838
+ evidence: [],
1839
+ ratio: 0
1840
+ };
1841
+ }
1842
+ const fixes = rows.filter((r) => r.is_fix === 1).length;
1843
+ const ratio = fixes / rows.length;
1844
+ const strength = Math.max(0, Math.min(1, (ratio - DEAD_ZONE) / SATURATION_OVER_DEAD_ZONE));
1845
+ return {
1846
+ type: "fix_ratio",
1847
+ strength,
1848
+ precision: cal.precision,
1849
+ sample_size: cal.sampleSize,
1850
+ evidence: [],
1851
+ ratio
1852
+ };
1853
+ }
1854
+
1855
+ // src/memory/signals/coverage-decline.ts
1856
+ var FALLBACK_PRECISION4 = 0.3;
1857
+ function computeCoverageDecline(db, repoPath, filePath) {
1858
+ const cal = getCalibration(db, "coverage_decline", FALLBACK_PRECISION4);
1859
+ let strength = 0;
1860
+ try {
1861
+ const entries = getGitLog(repoPath, 200);
1862
+ const trends = {
1863
+ hotspots: detectHotspots(entries, { threshold: 10, fixRatioThreshold: 0.5 }),
1864
+ decaySignals: detectDecay(entries),
1865
+ inconsistencies: detectInconsistencies(entries)
1866
+ };
1867
+ const health = computeHealthFromTrends(filePath, trends);
1868
+ if (health.coverageTrend === "down") strength = 1;
1869
+ } catch {
1870
+ strength = 0;
1871
+ }
1872
+ return {
1873
+ type: "coverage_decline",
1874
+ strength,
1875
+ precision: cal.precision,
1876
+ sample_size: cal.sampleSize,
1877
+ evidence: []
1878
+ };
1879
+ }
1880
+
1881
+ // src/memory/signals/author-churn.ts
1882
+ var WINDOW_SECONDS2 = 90 * 86400;
1883
+ var INACTIVE_THRESHOLD = 5;
1884
+ var FALLBACK_PRECISION5 = 0.3;
1885
+ function computeAuthorChurn(db, filePath) {
1886
+ const cal = getCalibration(db, "author_churn", FALLBACK_PRECISION5);
1887
+ const base = {
1888
+ type: "author_churn",
1889
+ precision: cal.precision,
1890
+ sample_size: cal.sampleSize,
1891
+ evidence: []
1892
+ };
1893
+ const lastTouch = db.prepare(`
1894
+ SELECT c.author, c.timestamp
1895
+ FROM file_touches ft
1896
+ JOIN commits c ON c.sha = ft.commit_sha
1897
+ WHERE ft.file_path = ?
1898
+ ORDER BY c.timestamp DESC
1899
+ LIMIT 1
1900
+ `).get(filePath);
1901
+ if (!lastTouch) return { ...base, strength: 0 };
1902
+ const now = Math.floor(Date.now() / 1e3);
1903
+ const lowerBound = now - WINDOW_SECONDS2;
1904
+ const activity = db.prepare(`SELECT COUNT(*) AS n FROM commits WHERE author = ? AND timestamp >= ?`).get(lastTouch.author, lowerBound);
1905
+ let strength = 0;
1906
+ if (activity.n === 0) strength = 1;
1907
+ else if (activity.n < INACTIVE_THRESHOLD) strength = 0.5;
1908
+ return { ...base, strength };
1909
+ }
1910
+
1911
+ // src/memory/signals/index.ts
1912
+ function collectSignals(db, repoPath, filePath) {
1913
+ return [
1914
+ computeRevertMatch(db, filePath),
1915
+ computeHotspot(db, filePath),
1916
+ computeFixRatio(db, filePath),
1917
+ computeCoverageDecline(db, repoPath, filePath),
1918
+ computeAuthorChurn(db, filePath)
1919
+ ];
1920
+ }
1921
+
1922
+ // src/memory/confidence.ts
1923
+ var USABLE_SAMPLE_THRESHOLD = 20;
1924
+ function coverageFactor(signals) {
1925
+ const usable = signals.filter(
1926
+ (s) => s.strength > 0 && s.sample_size >= USABLE_SAMPLE_THRESHOLD
1927
+ ).length;
1928
+ return Math.min(1, usable / 3);
1929
+ }
1930
+ function calibrationFactor(signals) {
1931
+ const firing = signals.filter((s) => s.strength > 0);
1932
+ if (firing.length === 0) return 1;
1933
+ const avg = firing.reduce((acc, s) => acc + s.sample_size, 0) / firing.length;
1934
+ if (avg < 20) return 0.3;
1935
+ if (avg < 100) return 0.6;
1936
+ return 1;
1937
+ }
1938
+ function freshnessFactor(ctx) {
1939
+ if (ctx.partial) return 0.4;
1940
+ switch (ctx.tazelik) {
1941
+ case "fresh":
1942
+ return 1;
1943
+ case "catching_up":
1944
+ return 0.8;
1945
+ case "partial":
1946
+ return 0.4;
1947
+ case "bootstrapping":
1948
+ return 0.2;
1949
+ }
1950
+ }
1951
+ function historyFactor(totalCommits) {
1952
+ if (totalCommits < 50) return 0.2;
1953
+ if (totalCommits < 200) return 0.5;
1954
+ if (totalCommits < 1e3) return 0.8;
1955
+ return 1;
1956
+ }
1957
+ function computeScoreAndConfidence(signals, ctx) {
1958
+ let num = 0;
1959
+ let den = 0;
1960
+ for (const s of signals) {
1961
+ if (s.strength <= 0 || s.precision <= 0) continue;
1962
+ num += s.strength * s.precision;
1963
+ den += s.precision;
1964
+ }
1965
+ const score = den === 0 ? 0 : num / den;
1966
+ const confidence = Math.min(
1967
+ coverageFactor(signals),
1968
+ calibrationFactor(signals),
1969
+ freshnessFactor(ctx),
1970
+ historyFactor(ctx.totalCommits)
1971
+ );
1972
+ return { score, confidence };
1973
+ }
1974
+
1975
+ // src/memory/verdict.ts
1976
+ function mapVerdict(score, confidence) {
1977
+ if (confidence < 0.3) return "unknown";
1978
+ if (score < 0.3) return "low";
1979
+ if (score < 0.6) return "medium";
1980
+ return "high";
1981
+ }
1982
+
1983
+ // src/memory/envelope.ts
1984
+ var CONFIDENCE_CAP = {
1985
+ ok: 1,
1986
+ empty_repo: 0,
1987
+ insufficient_history: 0.3,
1988
+ shallow_clone: 0,
1989
+ indexing: 0.4,
1990
+ squashed_history: 0.5,
1991
+ reindexing: 0,
1992
+ internal_error: 0,
1993
+ disabled: 0
1994
+ };
1995
+ var USABLE_SAMPLE_THRESHOLD2 = 20;
1996
+ function inferCalibrationSource(signals) {
1997
+ return signals.some((s) => s.sample_size > 0) ? "repo-calibrated" : "heuristic";
1998
+ }
1999
+ function buildEnvelope(args) {
2000
+ const cap = CONFIDENCE_CAP[args.status];
2001
+ const cappedConfidence = Math.min(args.confidence, cap);
2002
+ const verdict = mapVerdict(args.score, cappedConfidence);
2003
+ const usable = args.signals.filter(
2004
+ (s) => s.strength > 0 && s.sample_size >= USABLE_SAMPLE_THRESHOLD2
2005
+ ).length;
2006
+ return {
2007
+ status: args.status,
2008
+ reason: args.reason,
2009
+ verdict,
2010
+ score: args.score,
2011
+ confidence: cappedConfidence,
2012
+ signals: args.signals,
2013
+ calibration: inferCalibrationSource(args.signals),
2014
+ retry_hint_ms: args.retry_hint_ms,
2015
+ confidence_cap: args.status === "ok" ? void 0 : cap,
2016
+ metadata: {
2017
+ tazelik: args.tazelik,
2018
+ index_version: 1,
2019
+ indexed_commits_through: args.indexedThrough,
2020
+ indexed_commits_total: args.indexedTotal,
2021
+ query_ms: args.queryMs,
2022
+ signal_coverage: `${usable}/${args.signals.length}`
2023
+ }
2024
+ };
2025
+ }
2026
+
2027
+ // src/memory/pool.ts
2028
+ import { Worker } from "worker_threads";
2029
+ import { fileURLToPath as fileURLToPath2 } from "url";
2030
+ import { dirname as dirname3, join as join3 } from "path";
2031
+ function resolveWorkerPath() {
2032
+ const here = dirname3(fileURLToPath2(import.meta.url));
2033
+ if (here.endsWith("/memory") || here.endsWith("\\memory")) {
2034
+ return join3(here, "worker.js");
2035
+ }
2036
+ return join3(here, "memory", "worker.js");
2037
+ }
2038
+ var WorkerPool = class {
2039
+ workers = [];
2040
+ nextJobId = 1;
2041
+ pending = /* @__PURE__ */ new Map();
2042
+ constructor(opts = {}) {
2043
+ const size = Math.max(1, opts.size ?? 1);
2044
+ for (let i = 0; i < size; i++) this.spawn();
2045
+ }
2046
+ spawn() {
2047
+ const worker = new Worker(resolveWorkerPath());
2048
+ worker.on("message", (msg) => {
2049
+ const job = this.pending.get(msg.jobId);
2050
+ if (!job) return;
2051
+ this.pending.delete(msg.jobId);
2052
+ if (msg.type === "ingest_done") {
2053
+ job.resolve({ status: "done", commits: msg.commits });
2054
+ } else if (msg.type === "ingest_error") {
2055
+ job.reject(new Error(msg.message));
2056
+ }
2057
+ });
2058
+ worker.on("error", (err) => {
2059
+ const error = err instanceof Error ? err : new Error(String(err));
2060
+ for (const job of this.pending.values()) job.reject(error);
2061
+ this.pending.clear();
2062
+ });
2063
+ this.workers.push(worker);
2064
+ }
2065
+ runIngest(args) {
2066
+ const jobId = this.nextJobId++;
2067
+ const worker = this.workers[jobId % this.workers.length];
2068
+ return new Promise((resolve3, reject) => {
2069
+ this.pending.set(jobId, { resolve: resolve3, reject });
2070
+ worker.postMessage({ type: "ingest", jobId, ...args });
2071
+ });
2072
+ }
2073
+ async close() {
2074
+ await Promise.all(this.workers.map((w) => w.terminate()));
2075
+ this.workers = [];
2076
+ this.pending.clear();
2077
+ }
2078
+ };
2079
+
2080
+ // src/memory/detectors.ts
2081
+ function detectSquashed(db) {
2082
+ const row = db.prepare(`
2083
+ SELECT author, COUNT(*) AS n, MIN(timestamp) AS t0, MAX(timestamp) AS t1
2084
+ FROM commits
2085
+ GROUP BY author
2086
+ ORDER BY n DESC
2087
+ LIMIT 1
2088
+ `).get();
2089
+ if (!row || row.n < 50) return false;
2090
+ const span = row.t1 - row.t0;
2091
+ return span < 86400;
2092
+ }
2093
+
2094
+ // src/memory/failure-tracker.ts
2095
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
2096
+ import { join as join4 } from "path";
2097
+ var STRIKE_THRESHOLD = 3;
2098
+ var WINDOW_SECONDS3 = 300;
2099
+ function createFailureTracker(composto_dir) {
2100
+ const path = join4(composto_dir, "failures.json");
2101
+ try {
2102
+ mkdirSync2(composto_dir, { recursive: true });
2103
+ } catch {
2104
+ }
2105
+ function load() {
2106
+ try {
2107
+ const raw = readFileSync3(path, "utf-8");
2108
+ return JSON.parse(raw);
2109
+ } catch {
2110
+ return { failures: [], disabled: false };
2111
+ }
2112
+ }
2113
+ function save(s) {
2114
+ try {
2115
+ writeFileSync(path, JSON.stringify(s), "utf-8");
2116
+ } catch {
2117
+ }
2118
+ }
2119
+ function now() {
2120
+ return Math.floor(Date.now() / 1e3);
2121
+ }
2122
+ return {
2123
+ recordFailure: (failureClass) => {
2124
+ const s = load();
2125
+ s.failures.push({ class: failureClass, t: now() });
2126
+ s.failures = s.failures.filter((f) => now() - f.t <= WINDOW_SECONDS3);
2127
+ const sameClass = s.failures.filter((f) => f.class === failureClass);
2128
+ if (sameClass.length >= STRIKE_THRESHOLD) s.disabled = true;
2129
+ save(s);
2130
+ },
2131
+ recordSuccess: () => {
2132
+ save({ failures: [], disabled: false });
2133
+ },
2134
+ isDisabled: () => {
2135
+ return load().disabled;
2136
+ }
2137
+ };
2138
+ }
2139
+
2140
+ // src/memory/log.ts
2141
+ import { appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync, statSync, unlinkSync } from "fs";
2142
+ import { join as join5 } from "path";
2143
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
2144
+ var RETENTION_DAYS = 7;
2145
+ function currentThreshold() {
2146
+ const raw = (process.env.COMPOSTO_LOG ?? "info").toLowerCase();
2147
+ if (raw === "debug" || raw === "info" || raw === "warn" || raw === "error") return raw;
2148
+ return "info";
2149
+ }
2150
+ function rotateIfNeeded(dir) {
2151
+ const logPath = join5(dir, "index.log");
2152
+ try {
2153
+ const s = statSync(logPath);
2154
+ const age = (Date.now() - s.mtimeMs) / 864e5;
2155
+ if (age < 1) return;
2156
+ const files = readdirSync2(dir).filter((f) => /^index\.log(\.\d+)?$/.test(f));
2157
+ const numbered = files.map((f) => {
2158
+ const m = f.match(/^index\.log\.(\d+)$/);
2159
+ return { name: f, n: m ? parseInt(m[1], 10) : 0 };
2160
+ }).sort((a, b) => b.n - a.n);
2161
+ for (const f of numbered) {
2162
+ if (f.n >= RETENTION_DAYS) {
2163
+ unlinkSync(join5(dir, f.name));
2164
+ continue;
2165
+ }
2166
+ if (f.n === 0) {
2167
+ renameSync(join5(dir, f.name), join5(dir, "index.log.1"));
2168
+ } else {
2169
+ renameSync(join5(dir, f.name), join5(dir, `index.log.${f.n + 1}`));
2170
+ }
2171
+ }
2172
+ } catch {
2173
+ }
2174
+ }
2175
+ function createLogger(composto_dir) {
2176
+ let disabled = false;
2177
+ try {
2178
+ mkdirSync3(composto_dir, { recursive: true });
2179
+ rotateIfNeeded(composto_dir);
2180
+ } catch {
2181
+ disabled = true;
2182
+ }
2183
+ const path = join5(composto_dir, "index.log");
2184
+ const threshold = currentThreshold();
2185
+ function write(level, evt, extras) {
2186
+ if (disabled) return;
2187
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[threshold]) return;
2188
+ const line = JSON.stringify({
2189
+ t: Math.floor(Date.now() / 1e3),
2190
+ lvl: level,
2191
+ evt,
2192
+ ...extras ?? {}
2193
+ });
2194
+ try {
2195
+ appendFileSync(path, line + "\n", "utf-8");
2196
+ } catch {
2197
+ disabled = true;
2198
+ }
2199
+ }
2200
+ return {
2201
+ debug: (evt, extras) => write("debug", evt, extras),
2202
+ info: (evt, extras) => write("info", evt, extras),
2203
+ warn: (evt, extras) => write("warn", evt, extras),
2204
+ error: (evt, extras) => write("error", evt, extras),
2205
+ close: () => {
2206
+ }
2207
+ };
2208
+ }
2209
+
2210
+ // src/memory/api.ts
2211
+ import { dirname as dirname4 } from "path";
2212
+ var EMPTY_REPO_THRESHOLD = 10;
2213
+ var MemoryAPI = class {
2214
+ db;
2215
+ pool;
2216
+ dbPath;
2217
+ repoPath;
2218
+ compostoDir;
2219
+ log;
2220
+ failures;
2221
+ bootstrapPromise = null;
2222
+ constructor(opts) {
2223
+ this.dbPath = opts.dbPath;
2224
+ this.repoPath = opts.repoPath;
2225
+ this.compostoDir = dirname4(opts.dbPath);
2226
+ this.log = createLogger(this.compostoDir);
2227
+ this.failures = createFailureTracker(this.compostoDir);
2228
+ this.db = openDatabase(opts.dbPath);
2229
+ runMigrations(this.db);
2230
+ this.pool = new WorkerPool({ size: opts.workerPoolSize ?? 1 });
2231
+ this.log.info("api_open", { dbPath: opts.dbPath });
2232
+ }
2233
+ async bootstrapIfNeeded() {
2234
+ if (this.bootstrapPromise) return this.bootstrapPromise;
2235
+ const fresh = ensureFresh(this.db, this.repoPath);
2236
+ if (fresh.tazelik === "fresh" || !fresh.delta) return;
2237
+ this.bootstrapPromise = this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range: fresh.delta }).then(() => {
2238
+ this.log.info("bootstrap_done", { through: fresh.delta?.to });
2239
+ }).catch((err) => {
2240
+ this.log.error("bootstrap_failed", { message: err.message });
2241
+ this.failures.recordFailure("ingest_failure");
2242
+ throw err;
2243
+ }).finally(() => {
2244
+ this.bootstrapPromise = null;
2245
+ });
2246
+ return this.bootstrapPromise;
2247
+ }
2248
+ async blastradius(input) {
2249
+ const start = Date.now();
2250
+ if (this.failures.isDisabled()) {
2251
+ this.log.warn("call_on_disabled", { file: input.file });
2252
+ return buildEnvelope({
2253
+ status: "disabled",
2254
+ signals: [],
2255
+ score: 0,
2256
+ confidence: 0,
2257
+ tazelik: "fresh",
2258
+ indexedThrough: "",
2259
+ indexedTotal: 0,
2260
+ queryMs: Date.now() - start,
2261
+ reason: "tool disabled after repeated failures; clear .composto/failures.json to re-enable"
2262
+ });
2263
+ }
2264
+ try {
2265
+ return await this.runQuery(input, start);
2266
+ } catch (err) {
2267
+ const message = err instanceof Error ? err.message : String(err);
2268
+ this.log.error("internal_error", { file: input.file, message });
2269
+ this.failures.recordFailure("internal_error");
2270
+ return buildEnvelope({
2271
+ status: "internal_error",
2272
+ signals: [],
2273
+ score: 0,
2274
+ confidence: 0,
2275
+ tazelik: "fresh",
2276
+ indexedThrough: "",
2277
+ indexedTotal: 0,
2278
+ queryMs: Date.now() - start,
2279
+ reason: `internal error: ${message}; see .composto/index.log`
2280
+ });
2281
+ }
2282
+ }
2283
+ async runQuery(input, start) {
2284
+ if (isShallowRepo(this.repoPath)) {
2285
+ return buildEnvelope({
2286
+ status: "shallow_clone",
2287
+ signals: [],
2288
+ score: 0,
2289
+ confidence: 0,
2290
+ tazelik: "fresh",
2291
+ indexedThrough: "",
2292
+ indexedTotal: 0,
2293
+ queryMs: Date.now() - start,
2294
+ reason: "shallow clone detected; run `git fetch --unshallow` or `composto index --deepen`"
2295
+ });
2296
+ }
2297
+ const totalCommits = countCommits(this.repoPath);
2298
+ if (totalCommits < EMPTY_REPO_THRESHOLD) {
2299
+ return buildEnvelope({
2300
+ status: "empty_repo",
2301
+ signals: [],
2302
+ score: 0,
2303
+ confidence: 0,
2304
+ tazelik: "fresh",
2305
+ indexedThrough: "",
2306
+ indexedTotal: totalCommits,
2307
+ queryMs: Date.now() - start,
2308
+ reason: `repo has ${totalCommits} commits; blastradius requires >= ${EMPTY_REPO_THRESHOLD}`
2309
+ });
2310
+ }
2311
+ const fresh = ensureFresh(this.db, this.repoPath);
2312
+ let status = "ok";
2313
+ if (fresh.rewritten) {
2314
+ status = "reindexing";
2315
+ this.log.warn("history_rewritten", { last_indexed: fresh.head });
2316
+ }
2317
+ if (fresh.tazelik === "bootstrapping") {
2318
+ await this.bootstrapIfNeeded();
2319
+ } else if (fresh.tazelik === "catching_up" && fresh.delta) {
2320
+ this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range: fresh.delta }).catch((err) => {
2321
+ this.log.error("delta_ingest_failed", { message: err.message });
2322
+ });
2323
+ }
2324
+ if (status === "ok" && detectSquashed(this.db)) {
2325
+ status = "squashed_history";
2326
+ }
2327
+ const indexedTotalRow = this.db.prepare("SELECT value FROM index_state WHERE key='indexed_commits_total'").get();
2328
+ const indexedTotal = indexedTotalRow ? parseInt(indexedTotalRow.value, 10) : 0;
2329
+ const indexedThrough = this.db.prepare("SELECT value FROM index_state WHERE key='last_indexed_sha'").get()?.value ?? "";
2330
+ const signals = collectSignals(this.db, this.repoPath, input.file);
2331
+ const tazelik = fresh.tazelik === "bootstrapping" ? "fresh" : fresh.tazelik;
2332
+ const { score, confidence } = computeScoreAndConfidence(signals, {
2333
+ tazelik,
2334
+ partial: false,
2335
+ totalCommits: indexedTotal
2336
+ });
2337
+ const response = buildEnvelope({
2338
+ status,
2339
+ signals,
2340
+ score,
2341
+ confidence,
2342
+ tazelik,
2343
+ indexedThrough,
2344
+ indexedTotal,
2345
+ queryMs: Date.now() - start
2346
+ });
2347
+ this.log.info("query", {
2348
+ file: input.file,
2349
+ status: response.status,
2350
+ verdict: response.verdict,
2351
+ confidence: response.confidence,
2352
+ query_ms: response.metadata.query_ms
2353
+ });
2354
+ this.failures.recordSuccess();
2355
+ return response;
2356
+ }
2357
+ async close() {
2358
+ this.log.info("api_close", {});
2359
+ this.log.close();
2360
+ this.db.close();
2361
+ await this.pool.close();
2362
+ }
2363
+ };
2364
+
2365
+ // src/mcp/server.ts
2366
+ var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
41
2367
  var server = new McpServer({
42
2368
  name: "composto",
43
- version: "0.2.3"
2369
+ version: "0.4.1"
44
2370
  });
45
2371
  server.tool(
46
2372
  "composto_ir",
@@ -50,10 +2376,10 @@ server.tool(
50
2376
  layer: z.enum(["L0", "L1", "L2", "L3"]).default("L1").describe("L0=structure only, L1=full IR (default), L2=delta context, L3=raw source")
51
2377
  },
52
2378
  async ({ file, layer }) => {
53
- const filePath = resolve(file);
54
- const code = readFileSync(filePath, "utf-8");
55
- const projectPath = resolve(".");
56
- const relPath = relative(projectPath, filePath);
2379
+ const filePath = resolve2(file);
2380
+ const code = readFileSync4(filePath, "utf-8");
2381
+ const projectPath = resolve2(".");
2382
+ const relPath = relative2(projectPath, filePath);
57
2383
  const config = loadConfig(projectPath);
58
2384
  const entries = getGitLog(projectPath, 100);
59
2385
  const trends = {
@@ -85,12 +2411,12 @@ server.tool(
85
2411
  path: z.string().default(".").describe("Directory to benchmark")
86
2412
  },
87
2413
  async ({ path }) => {
88
- const projectPath = resolve(path);
2414
+ const projectPath = resolve2(path);
89
2415
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
90
2416
  const results = [];
91
2417
  for (const file of files) {
92
- const code = readFileSync(file, "utf-8");
93
- const relPath = relative(projectPath, file);
2418
+ const code = readFileSync4(file, "utf-8");
2419
+ const relPath = relative2(projectPath, file);
94
2420
  results.push(await benchmarkFile(code, relPath));
95
2421
  }
96
2422
  results.sort((a, b) => b.savedPercent - a.savedPercent);
@@ -119,7 +2445,7 @@ server.tool(
119
2445
  target: z.string().optional().describe("Target symbol (function/class/variable name). Its file will be included as raw code for implementation tasks.")
120
2446
  },
121
2447
  async ({ path, budget, target }) => {
122
- const projectPath = resolve(path);
2448
+ const projectPath = resolve2(path);
123
2449
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
124
2450
  const config = loadConfig(projectPath);
125
2451
  const entries = getGitLog(projectPath, 100);
@@ -128,8 +2454,8 @@ server.tool(
128
2454
  fixRatioThreshold: config.trends.bugFixRatioThreshold
129
2455
  });
130
2456
  const fileInputs = files.map((file) => {
131
- const code = readFileSync(file, "utf-8");
132
- const relPath = relative(projectPath, file);
2457
+ const code = readFileSync4(file, "utf-8");
2458
+ const relPath = relative2(projectPath, file);
133
2459
  return { path: relPath, code, rawTokens: estimateTokens(code) };
134
2460
  });
135
2461
  const result = await packContext(fileInputs, { budget, hotspots, target });
@@ -180,13 +2506,13 @@ server.tool(
180
2506
  path: z.string().default(".").describe("Directory to scan")
181
2507
  },
182
2508
  async ({ path }) => {
183
- const projectPath = resolve(path);
2509
+ const projectPath = resolve2(path);
184
2510
  const config = loadConfig(projectPath);
185
2511
  const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
186
2512
  const allFindings = [];
187
2513
  for (const file of files) {
188
- const code = readFileSync(file, "utf-8");
189
- const relPath = relative(projectPath, file);
2514
+ const code = readFileSync4(file, "utf-8");
2515
+ const relPath = relative2(projectPath, file);
190
2516
  const findings = runDetector(code, relPath, config.watchers);
191
2517
  allFindings.push(...findings);
192
2518
  }
@@ -206,6 +2532,36 @@ server.tool(
206
2532
  };
207
2533
  }
208
2534
  );
2535
+ server.tool(
2536
+ "composto_blastradius",
2537
+ 'Predict the historical blast radius of a code change before applying it. Returns a risk verdict (low/medium/high/unknown), confidence, and the git-derived signals behind it (revert history, hotspots, fix ratio, coverage decline, ownership churn). Call BEFORE proposing significant edits to files with non-trivial history. Honest about uncertainty \u2014 returns "unknown" when confidence is low instead of guessing. Degraded modes (empty repo, shallow clone, indexing) are explicit in the `status` field.',
2538
+ {
2539
+ file: z.string().describe("Repo-relative path of the file the agent intends to modify."),
2540
+ intent: z.enum(["refactor", "bugfix", "feature", "test", "docs", "unknown"]).default("unknown").optional(),
2541
+ level: z.enum(["summary", "detail"]).default("summary").optional(),
2542
+ diff: z.string().optional().describe("Optional unified diff. When present, narrows blast radius to actually-touched symbols (Plan 4).")
2543
+ },
2544
+ async ({ file, intent, level, diff }) => {
2545
+ if (process.env.COMPOSTO_BLASTRADIUS !== "1") {
2546
+ return {
2547
+ content: [{ type: "text", text: JSON.stringify({
2548
+ status: "disabled",
2549
+ reason: "composto_blastradius is gated by COMPOSTO_BLASTRADIUS=1 during Plan 1 rollout."
2550
+ }) }]
2551
+ };
2552
+ }
2553
+ const projectPath = resolve2(".");
2554
+ const dbPath = join6(projectPath, ".composto", "memory.db");
2555
+ const api = new MemoryAPI({ dbPath, repoPath: projectPath });
2556
+ try {
2557
+ await api.bootstrapIfNeeded();
2558
+ const res = await api.blastradius({ file, intent, level, diff });
2559
+ return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
2560
+ } finally {
2561
+ await api.close();
2562
+ }
2563
+ }
2564
+ );
209
2565
  async function main() {
210
2566
  const transport = new StdioServerTransport();
211
2567
  await server.connect(transport);