composto-ai 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -192
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/tree-sitter-rust.wasm +0 -0
- package/dist/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/index.js +775 -46
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +9 -2
package/dist/index.js
CHANGED
|
@@ -170,12 +170,24 @@ function extractStructure(code) {
|
|
|
170
170
|
|
|
171
171
|
// src/ir/fingerprint.ts
|
|
172
172
|
var PATTERNS = [
|
|
173
|
+
// import type { x, y } from "module"
|
|
174
|
+
{
|
|
175
|
+
match: /^import\s+type\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
176
|
+
transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
|
|
177
|
+
confidence: 0.95
|
|
178
|
+
},
|
|
173
179
|
// import { x, y } from "module"
|
|
174
180
|
{
|
|
175
181
|
match: /^import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
176
182
|
transform: (m) => `USE:${m[2]}{${m[1].replace(/\s/g, "")}}`,
|
|
177
183
|
confidence: 0.95
|
|
178
184
|
},
|
|
185
|
+
// import type x from "module"
|
|
186
|
+
{
|
|
187
|
+
match: /^import\s+type\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
188
|
+
transform: (m) => `USE:${m[2]}{${m[1]}}`,
|
|
189
|
+
confidence: 0.95
|
|
190
|
+
},
|
|
179
191
|
// import x from "module"
|
|
180
192
|
{
|
|
181
193
|
match: /^import\s+(\w+)\s+from\s+["']([^"']+)["'];?\s*$/,
|
|
@@ -254,11 +266,110 @@ var PATTERNS = [
|
|
|
254
266
|
transform: (m) => `CATCH:${m[1]}`,
|
|
255
267
|
confidence: 0.9
|
|
256
268
|
},
|
|
269
|
+
// switch (expr) {
|
|
270
|
+
{
|
|
271
|
+
match: /^switch\s*\(([^)]+)\)\s*\{?\s*$/,
|
|
272
|
+
transform: (m) => `SWITCH:${m[1].trim()}`,
|
|
273
|
+
confidence: 0.9
|
|
274
|
+
},
|
|
275
|
+
// case "value": / case value:
|
|
276
|
+
{
|
|
277
|
+
match: /^case\s+(.+)\s*:\s*$/,
|
|
278
|
+
transform: (m) => `CASE:${m[1].trim()}`,
|
|
279
|
+
confidence: 0.9
|
|
280
|
+
},
|
|
281
|
+
// default:
|
|
282
|
+
{
|
|
283
|
+
match: /^default\s*:\s*$/,
|
|
284
|
+
transform: () => "DEFAULT:",
|
|
285
|
+
confidence: 0.9
|
|
286
|
+
},
|
|
287
|
+
// export type Name = ...
|
|
288
|
+
{
|
|
289
|
+
match: /^export\s+type\s+(\w+)(?:<[^>]+>)?\s*=\s*(.+);?\s*$/,
|
|
290
|
+
transform: (m) => `OUT TYPE:${m[1]}`,
|
|
291
|
+
confidence: 0.9
|
|
292
|
+
},
|
|
293
|
+
// if (cond) expr; (inline if with method call)
|
|
294
|
+
{
|
|
295
|
+
match: /^if\s*\(([^)]+)\)\s+(\w+.+);?\s*$/,
|
|
296
|
+
transform: (m) => `IF:${m[1].trim()} -> ${m[2].replace(/;$/, "").trim().slice(0, 50)}`,
|
|
297
|
+
confidence: 0.9
|
|
298
|
+
},
|
|
299
|
+
// export async function name( (multiline signature)
|
|
300
|
+
{
|
|
301
|
+
match: /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(\s*$/,
|
|
302
|
+
transform: (m) => `OUT FN:${m[1]}(`,
|
|
303
|
+
confidence: 0.95
|
|
304
|
+
},
|
|
305
|
+
// interface/type property — name: type;
|
|
306
|
+
{
|
|
307
|
+
match: /^\s*(\w+)\??\s*:\s*(.+);?\s*$/,
|
|
308
|
+
transform: (m) => `PROP:${m[1]}: ${m[2].replace(/;$/, "").trim()}`,
|
|
309
|
+
confidence: 0.75
|
|
310
|
+
},
|
|
311
|
+
// const x = await expr;
|
|
312
|
+
{
|
|
313
|
+
match: /^(?:export\s+)?(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*await\s+(.+);?\s*$/,
|
|
314
|
+
transform: (m) => {
|
|
315
|
+
const prefix = m[0].startsWith("export") ? "OUT " : "";
|
|
316
|
+
return `${prefix}AWAIT:VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
|
|
317
|
+
},
|
|
318
|
+
confidence: 0.85
|
|
319
|
+
},
|
|
320
|
+
// export const name = async (params) => { OR export const name = (params) => expr;
|
|
321
|
+
{
|
|
322
|
+
match: /^export\s+(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
|
|
323
|
+
transform: (m) => {
|
|
324
|
+
const asyncPrefix = m[2] ? "ASYNC " : "";
|
|
325
|
+
const body = m[4].replace(/[{;]\s*$/, "").trim();
|
|
326
|
+
return `OUT ${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
|
|
327
|
+
},
|
|
328
|
+
confidence: 0.9
|
|
329
|
+
},
|
|
330
|
+
// const name = async (params) => { OR const name = (params) => expr;
|
|
331
|
+
{
|
|
332
|
+
match: /^(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*(.*)$/,
|
|
333
|
+
transform: (m) => {
|
|
334
|
+
const asyncPrefix = m[2] ? "ASYNC " : "";
|
|
335
|
+
const body = m[4].replace(/[{;]\s*$/, "").trim();
|
|
336
|
+
return `${asyncPrefix}FN:${m[1]} = (${m[3].trim()}) => ${body || "{"}`;
|
|
337
|
+
},
|
|
338
|
+
confidence: 0.9
|
|
339
|
+
},
|
|
340
|
+
// get name() {
|
|
341
|
+
{
|
|
342
|
+
match: /^\s*get\s+(\w+)\s*\(\)\s*(?::\s*\S+\s*)?\{?\s*$/,
|
|
343
|
+
transform: (m) => `GET:${m[1]}()`,
|
|
344
|
+
confidence: 0.9
|
|
345
|
+
},
|
|
346
|
+
// set name(value) {
|
|
347
|
+
{
|
|
348
|
+
match: /^\s*set\s+(\w+)\s*\(([^)]*)\)\s*\{?\s*$/,
|
|
349
|
+
transform: (m) => `SET:${m[1]}(${m[2].replace(/\s/g, "")})`,
|
|
350
|
+
confidence: 0.9
|
|
351
|
+
},
|
|
352
|
+
// methodName(params) { (inside class body, indented)
|
|
353
|
+
{
|
|
354
|
+
match: /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*\S+\s*)?\{\s*$/,
|
|
355
|
+
transform: (m) => {
|
|
356
|
+
const name = m[1];
|
|
357
|
+
if (["if", "for", "while", "switch", "catch", "function"].includes(name)) return `${name}`;
|
|
358
|
+
return `METHOD:${name}(${m[2].replace(/\s/g, "")})`;
|
|
359
|
+
},
|
|
360
|
+
confidence: 0.9
|
|
361
|
+
},
|
|
362
|
+
// const { a, b } = expr (object destructuring — before regular assignment)
|
|
363
|
+
{
|
|
364
|
+
match: /^(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(.+);?\s*$/,
|
|
365
|
+
transform: (m) => `VAR:{${m[1].replace(/\s/g, "")}} = ${m[2].replace(/;$/, "").trim()}`,
|
|
366
|
+
confidence: 0.9
|
|
367
|
+
},
|
|
257
368
|
// const [a, b] = expr (destructuring — before regular assignment)
|
|
258
369
|
{
|
|
259
370
|
match: /^(?:const|let|var)\s+\[([^\]]+)\]\s*=\s*(.+);?\s*$/,
|
|
260
371
|
transform: (m) => `VAR:[${m[1].replace(/\s/g, "")}] = ${m[2].replace(/;$/, "").trim()}`,
|
|
261
|
-
confidence: 0.
|
|
372
|
+
confidence: 0.9
|
|
262
373
|
},
|
|
263
374
|
// const name = value;
|
|
264
375
|
{
|
|
@@ -267,7 +378,7 @@ var PATTERNS = [
|
|
|
267
378
|
const prefix = m[0].startsWith("export") ? "OUT " : "";
|
|
268
379
|
return `${prefix}VAR:${m[1]} = ${m[2].replace(/;$/, "").trim()}`;
|
|
269
380
|
},
|
|
270
|
-
confidence: 0.
|
|
381
|
+
confidence: 0.85
|
|
271
382
|
}
|
|
272
383
|
];
|
|
273
384
|
function fingerprintLine(line) {
|
|
@@ -293,7 +404,7 @@ function fingerprintLine(line) {
|
|
|
293
404
|
};
|
|
294
405
|
}
|
|
295
406
|
}
|
|
296
|
-
return { type: "raw", ir: trimmed, confidence: 0.
|
|
407
|
+
return { type: "raw", ir: trimmed, confidence: 0.1 };
|
|
297
408
|
}
|
|
298
409
|
function fingerprintFile(code, confidenceThreshold = 0.6) {
|
|
299
410
|
const lines = code.split("\n");
|
|
@@ -305,8 +416,6 @@ function fingerprintFile(code, confidenceThreshold = 0.6) {
|
|
|
305
416
|
if (result.ir === "") continue;
|
|
306
417
|
if (result.confidence >= confidenceThreshold) {
|
|
307
418
|
irLines.push(`${indentStr}${result.ir}`);
|
|
308
|
-
} else {
|
|
309
|
-
irLines.push(`${indentStr}${result.ir}`);
|
|
310
419
|
}
|
|
311
420
|
}
|
|
312
421
|
return irLines.join("\n");
|
|
@@ -344,6 +453,419 @@ function computeHealthFromTrends(file, trends) {
|
|
|
344
453
|
};
|
|
345
454
|
}
|
|
346
455
|
|
|
456
|
+
// src/parser/init.ts
|
|
457
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
458
|
+
import { resolve, dirname } from "path";
|
|
459
|
+
import { existsSync as existsSync2 } from "fs";
|
|
460
|
+
import { fileURLToPath } from "url";
|
|
461
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
462
|
+
var initialized = false;
|
|
463
|
+
var cache = /* @__PURE__ */ new Map();
|
|
464
|
+
function grammarPath(lang) {
|
|
465
|
+
const distPath = resolve(__dirname, "grammars", `tree-sitter-${lang}.wasm`);
|
|
466
|
+
if (existsSync2(distPath)) return distPath;
|
|
467
|
+
const devPath = resolve(__dirname, "../../grammars", `tree-sitter-${lang}.wasm`);
|
|
468
|
+
if (existsSync2(devPath)) return devPath;
|
|
469
|
+
throw new Error(`Grammar not found for ${lang}`);
|
|
470
|
+
}
|
|
471
|
+
async function getParser(lang) {
|
|
472
|
+
if (!initialized) {
|
|
473
|
+
await Parser.init();
|
|
474
|
+
initialized = true;
|
|
475
|
+
}
|
|
476
|
+
const cached = cache.get(lang);
|
|
477
|
+
if (cached) return cached;
|
|
478
|
+
const parser = new Parser();
|
|
479
|
+
const language = await Language.load(grammarPath(lang));
|
|
480
|
+
parser.setLanguage(language);
|
|
481
|
+
const result = { parser, language };
|
|
482
|
+
cache.set(lang, result);
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/parser/languages.ts
|
|
487
|
+
import { extname } from "path";
|
|
488
|
+
var EXT_MAP = {
|
|
489
|
+
".ts": "typescript",
|
|
490
|
+
".tsx": "typescript",
|
|
491
|
+
".js": "javascript",
|
|
492
|
+
".jsx": "javascript",
|
|
493
|
+
".mjs": "javascript",
|
|
494
|
+
".py": "python",
|
|
495
|
+
".go": "go",
|
|
496
|
+
".rs": "rust"
|
|
497
|
+
};
|
|
498
|
+
var SUPPORTED_EXTENSIONS = Object.keys(EXT_MAP);
|
|
499
|
+
function detectLanguage(filePath) {
|
|
500
|
+
const ext = extname(filePath);
|
|
501
|
+
return EXT_MAP[ext] ?? null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/ir/ast-walker.ts
|
|
505
|
+
var TIER_MAP = {
|
|
506
|
+
// Tier 1 — structural declarations
|
|
507
|
+
import_statement: "T1_KEEP",
|
|
508
|
+
function_declaration: "T1_KEEP",
|
|
509
|
+
class_declaration: "T1_KEEP",
|
|
510
|
+
interface_declaration: "T1_KEEP",
|
|
511
|
+
type_alias_declaration: "T1_KEEP",
|
|
512
|
+
enum_declaration: "T1_KEEP",
|
|
513
|
+
// Tier 2 — control flow
|
|
514
|
+
if_statement: "T2_CONTROL",
|
|
515
|
+
else_clause: "WALK_ONLY",
|
|
516
|
+
for_statement: "T2_CONTROL",
|
|
517
|
+
for_in_statement: "T2_CONTROL",
|
|
518
|
+
while_statement: "T2_CONTROL",
|
|
519
|
+
do_statement: "T2_CONTROL",
|
|
520
|
+
switch_statement: "T2_CONTROL",
|
|
521
|
+
switch_case: "T2_CONTROL",
|
|
522
|
+
switch_default: "T2_CONTROL",
|
|
523
|
+
return_statement: "T2_CONTROL",
|
|
524
|
+
throw_statement: "T2_CONTROL",
|
|
525
|
+
try_statement: "T2_CONTROL",
|
|
526
|
+
catch_clause: "T2_CONTROL",
|
|
527
|
+
// Tier 3 — compressible expressions
|
|
528
|
+
lexical_declaration: "T3_COMPRESS",
|
|
529
|
+
expression_statement: "T3_COMPRESS",
|
|
530
|
+
// Walk-only — containers that need traversal but no emission
|
|
531
|
+
program: "WALK_ONLY",
|
|
532
|
+
statement_block: "WALK_ONLY",
|
|
533
|
+
class_body: "WALK_ONLY",
|
|
534
|
+
switch_body: "WALK_ONLY",
|
|
535
|
+
export_statement: "WALK_ONLY"
|
|
536
|
+
};
|
|
537
|
+
function tierOf(nodeType) {
|
|
538
|
+
return TIER_MAP[nodeType] ?? "T4_DROP";
|
|
539
|
+
}
|
|
540
|
+
function collapseText(text, maxLen) {
|
|
541
|
+
const collapsed = text.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
542
|
+
if (collapsed.length <= maxLen) return collapsed;
|
|
543
|
+
return collapsed.slice(0, maxLen - 3) + "...";
|
|
544
|
+
}
|
|
545
|
+
function getTypeParams(node) {
|
|
546
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
547
|
+
const child = node.child(i);
|
|
548
|
+
if (child.type === "type_parameters") {
|
|
549
|
+
return child.text;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
function isExported(node) {
|
|
555
|
+
return node.parent?.type === "export_statement";
|
|
556
|
+
}
|
|
557
|
+
function isAsync(node) {
|
|
558
|
+
return node.text.trimStart().startsWith("async");
|
|
559
|
+
}
|
|
560
|
+
function extractCondition(node) {
|
|
561
|
+
const condNode = node.childForFieldName("condition") ?? (() => {
|
|
562
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
563
|
+
const c = node.child(i);
|
|
564
|
+
if (c.type === "parenthesized_expression") return c;
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
})();
|
|
568
|
+
if (!condNode) return "...";
|
|
569
|
+
const text = condNode.text.replace(/^\(/, "").replace(/\)$/, "").trim();
|
|
570
|
+
return text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
571
|
+
}
|
|
572
|
+
function emitTier2(node) {
|
|
573
|
+
switch (node.type) {
|
|
574
|
+
case "if_statement": {
|
|
575
|
+
const cond = extractCondition(node);
|
|
576
|
+
return `IF:${cond}`;
|
|
577
|
+
}
|
|
578
|
+
case "else_clause":
|
|
579
|
+
return "ELSE:";
|
|
580
|
+
case "for_statement":
|
|
581
|
+
case "for_in_statement":
|
|
582
|
+
return "LOOP";
|
|
583
|
+
case "while_statement": {
|
|
584
|
+
const cond = extractCondition(node);
|
|
585
|
+
return `WHILE:${cond}`;
|
|
586
|
+
}
|
|
587
|
+
case "do_statement": {
|
|
588
|
+
const cond = extractCondition(node);
|
|
589
|
+
return `WHILE:${cond}`;
|
|
590
|
+
}
|
|
591
|
+
case "switch_statement": {
|
|
592
|
+
const expr = node.childForFieldName("value") ?? node.childForFieldName("condition") ?? (() => {
|
|
593
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
594
|
+
const c = node.child(i);
|
|
595
|
+
if (c.type === "parenthesized_expression") return c;
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
})();
|
|
599
|
+
const text = expr ? expr.text.replace(/^\(/, "").replace(/\)$/, "").trim() : "...";
|
|
600
|
+
return `SWITCH:${text.length > 60 ? text.slice(0, 57) + "..." : text}`;
|
|
601
|
+
}
|
|
602
|
+
case "switch_case": {
|
|
603
|
+
let value = null;
|
|
604
|
+
const valNode = node.childForFieldName("value");
|
|
605
|
+
if (valNode) {
|
|
606
|
+
value = valNode.text;
|
|
607
|
+
} else {
|
|
608
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
609
|
+
const c = node.child(i);
|
|
610
|
+
if (c.type !== "case" && c.type !== ":" && c.childCount === 0 && c.text === "case") continue;
|
|
611
|
+
if (c.type !== "case" && c.text !== "case" && c.text !== ":") {
|
|
612
|
+
value = c.text;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return `CASE:${value ?? "..."}`;
|
|
618
|
+
}
|
|
619
|
+
case "switch_default":
|
|
620
|
+
return "DEFAULT:";
|
|
621
|
+
case "return_statement": {
|
|
622
|
+
let retText = "";
|
|
623
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
624
|
+
const c = node.child(i);
|
|
625
|
+
if (c.text !== "return" && c.text !== ";") {
|
|
626
|
+
retText += (retText ? " " : "") + c.text;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
retText = retText.replace(/\s*\n\s*/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
630
|
+
if (!retText) return "RET";
|
|
631
|
+
return `RET ${retText.length > 60 ? retText.slice(0, 57) + "..." : retText}`;
|
|
632
|
+
}
|
|
633
|
+
case "throw_statement": {
|
|
634
|
+
let throwText = "";
|
|
635
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
636
|
+
const c = node.child(i);
|
|
637
|
+
if (c.text !== "throw" && c.text !== ";") {
|
|
638
|
+
throwText += (throwText ? " " : "") + c.text;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
throwText = throwText.trim();
|
|
642
|
+
return `THROW:${throwText.length > 60 ? throwText.slice(0, 57) + "..." : throwText}`;
|
|
643
|
+
}
|
|
644
|
+
case "try_statement":
|
|
645
|
+
return "TRY";
|
|
646
|
+
case "catch_clause": {
|
|
647
|
+
const param = node.childForFieldName("parameter");
|
|
648
|
+
const paramText = param ? param.text : "...";
|
|
649
|
+
return `CATCH:${paramText}`;
|
|
650
|
+
}
|
|
651
|
+
default:
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function emitTier1(node) {
|
|
656
|
+
const exported = isExported(node);
|
|
657
|
+
const outPrefix = exported ? "OUT " : "";
|
|
658
|
+
switch (node.type) {
|
|
659
|
+
case "import_statement": {
|
|
660
|
+
const text = collapseText(node.text, 80);
|
|
661
|
+
return `USE:${text}`;
|
|
662
|
+
}
|
|
663
|
+
case "function_declaration": {
|
|
664
|
+
const name = node.childForFieldName("name")?.text ?? "anonymous";
|
|
665
|
+
const rawParams = node.childForFieldName("parameters")?.text ?? "()";
|
|
666
|
+
const params = collapseText(rawParams, 60);
|
|
667
|
+
const asyncPrefix = isAsync(node) ? "ASYNC " : "";
|
|
668
|
+
return `${outPrefix}${asyncPrefix}FN:${name}${params}`;
|
|
669
|
+
}
|
|
670
|
+
case "class_declaration": {
|
|
671
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
672
|
+
const typeParams = getTypeParams(node);
|
|
673
|
+
return `${outPrefix}CLASS:${name}${typeParams}`;
|
|
674
|
+
}
|
|
675
|
+
case "interface_declaration": {
|
|
676
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
677
|
+
const typeParams = getTypeParams(node);
|
|
678
|
+
return `${outPrefix}INTERFACE:${name}${typeParams}`;
|
|
679
|
+
}
|
|
680
|
+
case "type_alias_declaration": {
|
|
681
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
682
|
+
return `${outPrefix}TYPE:${name}`;
|
|
683
|
+
}
|
|
684
|
+
case "enum_declaration": {
|
|
685
|
+
const name = node.childForFieldName("name")?.text ?? "Anonymous";
|
|
686
|
+
return `${outPrefix}ENUM:${name}`;
|
|
687
|
+
}
|
|
688
|
+
default:
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function emitTier3(node) {
|
|
693
|
+
switch (node.type) {
|
|
694
|
+
case "lexical_declaration": {
|
|
695
|
+
let declarator = null;
|
|
696
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
697
|
+
const c = node.child(i);
|
|
698
|
+
if (c.type === "variable_declarator") {
|
|
699
|
+
declarator = c;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!declarator) return null;
|
|
704
|
+
const name = declarator.childForFieldName("name")?.text ?? "?";
|
|
705
|
+
const value = declarator.childForFieldName("value");
|
|
706
|
+
if (value) {
|
|
707
|
+
if (value.type === "arrow_function") {
|
|
708
|
+
const asyncPrefix = isAsync(value) ? "ASYNC " : "";
|
|
709
|
+
const params = value.childForFieldName("parameters")?.text ?? "()";
|
|
710
|
+
return `${asyncPrefix}FN:${name}${collapseText(params, 60)} => ...`;
|
|
711
|
+
}
|
|
712
|
+
if (value.type === "await_expression") {
|
|
713
|
+
const callee = value.childCount > 1 ? value.child(1).text : "...";
|
|
714
|
+
return `AWAIT:${name}=${collapseText(callee, 40)}`;
|
|
715
|
+
}
|
|
716
|
+
if (node.parent?.type === "statement_block") return null;
|
|
717
|
+
const vt = value.type;
|
|
718
|
+
if (vt === "number" || vt === "true" || vt === "false") return null;
|
|
719
|
+
if (vt === "object" || vt === "array") return null;
|
|
720
|
+
if (vt === "new_expression" || vt === "call_expression") return null;
|
|
721
|
+
const valText = value.text.replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, "''").replace(/`[^`]*`/g, "``");
|
|
722
|
+
return `VAR:${name} = ${collapseText(valText, 50)}`;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
case "expression_statement": {
|
|
727
|
+
const expr = node.child(0);
|
|
728
|
+
if (!expr) return null;
|
|
729
|
+
if (expr.type === "await_expression") {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
if (expr.type === "call_expression") {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
default:
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function walkNode(node, depth, lines) {
|
|
742
|
+
const tier = tierOf(node.type);
|
|
743
|
+
switch (tier) {
|
|
744
|
+
case "T1_KEEP": {
|
|
745
|
+
const ir = emitTier1(node);
|
|
746
|
+
if (ir) lines.push(ir);
|
|
747
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
748
|
+
const child = node.child(i);
|
|
749
|
+
const childType = child.type;
|
|
750
|
+
if (childType === "statement_block" || childType === "class_body") {
|
|
751
|
+
walkNode(child, depth + 1, lines);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case "T2_CONTROL": {
|
|
757
|
+
if (depth > 4 && node.type !== "return_statement" && node.type !== "throw_statement" && node.type !== "switch_case" && node.type !== "switch_default") break;
|
|
758
|
+
if (node.type === "if_statement") {
|
|
759
|
+
let hasElse = false;
|
|
760
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
761
|
+
if (node.child(i).type === "else_clause") {
|
|
762
|
+
hasElse = true;
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!hasElse) {
|
|
767
|
+
const body = node.childForFieldName("consequence") ?? (() => {
|
|
768
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
769
|
+
const c = node.child(i);
|
|
770
|
+
if (c.type === "statement_block") return c;
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
773
|
+
})();
|
|
774
|
+
if (body) {
|
|
775
|
+
let singleStmt = null;
|
|
776
|
+
if (body.type === "statement_block") {
|
|
777
|
+
const stmts = [];
|
|
778
|
+
for (let i = 0; i < body.childCount; i++) {
|
|
779
|
+
const c = body.child(i);
|
|
780
|
+
if (c.type !== "{" && c.type !== "}") stmts.push(c);
|
|
781
|
+
}
|
|
782
|
+
if (stmts.length === 1) singleStmt = stmts[0];
|
|
783
|
+
} else if (body.type === "return_statement" || body.type === "throw_statement") {
|
|
784
|
+
singleStmt = body;
|
|
785
|
+
}
|
|
786
|
+
if (singleStmt && (singleStmt.type === "return_statement" || singleStmt.type === "throw_statement")) {
|
|
787
|
+
const cond = extractCondition(node);
|
|
788
|
+
const retLine = emitTier2(singleStmt);
|
|
789
|
+
if (retLine) {
|
|
790
|
+
const indent2 = " ".repeat(depth);
|
|
791
|
+
lines.push(`${indent2}IF:${cond} \u2192 ${retLine}`);
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const line = emitTier2(node);
|
|
799
|
+
const indent = " ".repeat(depth);
|
|
800
|
+
if (line) lines.push(indent + line);
|
|
801
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
802
|
+
walkNode(node.child(i), depth + 1, lines);
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "T3_COMPRESS": {
|
|
807
|
+
if (depth > 4) break;
|
|
808
|
+
const line = emitTier3(node);
|
|
809
|
+
const indent = " ".repeat(depth);
|
|
810
|
+
if (line) lines.push(indent + line);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case "WALK_ONLY": {
|
|
814
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
815
|
+
const child = node.child(i);
|
|
816
|
+
if (node.type === "export_statement") {
|
|
817
|
+
if (child.type === "export" || child.type === "default" || child.text === "export" || child.text === "default") {
|
|
818
|
+
if (child.childCount === 0 && (child.text === "export" || child.text === "default")) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
walkNode(child, depth + 1, lines);
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "T4_DROP":
|
|
828
|
+
default:
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function astWalkIR(code, filePath) {
|
|
833
|
+
const lang = detectLanguage(filePath);
|
|
834
|
+
if (!lang) return null;
|
|
835
|
+
const { parser } = await getParser(lang);
|
|
836
|
+
const tree = parser.parse(code);
|
|
837
|
+
const root = tree.rootNode;
|
|
838
|
+
const lines = [];
|
|
839
|
+
walkNode(root, 0, lines);
|
|
840
|
+
if (lines.length === 0) return null;
|
|
841
|
+
const merged = [];
|
|
842
|
+
let useBlock = [];
|
|
843
|
+
for (const line of lines) {
|
|
844
|
+
if (line.startsWith("USE:")) {
|
|
845
|
+
const m = line.match(/from\s+["']([^"']+)["']/);
|
|
846
|
+
useBlock.push(m ? m[1] : line.slice(4));
|
|
847
|
+
} else {
|
|
848
|
+
if (useBlock.length > 0) {
|
|
849
|
+
if (useBlock.length <= 3) {
|
|
850
|
+
for (const mod of useBlock) merged.push(`USE:${mod}`);
|
|
851
|
+
} else {
|
|
852
|
+
merged.push(`USE:[${useBlock.join(", ")}]`);
|
|
853
|
+
}
|
|
854
|
+
useBlock = [];
|
|
855
|
+
}
|
|
856
|
+
merged.push(line);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (useBlock.length > 0) {
|
|
860
|
+
if (useBlock.length <= 3) {
|
|
861
|
+
for (const mod of useBlock) merged.push(`USE:${mod}`);
|
|
862
|
+
} else {
|
|
863
|
+
merged.push(`USE:[${useBlock.join(", ")}]`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return merged.join("\n");
|
|
867
|
+
}
|
|
868
|
+
|
|
347
869
|
// src/ir/layers.ts
|
|
348
870
|
function generateL0(code, filePath) {
|
|
349
871
|
const structure = extractStructure(code);
|
|
@@ -359,8 +881,8 @@ function generateL0(code, filePath) {
|
|
|
359
881
|
return `${filePath}
|
|
360
882
|
${declarations.join("\n")}`;
|
|
361
883
|
}
|
|
362
|
-
function generateL1(code, health) {
|
|
363
|
-
const ir = fingerprintFile(code, 0.
|
|
884
|
+
async function generateL1(code, filePath, health) {
|
|
885
|
+
const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
|
|
364
886
|
if (health) {
|
|
365
887
|
return annotateIR(ir, health);
|
|
366
888
|
}
|
|
@@ -384,14 +906,14 @@ function generateL3(code, startLine, endLine) {
|
|
|
384
906
|
const lines = code.split("\n");
|
|
385
907
|
return lines.slice(startLine - 1, endLine).join("\n");
|
|
386
908
|
}
|
|
387
|
-
function generateLayer(layer, options) {
|
|
909
|
+
async function generateLayer(layer, options) {
|
|
388
910
|
switch (layer) {
|
|
389
911
|
case "L0":
|
|
390
912
|
return generateL0(options.code, options.filePath);
|
|
391
913
|
case "L1":
|
|
392
|
-
return generateL1(options.code, options.health);
|
|
914
|
+
return generateL1(options.code, options.filePath, options.health);
|
|
393
915
|
case "L2":
|
|
394
|
-
if (!options.delta) return generateL1(options.code, options.health);
|
|
916
|
+
if (!options.delta) return generateL1(options.code, options.filePath, options.health);
|
|
395
917
|
return generateL2(options.delta, options.health);
|
|
396
918
|
case "L3":
|
|
397
919
|
if (options.lineRange) {
|
|
@@ -654,33 +1176,141 @@ function estimateTokens(text) {
|
|
|
654
1176
|
}
|
|
655
1177
|
|
|
656
1178
|
// src/benchmark/runner.ts
|
|
657
|
-
function benchmarkFile(code, filePath) {
|
|
1179
|
+
async function benchmarkFile(code, filePath) {
|
|
658
1180
|
const rawTokens = estimateTokens(code);
|
|
659
|
-
const irL0 = generateLayer("L0", { code, filePath, health: null });
|
|
660
|
-
const irL1 = generateLayer("L1", { code, filePath, health: null });
|
|
1181
|
+
const irL0 = await generateLayer("L0", { code, filePath, health: null });
|
|
1182
|
+
const irL1 = await generateLayer("L1", { code, filePath, health: null });
|
|
661
1183
|
const irL0Tokens = estimateTokens(irL0);
|
|
662
1184
|
const irL1Tokens = estimateTokens(irL1);
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
let count = 0;
|
|
666
|
-
for (const line of lines) {
|
|
667
|
-
const result = fingerprintLine(line);
|
|
668
|
-
if (result.ir !== "") {
|
|
669
|
-
totalConf += result.confidence;
|
|
670
|
-
count++;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
1185
|
+
const astResult = await astWalkIR(code, filePath);
|
|
1186
|
+
const engine = astResult !== null ? "AST" : "FP";
|
|
673
1187
|
const savedPercent = rawTokens > 0 ? (rawTokens - irL1Tokens) / rawTokens * 100 : 0;
|
|
674
|
-
|
|
675
|
-
return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, avgConfidence };
|
|
1188
|
+
return { file: filePath, rawTokens, irL0Tokens, irL1Tokens, savedPercent, engine };
|
|
676
1189
|
}
|
|
677
1190
|
function summarize(results) {
|
|
678
1191
|
const totalRaw = results.reduce((s, r) => s + r.rawTokens, 0);
|
|
679
1192
|
const totalIRL0 = results.reduce((s, r) => s + r.irL0Tokens, 0);
|
|
680
1193
|
const totalIRL1 = results.reduce((s, r) => s + r.irL1Tokens, 0);
|
|
681
1194
|
const totalSavedPercent = totalRaw > 0 ? (totalRaw - totalIRL1) / totalRaw * 100 : 0;
|
|
682
|
-
const
|
|
683
|
-
|
|
1195
|
+
const astCount = results.filter((r) => r.engine === "AST").length;
|
|
1196
|
+
const fpCount = results.filter((r) => r.engine === "FP").length;
|
|
1197
|
+
return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, astCount, fpCount };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/benchmark/quality.ts
|
|
1201
|
+
var BENCHMARK_PROMPTS = [
|
|
1202
|
+
{
|
|
1203
|
+
id: "understand",
|
|
1204
|
+
label: "Comprehension",
|
|
1205
|
+
template: "What does this code do? List the main functions/classes and briefly describe each one's purpose and dependencies.\n\n{code}"
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
id: "fix-bug",
|
|
1209
|
+
label: "Bug Detection",
|
|
1210
|
+
template: "Review this code for potential bugs, edge cases, or error handling issues. List any problems you find.\n\n{code}"
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
id: "review",
|
|
1214
|
+
label: "Code Review",
|
|
1215
|
+
template: "Do a code review of this file. Comment on code quality, naming, structure, and any improvements you'd suggest.\n\n{code}"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
id: "explain",
|
|
1219
|
+
label: "Explanation",
|
|
1220
|
+
template: "Explain this code to a developer who is new to the codebase. Focus on how the pieces fit together.\n\n{code}"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
id: "refactor",
|
|
1224
|
+
label: "Refactoring",
|
|
1225
|
+
template: "How would you refactor this code for better maintainability and testability? Suggest specific changes.\n\n{code}"
|
|
1226
|
+
}
|
|
1227
|
+
];
|
|
1228
|
+
async function askClaude(context, prompt, apiKey) {
|
|
1229
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
1230
|
+
const client = new Anthropic({ apiKey });
|
|
1231
|
+
const userMessage = prompt.replace("{code}", context);
|
|
1232
|
+
const estimatedInput = estimateTokens(userMessage);
|
|
1233
|
+
const start = performance.now();
|
|
1234
|
+
const response = await client.messages.create({
|
|
1235
|
+
model: "claude-haiku-4-5-20251001",
|
|
1236
|
+
max_tokens: 1024,
|
|
1237
|
+
messages: [{ role: "user", content: userMessage }]
|
|
1238
|
+
});
|
|
1239
|
+
const elapsed = performance.now() - start;
|
|
1240
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
1241
|
+
const text = textBlock?.text ?? "";
|
|
1242
|
+
return {
|
|
1243
|
+
label: "",
|
|
1244
|
+
inputTokens: response.usage.input_tokens,
|
|
1245
|
+
outputTokens: response.usage.output_tokens,
|
|
1246
|
+
totalTokens: response.usage.input_tokens + response.usage.output_tokens,
|
|
1247
|
+
responseTimeMs: elapsed,
|
|
1248
|
+
response: text,
|
|
1249
|
+
usageInput: response.usage.input_tokens,
|
|
1250
|
+
usageOutput: response.usage.output_tokens
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async function runQualityBenchmark(code, filePath, apiKey, promptId = "understand") {
|
|
1254
|
+
const irL1 = await generateLayer("L1", { code, filePath, health: null });
|
|
1255
|
+
const prompt = BENCHMARK_PROMPTS.find((p) => p.id === promptId) ?? BENCHMARK_PROMPTS[0];
|
|
1256
|
+
const [rawResult, irResult] = await Promise.all([
|
|
1257
|
+
askClaude(code, prompt.template, apiKey),
|
|
1258
|
+
askClaude(irL1, prompt.template, apiKey)
|
|
1259
|
+
]);
|
|
1260
|
+
rawResult.label = "Raw Code";
|
|
1261
|
+
irResult.label = "IR (L1)";
|
|
1262
|
+
const savedPercent = rawResult.totalTokens > 0 ? (rawResult.totalTokens - irResult.totalTokens) / rawResult.totalTokens * 100 : 0;
|
|
1263
|
+
return { file: filePath, raw: rawResult, ir: irResult, savedPercent };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/context/packer.ts
|
|
1267
|
+
async function packContext(files, options) {
|
|
1268
|
+
const { budget, hotspots } = options;
|
|
1269
|
+
const hotspotSet = new Set(hotspots.map((h) => h.file));
|
|
1270
|
+
const entries = [];
|
|
1271
|
+
let totalTokens = 0;
|
|
1272
|
+
for (const file of files) {
|
|
1273
|
+
const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
|
|
1274
|
+
const l0Tokens = estimateTokens(l0);
|
|
1275
|
+
entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
|
|
1276
|
+
totalTokens += l0Tokens;
|
|
1277
|
+
}
|
|
1278
|
+
if (totalTokens > budget) {
|
|
1279
|
+
const truncated = [];
|
|
1280
|
+
let used = 0;
|
|
1281
|
+
for (const entry of entries) {
|
|
1282
|
+
if (used + entry.tokens <= budget) {
|
|
1283
|
+
truncated.push(entry);
|
|
1284
|
+
used += entry.tokens;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return { entries: truncated, totalTokens: used, budget, filesAtL0: truncated.length, filesAtL1: 0 };
|
|
1288
|
+
}
|
|
1289
|
+
const upgradeOrder = entries.map((e, i) => ({ index: i, path: e.path, rawTokens: files[i].rawTokens, isHotspot: hotspotSet.has(e.path) })).sort((a, b) => {
|
|
1290
|
+
if (a.isHotspot && !b.isHotspot) return -1;
|
|
1291
|
+
if (!a.isHotspot && b.isHotspot) return 1;
|
|
1292
|
+
return b.rawTokens - a.rawTokens;
|
|
1293
|
+
});
|
|
1294
|
+
let filesAtL1 = 0;
|
|
1295
|
+
for (const item of upgradeOrder) {
|
|
1296
|
+
const file = files[item.index];
|
|
1297
|
+
const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
|
|
1298
|
+
const l1Tokens = estimateTokens(l1);
|
|
1299
|
+
const currentL0Tokens = entries[item.index].tokens;
|
|
1300
|
+
const additionalTokens = l1Tokens - currentL0Tokens;
|
|
1301
|
+
if (totalTokens + additionalTokens <= budget) {
|
|
1302
|
+
entries[item.index] = { path: item.path, layer: "L1", ir: l1, tokens: l1Tokens };
|
|
1303
|
+
totalTokens += additionalTokens;
|
|
1304
|
+
filesAtL1++;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
entries,
|
|
1309
|
+
totalTokens,
|
|
1310
|
+
budget,
|
|
1311
|
+
filesAtL0: entries.length - filesAtL1,
|
|
1312
|
+
filesAtL1
|
|
1313
|
+
};
|
|
684
1314
|
}
|
|
685
1315
|
|
|
686
1316
|
// src/cli/commands.ts
|
|
@@ -749,7 +1379,7 @@ function runTrends(projectPath) {
|
|
|
749
1379
|
};
|
|
750
1380
|
adapter.notify({ type: "trend-report", data: trends });
|
|
751
1381
|
}
|
|
752
|
-
function runIR(projectPath, filePath, layer) {
|
|
1382
|
+
async function runIR(projectPath, filePath, layer) {
|
|
753
1383
|
const config = loadConfig(projectPath);
|
|
754
1384
|
const code = readFileSync2(filePath, "utf-8");
|
|
755
1385
|
const relPath = relative(projectPath, filePath);
|
|
@@ -764,25 +1394,27 @@ function runIR(projectPath, filePath, layer) {
|
|
|
764
1394
|
};
|
|
765
1395
|
const health = computeHealthFromTrends(relPath, trends);
|
|
766
1396
|
const irLayer = layer || "L1";
|
|
767
|
-
const result = generateLayer(irLayer, {
|
|
1397
|
+
const result = await generateLayer(irLayer, {
|
|
768
1398
|
code,
|
|
769
1399
|
filePath: relPath,
|
|
770
1400
|
health: health.churn > 0 ? health : null
|
|
771
1401
|
});
|
|
772
1402
|
console.log(result);
|
|
773
1403
|
}
|
|
774
|
-
|
|
1404
|
+
var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
|
|
1405
|
+
async function runBenchmark(projectPath) {
|
|
775
1406
|
console.log("composto v0.1.0 \u2014 benchmark\n");
|
|
776
|
-
const files = collectFiles(projectPath,
|
|
1407
|
+
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
777
1408
|
console.log(` ${files.length} files
|
|
778
1409
|
`);
|
|
779
|
-
const results =
|
|
1410
|
+
const results = [];
|
|
1411
|
+
for (const file of files) {
|
|
780
1412
|
const code = readFileSync2(file, "utf-8");
|
|
781
1413
|
const relPath = relative(projectPath, file);
|
|
782
|
-
|
|
783
|
-
}
|
|
1414
|
+
results.push(await benchmarkFile(code, relPath));
|
|
1415
|
+
}
|
|
784
1416
|
results.sort((a, b) => b.savedPercent - a.savedPercent);
|
|
785
|
-
const header = " File Raw L0 L1 Saved
|
|
1417
|
+
const header = " File Raw L0 L1 Saved Eng";
|
|
786
1418
|
const divider = " " + "\u2500".repeat(header.length - 2);
|
|
787
1419
|
console.log(header);
|
|
788
1420
|
console.log(divider);
|
|
@@ -792,8 +1424,8 @@ function runBenchmark(projectPath) {
|
|
|
792
1424
|
const l0 = String(r.irL0Tokens).padStart(7);
|
|
793
1425
|
const l1 = String(r.irL1Tokens).padStart(7);
|
|
794
1426
|
const saved = (r.savedPercent.toFixed(1) + "%").padStart(7);
|
|
795
|
-
const
|
|
796
|
-
console.log(` ${file} ${raw} ${l0} ${l1} ${saved} ${
|
|
1427
|
+
const eng = r.engine.padStart(5);
|
|
1428
|
+
console.log(` ${file} ${raw} ${l0} ${l1} ${saved} ${eng}`);
|
|
797
1429
|
}
|
|
798
1430
|
const summary = summarize(results);
|
|
799
1431
|
console.log(divider);
|
|
@@ -802,28 +1434,107 @@ function runBenchmark(projectPath) {
|
|
|
802
1434
|
const totalL0 = String(summary.totalIRL0).padStart(7);
|
|
803
1435
|
const totalL1 = String(summary.totalIRL1).padStart(7);
|
|
804
1436
|
const totalSaved = (summary.totalSavedPercent.toFixed(1) + "%").padStart(7);
|
|
805
|
-
|
|
806
|
-
console.log(` ${totalLabel} ${totalRaw} ${totalL0} ${totalL1} ${totalSaved} ${totalConf}`);
|
|
1437
|
+
console.log(` ${totalLabel} ${totalRaw} ${totalL0} ${totalL1} ${totalSaved}`);
|
|
807
1438
|
const l0Percent = summary.totalRaw > 0 ? (summary.totalRaw - summary.totalIRL0) / summary.totalRaw * 100 : 0;
|
|
808
1439
|
console.log(`
|
|
809
1440
|
L0 (structure map): ${summary.totalRaw} \u2192 ${summary.totalIRL0} tokens (${l0Percent.toFixed(1)}% reduction)`);
|
|
810
1441
|
console.log(` L1 (full IR): ${summary.totalRaw} \u2192 ${summary.totalIRL1} tokens (${summary.totalSavedPercent.toFixed(1)}% reduction)`);
|
|
811
1442
|
console.log(` Files analyzed: ${summary.fileCount}`);
|
|
812
|
-
console.log(`
|
|
1443
|
+
console.log(` Engine: ${summary.astCount} AST, ${summary.fpCount} FP`);
|
|
1444
|
+
}
|
|
1445
|
+
async function runBenchmarkQuality(projectPath, filePath) {
|
|
1446
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1447
|
+
if (!apiKey) {
|
|
1448
|
+
console.error(" Error: ANTHROPIC_API_KEY environment variable is required.");
|
|
1449
|
+
process.exit(1);
|
|
1450
|
+
}
|
|
1451
|
+
const code = readFileSync2(filePath, "utf-8");
|
|
1452
|
+
const relPath = relative(projectPath, filePath);
|
|
1453
|
+
console.log("composto v0.1.0 \u2014 quality benchmark\n");
|
|
1454
|
+
console.log(` File: ${relPath}
|
|
1455
|
+
`);
|
|
1456
|
+
console.log(" Sending to Claude Haiku...\n");
|
|
1457
|
+
const result = await runQualityBenchmark(code, relPath, apiKey);
|
|
1458
|
+
const col1 = 20;
|
|
1459
|
+
const col2 = 12;
|
|
1460
|
+
const col3 = 12;
|
|
1461
|
+
const line = " " + "\u2500".repeat(col1 + col2 + col3 + 4);
|
|
1462
|
+
console.log(line);
|
|
1463
|
+
console.log(` ${"".padEnd(col1)} ${"Raw Code".padStart(col2)} ${"IR (L1)".padStart(col3)}`);
|
|
1464
|
+
console.log(line);
|
|
1465
|
+
console.log(` ${"Input tokens".padEnd(col1)} ${String(result.raw.inputTokens).padStart(col2)} ${String(result.ir.inputTokens).padStart(col3)}`);
|
|
1466
|
+
console.log(` ${"Output tokens".padEnd(col1)} ${String(result.raw.outputTokens).padStart(col2)} ${String(result.ir.outputTokens).padStart(col3)}`);
|
|
1467
|
+
console.log(` ${"Total tokens".padEnd(col1)} ${String(result.raw.totalTokens).padStart(col2)} ${String(result.ir.totalTokens).padStart(col3)}`);
|
|
1468
|
+
console.log(` ${"Response time".padEnd(col1)} ${(result.raw.responseTimeMs / 1e3).toFixed(1).padStart(col2 - 1)}s ${(result.ir.responseTimeMs / 1e3).toFixed(1).padStart(col3 - 1)}s`);
|
|
1469
|
+
console.log(` ${"Saved".padEnd(col1)} ${"\u2014".padStart(col2)} ${(result.savedPercent.toFixed(1) + "%").padStart(col3)}`);
|
|
1470
|
+
console.log(line);
|
|
1471
|
+
console.log(`
|
|
1472
|
+
--- Raw Code Response ---
|
|
1473
|
+
${result.raw.response}
|
|
1474
|
+
`);
|
|
1475
|
+
console.log(` --- IR Response ---
|
|
1476
|
+
${result.ir.response}
|
|
1477
|
+
`);
|
|
1478
|
+
if (result.savedPercent > 0) {
|
|
1479
|
+
console.log(` Verdict: ${result.savedPercent.toFixed(1)}% fewer tokens with IR.`);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function runContext(projectPath, budget) {
|
|
1483
|
+
console.log(`composto v0.1.0 \u2014 context (budget: ${budget} tokens)
|
|
1484
|
+
`);
|
|
1485
|
+
const files = collectFiles(projectPath, ALL_EXTENSIONS);
|
|
1486
|
+
console.log(` ${files.length} files
|
|
1487
|
+
`);
|
|
1488
|
+
const config = loadConfig(projectPath);
|
|
1489
|
+
const entries = getGitLog(projectPath, 100);
|
|
1490
|
+
const hotspots = detectHotspots(entries, {
|
|
1491
|
+
threshold: config.trends.hotspotThreshold,
|
|
1492
|
+
fixRatioThreshold: config.trends.bugFixRatioThreshold
|
|
1493
|
+
});
|
|
1494
|
+
const fileInputs = files.map((file) => {
|
|
1495
|
+
const code = readFileSync2(file, "utf-8");
|
|
1496
|
+
const relPath = relative(projectPath, file);
|
|
1497
|
+
return { path: relPath, code, rawTokens: estimateTokens(code) };
|
|
1498
|
+
});
|
|
1499
|
+
const result = await packContext(fileInputs, { budget, hotspots });
|
|
1500
|
+
const l1Entries = result.entries.filter((e) => e.layer === "L1");
|
|
1501
|
+
const l0Entries = result.entries.filter((e) => e.layer === "L0");
|
|
1502
|
+
if (l1Entries.length > 0) {
|
|
1503
|
+
console.log(" == L1 (detailed) ==\n");
|
|
1504
|
+
for (const entry of l1Entries) {
|
|
1505
|
+
const label = hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
|
|
1506
|
+
console.log(` [${label}] ${entry.path}`);
|
|
1507
|
+
for (const line of entry.ir.split("\n")) {
|
|
1508
|
+
console.log(` ${line}`);
|
|
1509
|
+
}
|
|
1510
|
+
console.log();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (l0Entries.length > 0) {
|
|
1514
|
+
console.log(" == L0 (structure) ==\n");
|
|
1515
|
+
for (const entry of l0Entries) {
|
|
1516
|
+
for (const line of entry.ir.split("\n")) {
|
|
1517
|
+
console.log(` ${line}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
console.log();
|
|
1521
|
+
}
|
|
1522
|
+
console.log(` Budget: ${result.totalTokens}/${result.budget} tokens`);
|
|
1523
|
+
console.log(` Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
|
|
813
1524
|
}
|
|
814
1525
|
|
|
815
1526
|
// src/index.ts
|
|
816
|
-
import { resolve } from "path";
|
|
1527
|
+
import { resolve as resolve2 } from "path";
|
|
817
1528
|
var args = process.argv.slice(2);
|
|
818
1529
|
var command = args[0];
|
|
819
1530
|
switch (command) {
|
|
820
1531
|
case "scan": {
|
|
821
|
-
const projectPath =
|
|
1532
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
822
1533
|
runScan(projectPath);
|
|
823
1534
|
break;
|
|
824
1535
|
}
|
|
825
1536
|
case "trends": {
|
|
826
|
-
const projectPath =
|
|
1537
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
827
1538
|
runTrends(projectPath);
|
|
828
1539
|
break;
|
|
829
1540
|
}
|
|
@@ -834,12 +1545,28 @@ switch (command) {
|
|
|
834
1545
|
console.error("Usage: composto ir <file> [L0|L1|L2|L3]");
|
|
835
1546
|
process.exit(1);
|
|
836
1547
|
}
|
|
837
|
-
runIR(
|
|
1548
|
+
await runIR(resolve2("."), resolve2(filePath), layer);
|
|
838
1549
|
break;
|
|
839
1550
|
}
|
|
840
1551
|
case "benchmark": {
|
|
841
|
-
const projectPath =
|
|
842
|
-
runBenchmark(projectPath);
|
|
1552
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
1553
|
+
await runBenchmark(projectPath);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
case "benchmark-quality": {
|
|
1557
|
+
const filePath = args[1];
|
|
1558
|
+
if (!filePath) {
|
|
1559
|
+
console.error("Usage: composto benchmark-quality <file>");
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
}
|
|
1562
|
+
await runBenchmarkQuality(resolve2("."), resolve2(filePath));
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1565
|
+
case "context": {
|
|
1566
|
+
const projectPath = resolve2(args[1] ?? ".");
|
|
1567
|
+
const budgetFlag = args.indexOf("--budget");
|
|
1568
|
+
const budget = budgetFlag !== -1 && args[budgetFlag + 1] ? parseInt(args[budgetFlag + 1], 10) : 4e3;
|
|
1569
|
+
await runContext(projectPath, budget);
|
|
843
1570
|
break;
|
|
844
1571
|
}
|
|
845
1572
|
case "version":
|
|
@@ -852,6 +1579,8 @@ switch (command) {
|
|
|
852
1579
|
console.log(" trends [path] Analyze codebase health trends");
|
|
853
1580
|
console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
|
|
854
1581
|
console.log(" benchmark [path] Benchmark IR token savings");
|
|
1582
|
+
console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
|
|
1583
|
+
console.log(" context [path] --budget N Smart context within token budget");
|
|
855
1584
|
console.log(" version Show version");
|
|
856
1585
|
break;
|
|
857
1586
|
}
|