composto-ai 0.1.2 → 0.2.0

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