composto-ai 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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.65
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.7
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.3 };
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.6);
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 lines = code.split("\n");
664
- let totalConf = 0;
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
- const avgConfidence = count > 0 ? totalConf / count : 0;
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 avgConfidence = results.length > 0 ? results.reduce((s, r) => s + r.avgConfidence, 0) / results.length : 0;
683
- return { fileCount: results.length, totalRaw, totalIRL0, totalIRL1, totalSavedPercent, avgConfidence };
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
- function runBenchmark(projectPath) {
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, [".ts", ".tsx", ".js", ".jsx"]);
1407
+ const files = collectFiles(projectPath, ALL_EXTENSIONS);
777
1408
  console.log(` ${files.length} files
778
1409
  `);
779
- const results = files.map((file) => {
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
- return benchmarkFile(code, relPath);
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 Conf";
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 conf = r.avgConfidence.toFixed(2).padStart(6);
796
- console.log(` ${file} ${raw} ${l0} ${l1} ${saved} ${conf}`);
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
- const totalConf = summary.avgConfidence.toFixed(2).padStart(6);
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(` Avg confidence: ${summary.avgConfidence.toFixed(2)}`);
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 = resolve(args[1] ?? ".");
1532
+ const projectPath = resolve2(args[1] ?? ".");
822
1533
  runScan(projectPath);
823
1534
  break;
824
1535
  }
825
1536
  case "trends": {
826
- const projectPath = resolve(args[1] ?? ".");
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(resolve("."), resolve(filePath), layer);
1548
+ await runIR(resolve2("."), resolve2(filePath), layer);
838
1549
  break;
839
1550
  }
840
1551
  case "benchmark": {
841
- const projectPath = resolve(args[1] ?? ".");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,12 @@
20
20
  "test:watch": "vitest",
21
21
  "dev": "tsx src/index.ts"
22
22
  },
23
- "keywords": ["ai", "coding", "companion", "proactive"],
23
+ "keywords": [
24
+ "ai",
25
+ "coding",
26
+ "companion",
27
+ "proactive"
28
+ ],
24
29
  "author": "Mert Can Altin",
25
30
  "license": "MIT",
26
31
  "packageManager": "pnpm@10.30.1",
@@ -35,6 +40,7 @@
35
40
  "dependencies": {
36
41
  "@anthropic-ai/sdk": "^0.87.0",
37
42
  "picomatch": "^4.0.4",
43
+ "web-tree-sitter": "^0.26.8",
38
44
  "yaml": "^2.8.3"
39
45
  }
40
46
  }