composto-ai 0.2.0 → 0.3.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.
@@ -499,36 +499,94 @@ function detectLanguage(filePath) {
499
499
 
500
500
  // src/ir/ast-walker.ts
501
501
  var TIER_MAP = {
502
- // Tier 1 — structural declarations
502
+ // Tier 1 — structural declarations (JS/TS)
503
503
  import_statement: "T1_KEEP",
504
504
  function_declaration: "T1_KEEP",
505
505
  class_declaration: "T1_KEEP",
506
506
  interface_declaration: "T1_KEEP",
507
507
  type_alias_declaration: "T1_KEEP",
508
508
  enum_declaration: "T1_KEEP",
509
- // Tier 2control flow
509
+ // Tier 1Python
510
+ function_definition: "T1_KEEP",
511
+ class_definition: "T1_KEEP",
512
+ import_from_statement: "T1_KEEP",
513
+ decorated_definition: "T1_KEEP",
514
+ // Tier 1 — class members (qualified methods)
515
+ method_definition: "T1_KEEP",
516
+ // JS/TS class method
517
+ public_field_definition: "T1_KEEP",
518
+ // TS class property
519
+ // Tier 1 — Go
520
+ function_item: "T1_KEEP",
521
+ // Rust
522
+ method_declaration: "T1_KEEP",
523
+ // Go
524
+ type_declaration: "T1_KEEP",
525
+ // Go
526
+ import_declaration: "T1_KEEP",
527
+ // Go
528
+ use_declaration: "T1_KEEP",
529
+ // Rust
530
+ struct_item: "T1_KEEP",
531
+ // Rust
532
+ enum_item: "T1_KEEP",
533
+ // Rust
534
+ trait_item: "T1_KEEP",
535
+ // Rust
536
+ impl_item: "T1_KEEP",
537
+ // Rust
538
+ // Tier 2 — control flow (universal)
510
539
  if_statement: "T2_CONTROL",
540
+ if_expression: "T2_CONTROL",
541
+ // Rust
511
542
  else_clause: "WALK_ONLY",
543
+ elif_clause: "T2_CONTROL",
544
+ // Python
512
545
  for_statement: "T2_CONTROL",
513
546
  for_in_statement: "T2_CONTROL",
547
+ for_expression: "T2_CONTROL",
548
+ // Rust
514
549
  while_statement: "T2_CONTROL",
515
550
  do_statement: "T2_CONTROL",
516
551
  switch_statement: "T2_CONTROL",
517
552
  switch_case: "T2_CONTROL",
518
553
  switch_default: "T2_CONTROL",
554
+ match_expression: "T2_CONTROL",
555
+ // Rust
519
556
  return_statement: "T2_CONTROL",
557
+ return_expression: "T2_CONTROL",
558
+ // Rust
520
559
  throw_statement: "T2_CONTROL",
560
+ raise_statement: "T2_CONTROL",
561
+ // Python
521
562
  try_statement: "T2_CONTROL",
522
563
  catch_clause: "T2_CONTROL",
564
+ except_clause: "T2_CONTROL",
565
+ // Python
566
+ with_statement: "T2_CONTROL",
567
+ // Python
568
+ defer_statement: "T2_CONTROL",
569
+ // Go
523
570
  // Tier 3 — compressible expressions
524
571
  lexical_declaration: "T3_COMPRESS",
525
572
  expression_statement: "T3_COMPRESS",
526
- // Walk-only — containers that need traversal but no emission
573
+ assignment: "T3_COMPRESS",
574
+ // Python
575
+ short_var_declaration: "T3_COMPRESS",
576
+ // Go
577
+ // Walk-only — containers
527
578
  program: "WALK_ONLY",
579
+ module: "WALK_ONLY",
580
+ // Python
528
581
  statement_block: "WALK_ONLY",
582
+ block: "WALK_ONLY",
583
+ // Python/Go/Rust
529
584
  class_body: "WALK_ONLY",
585
+ // JS/TS
530
586
  switch_body: "WALK_ONLY",
531
- export_statement: "WALK_ONLY"
587
+ export_statement: "WALK_ONLY",
588
+ source_file: "WALK_ONLY"
589
+ // Go/Rust
532
590
  };
533
591
  function tierOf(nodeType) {
534
592
  return TIER_MAP[nodeType] ?? "T4_DROP";
@@ -553,6 +611,49 @@ function isExported(node) {
553
611
  function isAsync(node) {
554
612
  return node.text.trimStart().startsWith("async");
555
613
  }
614
+ function extractDocComment(node) {
615
+ let prev = node.previousNamedSibling;
616
+ if (!prev && node.parent?.type === "export_statement") {
617
+ prev = node.parent.previousNamedSibling;
618
+ }
619
+ if (!prev || prev.type !== "comment") return null;
620
+ const text = prev.text;
621
+ if (!text.startsWith("/**")) return null;
622
+ const body = text.replace(/^\/\*\*|\*\/$/g, "").replace(/^\s*\*\s?/gm, "").trim();
623
+ const tags = [];
624
+ const tagMatches = body.matchAll(/@(\w+)(?:\s+([^\n@]+))?/g);
625
+ for (const m of tagMatches) {
626
+ const tag = m[1];
627
+ const val = (m[2] ?? "").trim();
628
+ if (tag === "deprecated") tags.push("@deprecated");
629
+ else if (tag === "internal") tags.push("@internal");
630
+ else if (tag === "throws" && val) tags.push(`@throws:${val.length > 30 ? val.slice(0, 27) + "..." : val}`);
631
+ }
632
+ const beforeTags = body.split(/@\w+/)[0].trim();
633
+ const desc = beforeTags.split(/[.\n]/)[0].trim();
634
+ const parts = [];
635
+ if (tags.length > 0) parts.push(tags.join(" "));
636
+ if (desc && desc.length > 3) {
637
+ parts.push(`"${desc.length > 50 ? desc.slice(0, 47) + "..." : desc}"`);
638
+ }
639
+ return parts.length > 0 ? parts.join(" ") : null;
640
+ }
641
+ function extractPythonDocstring(bodyNode) {
642
+ if (!bodyNode) return null;
643
+ for (let i = 0; i < bodyNode.childCount; i++) {
644
+ const child = bodyNode.child(i);
645
+ if (child.type === "expression_statement" && child.childCount > 0) {
646
+ const expr = child.child(0);
647
+ if (expr.type === "string") {
648
+ const text = expr.text.replace(/^(['"]{3}|['"])|(['"]{3}|['"])$/g, "").trim();
649
+ const firstLine = text.split("\n")[0].trim();
650
+ return firstLine.length > 3 ? `"${firstLine.length > 50 ? firstLine.slice(0, 47) + "..." : firstLine}"` : null;
651
+ }
652
+ break;
653
+ }
654
+ }
655
+ return null;
656
+ }
556
657
  function extractCondition(node) {
557
658
  const condNode = node.childForFieldName("condition") ?? (() => {
558
659
  for (let i = 0; i < node.childCount; i++) {
@@ -644,6 +745,35 @@ function emitTier2(node) {
644
745
  const paramText = param ? param.text : "...";
645
746
  return `CATCH:${paramText}`;
646
747
  }
748
+ // Python
749
+ case "raise_statement": {
750
+ const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
751
+ return `RAISE:${val.length > 50 ? val.slice(0, 47) + "..." : val}`;
752
+ }
753
+ case "except_clause":
754
+ return "EXCEPT";
755
+ case "elif_clause": {
756
+ const cond = extractCondition(node);
757
+ return `ELIF:${cond}`;
758
+ }
759
+ case "with_statement":
760
+ return "WITH";
761
+ // Rust
762
+ case "if_expression": {
763
+ const cond = extractCondition(node);
764
+ return `IF:${cond}`;
765
+ }
766
+ case "for_expression":
767
+ return "LOOP";
768
+ case "match_expression":
769
+ return "MATCH";
770
+ case "return_expression": {
771
+ const val = node.childCount > 1 ? node.child(1)?.text ?? "" : "";
772
+ return `RET ${val.length > 60 ? val.slice(0, 57) + "..." : val}`.trimEnd();
773
+ }
774
+ // Go
775
+ case "defer_statement":
776
+ return "DEFER";
647
777
  default:
648
778
  return null;
649
779
  }
@@ -661,25 +791,103 @@ function emitTier1(node) {
661
791
  const rawParams = node.childForFieldName("parameters")?.text ?? "()";
662
792
  const params = collapseText(rawParams, 60);
663
793
  const asyncPrefix = isAsync(node) ? "ASYNC " : "";
664
- return `${outPrefix}${asyncPrefix}FN:${name}${params}`;
794
+ const doc = extractDocComment(node);
795
+ const docPrefix = doc ? `${doc} ` : "";
796
+ return `${docPrefix}${outPrefix}${asyncPrefix}FN:${name}${params}`;
665
797
  }
666
798
  case "class_declaration": {
667
799
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
668
800
  const typeParams = getTypeParams(node);
669
- return `${outPrefix}CLASS:${name}${typeParams}`;
801
+ const doc = extractDocComment(node);
802
+ const docPrefix = doc ? `${doc} ` : "";
803
+ return `${docPrefix}${outPrefix}CLASS:${name}${typeParams}`;
670
804
  }
671
805
  case "interface_declaration": {
672
806
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
673
807
  const typeParams = getTypeParams(node);
674
- return `${outPrefix}INTERFACE:${name}${typeParams}`;
808
+ const doc = extractDocComment(node);
809
+ const docPrefix = doc ? `${doc} ` : "";
810
+ return `${docPrefix}${outPrefix}INTERFACE:${name}${typeParams}`;
675
811
  }
676
812
  case "type_alias_declaration": {
677
813
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
678
- return `${outPrefix}TYPE:${name}`;
814
+ const doc = extractDocComment(node);
815
+ const docPrefix = doc ? `${doc} ` : "";
816
+ return `${docPrefix}${outPrefix}TYPE:${name}`;
679
817
  }
680
818
  case "enum_declaration": {
681
819
  const name = node.childForFieldName("name")?.text ?? "Anonymous";
682
- return `${outPrefix}ENUM:${name}`;
820
+ const doc = extractDocComment(node);
821
+ const docPrefix = doc ? `${doc} ` : "";
822
+ return `${docPrefix}${outPrefix}ENUM:${name}`;
823
+ }
824
+ case "method_definition": {
825
+ let enclosingClass = null;
826
+ let parent = node.parent;
827
+ while (parent) {
828
+ if (parent.type === "class_declaration" || parent.type === "class_definition") {
829
+ enclosingClass = parent.childForFieldName("name")?.text ?? null;
830
+ break;
831
+ }
832
+ parent = parent.parent;
833
+ }
834
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
835
+ const params = node.childForFieldName("parameters")?.text ?? "()";
836
+ const asyncPrefix = isAsync(node) ? "ASYNC " : "";
837
+ const doc = extractDocComment(node);
838
+ const docPrefix = doc ? `${doc} ` : "";
839
+ const qualifiedName = enclosingClass ? `${enclosingClass}.${name}` : name;
840
+ return `${docPrefix}${asyncPrefix}METHOD:${qualifiedName}${collapseText(params, 60)}`;
841
+ }
842
+ case "public_field_definition": {
843
+ const name = node.childForFieldName("name")?.text ?? "field";
844
+ return `FIELD:${name}`;
845
+ }
846
+ // Python
847
+ case "function_definition": {
848
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
849
+ const params = node.childForFieldName("parameters")?.text ?? "()";
850
+ const returnType = node.childForFieldName("return_type")?.text ?? "";
851
+ const rt = returnType ? ` -> ${returnType}` : "";
852
+ const body = node.childForFieldName("body");
853
+ const doc = extractPythonDocstring(body);
854
+ const docPrefix = doc ? `${doc} ` : "";
855
+ return `${docPrefix}FN:${name}${collapseText(params, 60)}${rt}`;
856
+ }
857
+ case "class_definition": {
858
+ const name = node.childForFieldName("name")?.text ?? "Anonymous";
859
+ const superclass = node.childForFieldName("superclasses")?.text ?? "";
860
+ const sc = superclass ? `(${collapseText(superclass, 40)})` : "";
861
+ const body = node.childForFieldName("body");
862
+ const doc = extractPythonDocstring(body);
863
+ const docPrefix = doc ? `${doc} ` : "";
864
+ return `${docPrefix}CLASS:${name}${sc}`;
865
+ }
866
+ case "import_from_statement": {
867
+ return `USE:${collapseText(node.text, 80)}`;
868
+ }
869
+ case "decorated_definition": {
870
+ return null;
871
+ }
872
+ // Go
873
+ case "method_declaration":
874
+ case "type_declaration":
875
+ case "import_declaration": {
876
+ return `${collapseText(node.text, 80)}`;
877
+ }
878
+ // Rust
879
+ case "function_item": {
880
+ const name = node.childForFieldName("name")?.text ?? "anonymous";
881
+ const params = node.childForFieldName("parameters")?.text ?? "()";
882
+ return `FN:${name}${collapseText(params, 60)}`;
883
+ }
884
+ case "struct_item":
885
+ case "enum_item":
886
+ case "trait_item":
887
+ case "impl_item":
888
+ case "use_declaration": {
889
+ const firstLine = node.text.split("\n")[0];
890
+ return collapseText(firstLine, 80);
683
891
  }
684
892
  default:
685
893
  return null;
@@ -726,6 +934,17 @@ function emitTier3(node) {
726
934
  return null;
727
935
  }
728
936
  if (expr.type === "call_expression") {
937
+ const callee = expr.child(0)?.text ?? "";
938
+ if (callee === "ObjectSetPrototypeOf" || callee === "Object.setPrototypeOf") {
939
+ const args = expr.child(1);
940
+ if (args && args.childCount >= 4) {
941
+ const child = args.child(1)?.text ?? "?";
942
+ const parent = args.child(3)?.text ?? "?";
943
+ const shortChild = child.length > 30 ? child.slice(0, 27) + "..." : child;
944
+ const shortParent = parent.length > 30 ? parent.slice(0, 27) + "..." : parent;
945
+ return `EXTENDS:${shortChild} < ${shortParent}`;
946
+ }
947
+ }
729
948
  return null;
730
949
  }
731
950
  return null;
@@ -743,7 +962,9 @@ function walkNode(node, depth, lines) {
743
962
  for (let i = 0; i < node.childCount; i++) {
744
963
  const child = node.child(i);
745
964
  const childType = child.type;
746
- if (childType === "statement_block" || childType === "class_body") {
965
+ if (childType === "statement_block" || childType === "class_body" || childType === "block" || // Python/Go/Rust
966
+ childType === "body" || // Python class/function body
967
+ childType === "declaration_list") {
747
968
  walkNode(child, depth + 1, lines);
748
969
  }
749
970
  }
@@ -834,7 +1055,7 @@ async function astWalkIR(code, filePath) {
834
1055
  const lines = [];
835
1056
  walkNode(root, 0, lines);
836
1057
  if (lines.length === 0) return null;
837
- const merged = [];
1058
+ const pass1 = [];
838
1059
  let useBlock = [];
839
1060
  for (const line of lines) {
840
1061
  if (line.startsWith("USE:")) {
@@ -843,20 +1064,45 @@ async function astWalkIR(code, filePath) {
843
1064
  } else {
844
1065
  if (useBlock.length > 0) {
845
1066
  if (useBlock.length <= 3) {
846
- for (const mod of useBlock) merged.push(`USE:${mod}`);
1067
+ for (const mod of useBlock) pass1.push(`USE:${mod}`);
847
1068
  } else {
848
- merged.push(`USE:[${useBlock.join(", ")}]`);
1069
+ pass1.push(`USE:[${useBlock.join(", ")}]`);
849
1070
  }
850
1071
  useBlock = [];
851
1072
  }
852
- merged.push(line);
1073
+ pass1.push(line);
853
1074
  }
854
1075
  }
855
1076
  if (useBlock.length > 0) {
856
1077
  if (useBlock.length <= 3) {
857
- for (const mod of useBlock) merged.push(`USE:${mod}`);
1078
+ for (const mod of useBlock) pass1.push(`USE:${mod}`);
858
1079
  } else {
859
- merged.push(`USE:[${useBlock.join(", ")}]`);
1080
+ pass1.push(`USE:[${useBlock.join(", ")}]`);
1081
+ }
1082
+ }
1083
+ const merged = [];
1084
+ let guardBlock = [];
1085
+ for (const line of pass1) {
1086
+ const guardMatch = line.match(/^(\s*)IF:(.+?) \u2192 RET/);
1087
+ if (guardMatch) {
1088
+ guardBlock.push(guardMatch[2].trim());
1089
+ continue;
1090
+ }
1091
+ if (guardBlock.length > 0) {
1092
+ if (guardBlock.length < 3) {
1093
+ for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1094
+ } else {
1095
+ merged.push(` GUARD:[${guardBlock.join(", ")}]`);
1096
+ }
1097
+ guardBlock = [];
1098
+ }
1099
+ merged.push(line);
1100
+ }
1101
+ if (guardBlock.length > 0) {
1102
+ if (guardBlock.length < 3) {
1103
+ for (const g of guardBlock) merged.push(` IF:${g} \u2192 RET`);
1104
+ } else {
1105
+ merged.push(` GUARD:[${guardBlock.join(", ")}]`);
860
1106
  }
861
1107
  }
862
1108
  return merged.join("\n");
@@ -879,10 +1125,11 @@ ${declarations.join("\n")}`;
879
1125
  }
880
1126
  async function generateL1(code, filePath, health) {
881
1127
  const ir = await astWalkIR(code, filePath) ?? fingerprintFile(code, 0.75);
1128
+ const result = ir.length < code.length ? ir : code;
882
1129
  if (health) {
883
- return annotateIR(ir, health);
1130
+ return annotateIR(result, health);
884
1131
  }
885
- return ir;
1132
+ return result;
886
1133
  }
887
1134
  function generateL2(delta, health) {
888
1135
  const parts = [`FILE: ${delta.file}`];
@@ -1098,43 +1345,161 @@ function summarize(results) {
1098
1345
  }
1099
1346
 
1100
1347
  // src/context/packer.ts
1348
+ function escapeRegex(s) {
1349
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1350
+ }
1351
+ function findTargetFile(files, target) {
1352
+ const t = escapeRegex(target);
1353
+ const declarationPatterns = [
1354
+ // JS/TS declarations
1355
+ new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${t}\\b`),
1356
+ new RegExp(`(?:export\\s+)?class\\s+${t}\\b`),
1357
+ new RegExp(`(?:export\\s+)?interface\\s+${t}\\b`),
1358
+ new RegExp(`(?:export\\s+)?type\\s+${t}\\b`),
1359
+ new RegExp(`(?:export\\s+)?enum\\s+${t}\\b`),
1360
+ new RegExp(`(?:export\\s+)?(?:const|let|var)\\s+${t}\\b`),
1361
+ // Python
1362
+ new RegExp(`\\bdef\\s+${t}\\b`),
1363
+ // Rust
1364
+ new RegExp(`\\bfn\\s+${t}\\b`),
1365
+ new RegExp(`\\bstruct\\s+${t}\\b`),
1366
+ new RegExp(`\\btrait\\s+${t}\\b`),
1367
+ // Go
1368
+ new RegExp(`\\bfunc\\s+${t}\\b`),
1369
+ new RegExp(`\\btype\\s+${t}\\b`),
1370
+ // Object method shorthand
1371
+ new RegExp(`\\b${t}\\s*:\\s*(?:async\\s+)?function\\b`),
1372
+ new RegExp(`\\b${t}\\s*\\([^)]*\\)\\s*\\{`)
1373
+ ];
1374
+ for (const pattern of declarationPatterns) {
1375
+ const match2 = files.find((f) => pattern.test(f.code));
1376
+ if (match2) return match2.path;
1377
+ }
1378
+ const fallback = new RegExp(`\\b${t}\\s*\\(`);
1379
+ const match = files.find((f) => fallback.test(f.code));
1380
+ return match ? match.path : null;
1381
+ }
1382
+ function findRelatedFiles(files, targetPath) {
1383
+ const related = /* @__PURE__ */ new Set();
1384
+ const targetFile = files.find((f) => f.path === targetPath);
1385
+ if (!targetFile) return related;
1386
+ const importPattern = /(?:import|require)\s*(?:\([^)]*|\{[^}]*\}|\w+)?\s*(?:from)?\s*["']([^"']+)["']/g;
1387
+ const imports = [...targetFile.code.matchAll(importPattern)].map((m) => m[1]);
1388
+ for (const imp of imports) {
1389
+ const match = files.find((f) => {
1390
+ const basename = f.path.replace(/\.[^.]+$/, "");
1391
+ return imp.includes(basename) || basename.endsWith(imp.replace(/^\.\.?\//, "").replace(/\.[^.]+$/, ""));
1392
+ });
1393
+ if (match) related.add(match.path);
1394
+ }
1395
+ const targetBasename = targetPath.replace(/\.[^.]+$/, "").split("/").pop() ?? "";
1396
+ for (const file of files) {
1397
+ if (file.path === targetPath) continue;
1398
+ if (file.code.includes(targetBasename)) {
1399
+ related.add(file.path);
1400
+ }
1401
+ }
1402
+ return related;
1403
+ }
1101
1404
  async function packContext(files, options) {
1102
- const { budget, hotspots } = options;
1405
+ const { budget, hotspots, target } = options;
1103
1406
  const hotspotSet = new Set(hotspots.map((h) => h.file));
1407
+ let targetPath = null;
1408
+ let relatedFiles = /* @__PURE__ */ new Set();
1409
+ if (target) {
1410
+ targetPath = findTargetFile(files, target);
1411
+ if (targetPath) {
1412
+ relatedFiles = findRelatedFiles(files, targetPath);
1413
+ }
1414
+ }
1104
1415
  const entries = [];
1105
1416
  let totalTokens = 0;
1417
+ let filesAtL3 = 0;
1418
+ let targetDowngraded = false;
1419
+ if (targetPath) {
1420
+ const targetFile = files.find((f) => f.path === targetPath);
1421
+ const rawTokens = estimateTokens(targetFile.code);
1422
+ if (rawTokens <= budget * 0.6) {
1423
+ entries.push({
1424
+ path: targetPath,
1425
+ layer: "L3",
1426
+ ir: targetFile.code,
1427
+ tokens: rawTokens,
1428
+ isTarget: true
1429
+ });
1430
+ totalTokens += rawTokens;
1431
+ filesAtL3 = 1;
1432
+ } else {
1433
+ targetDowngraded = true;
1434
+ const l1 = await generateLayer("L1", { code: targetFile.code, filePath: targetFile.path, health: null });
1435
+ const l1Tokens = estimateTokens(l1);
1436
+ entries.push({
1437
+ path: targetPath,
1438
+ layer: "L1",
1439
+ ir: l1,
1440
+ tokens: l1Tokens,
1441
+ isTarget: true
1442
+ });
1443
+ totalTokens += l1Tokens;
1444
+ }
1445
+ }
1106
1446
  for (const file of files) {
1447
+ if (file.path === targetPath) continue;
1107
1448
  const l0 = await generateLayer("L0", { code: file.code, filePath: file.path, health: null });
1108
1449
  const l0Tokens = estimateTokens(l0);
1109
1450
  entries.push({ path: file.path, layer: "L0", ir: l0, tokens: l0Tokens });
1110
1451
  totalTokens += l0Tokens;
1111
1452
  }
1112
1453
  if (totalTokens > budget) {
1113
- const truncated = [];
1114
- let used = 0;
1454
+ const truncated = entries.filter((e) => e.isTarget);
1455
+ let used = truncated.reduce((s, e) => s + e.tokens, 0);
1115
1456
  for (const entry of entries) {
1457
+ if (entry.isTarget) continue;
1116
1458
  if (used + entry.tokens <= budget) {
1117
1459
  truncated.push(entry);
1118
1460
  used += entry.tokens;
1119
1461
  }
1120
1462
  }
1121
- return { entries: truncated, totalTokens: used, budget, filesAtL0: truncated.length, filesAtL1: 0 };
1463
+ return {
1464
+ entries: truncated,
1465
+ totalTokens: used,
1466
+ budget,
1467
+ filesAtL0: truncated.filter((e) => e.layer === "L0").length,
1468
+ filesAtL1: truncated.filter((e) => e.layer === "L1").length,
1469
+ filesAtL3,
1470
+ targetFile: targetPath ?? void 0,
1471
+ targetDowngraded
1472
+ };
1122
1473
  }
1123
- const upgradeOrder = entries.map((e, i) => ({ index: i, path: e.path, rawTokens: files[i].rawTokens, isHotspot: hotspotSet.has(e.path) })).sort((a, b) => {
1474
+ const upgradeOrder = entries.map((e, i) => ({
1475
+ index: i,
1476
+ path: e.path,
1477
+ rawTokens: files.find((f) => f.path === e.path)?.rawTokens ?? 0,
1478
+ isHotspot: hotspotSet.has(e.path),
1479
+ isRelated: relatedFiles.has(e.path),
1480
+ isTarget: e.isTarget ?? false
1481
+ })).filter((x) => x.isTarget === false && entries[x.index].layer === "L0").sort((a, b) => {
1482
+ if (a.isRelated && !b.isRelated) return -1;
1483
+ if (!a.isRelated && b.isRelated) return 1;
1124
1484
  if (a.isHotspot && !b.isHotspot) return -1;
1125
1485
  if (!a.isHotspot && b.isHotspot) return 1;
1126
1486
  return b.rawTokens - a.rawTokens;
1127
1487
  });
1128
1488
  let filesAtL1 = 0;
1129
1489
  for (const item of upgradeOrder) {
1130
- const file = files[item.index];
1490
+ const file = files.find((f) => f.path === item.path);
1131
1491
  const l1 = await generateLayer("L1", { code: file.code, filePath: file.path, health: null });
1132
1492
  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;
1493
+ const currentTokens = entries[item.index].tokens;
1494
+ const additional = l1Tokens - currentTokens;
1495
+ if (totalTokens + additional <= budget) {
1496
+ entries[item.index] = {
1497
+ path: item.path,
1498
+ layer: "L1",
1499
+ ir: l1,
1500
+ tokens: l1Tokens
1501
+ };
1502
+ totalTokens += additional;
1138
1503
  filesAtL1++;
1139
1504
  }
1140
1505
  }
@@ -1142,8 +1507,11 @@ async function packContext(files, options) {
1142
1507
  entries,
1143
1508
  totalTokens,
1144
1509
  budget,
1145
- filesAtL0: entries.length - filesAtL1,
1146
- filesAtL1
1510
+ filesAtL0: entries.filter((e) => e.layer === "L0").length,
1511
+ filesAtL1,
1512
+ filesAtL3,
1513
+ targetFile: targetPath ?? void 0,
1514
+ targetDowngraded
1147
1515
  };
1148
1516
  }
1149
1517
 
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  packContext,
13
13
  runDetector,
14
14
  summarize
15
- } from "./chunk-MFNGSHTZ.js";
15
+ } from "./chunk-AARGW2GV.js";
16
16
 
17
17
  // src/cli/commands.ts
18
18
  import { readFileSync, readdirSync } from "fs";
@@ -201,7 +201,7 @@ function collectFiles(dir, extensions) {
201
201
  function runScan(projectPath) {
202
202
  const adapter = new CLIAdapter();
203
203
  const config = loadConfig(projectPath);
204
- console.log("composto v0.1.0 \u2014 scanning...\n");
204
+ console.log("composto v0.2.3 \u2014 scanning...\n");
205
205
  const files = collectFiles(projectPath, [".ts", ".tsx", ".js", ".jsx"]);
206
206
  console.log(` Found ${files.length} files
207
207
  `);
@@ -228,7 +228,7 @@ function runScan(projectPath) {
228
228
  function runTrends(projectPath) {
229
229
  const adapter = new CLIAdapter();
230
230
  const config = loadConfig(projectPath);
231
- console.log("composto v0.1.0 \u2014 trend analysis...\n");
231
+ console.log("composto v0.2.3 \u2014 trend analysis...\n");
232
232
  const entries = getGitLog(projectPath, 100);
233
233
  if (entries.length === 0) {
234
234
  console.log(" No git history found.\n");
@@ -270,7 +270,7 @@ async function runIR(projectPath, filePath, layer) {
270
270
  }
271
271
  var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
272
272
  async function runBenchmark(projectPath) {
273
- console.log("composto v0.1.0 \u2014 benchmark\n");
273
+ console.log("composto v0.2.3 \u2014 benchmark\n");
274
274
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
275
275
  console.log(` ${files.length} files
276
276
  `);
@@ -317,7 +317,7 @@ async function runBenchmarkQuality(projectPath, filePath) {
317
317
  }
318
318
  const code = readFileSync(filePath, "utf-8");
319
319
  const relPath = relative(projectPath, filePath);
320
- console.log("composto v0.1.0 \u2014 quality benchmark\n");
320
+ console.log("composto v0.2.3 \u2014 quality benchmark\n");
321
321
  console.log(` File: ${relPath}
322
322
  `);
323
323
  console.log(" Sending to Claude Haiku...\n");
@@ -346,9 +346,11 @@ ${result.ir.response}
346
346
  console.log(` Verdict: ${result.savedPercent.toFixed(1)}% fewer tokens with IR.`);
347
347
  }
348
348
  }
349
- async function runContext(projectPath, budget) {
350
- console.log(`composto v0.1.0 \u2014 context (budget: ${budget} tokens)
351
- `);
349
+ async function runContext(projectPath, budget, target) {
350
+ const header = target ? `composto v0.2.3 \u2014 context (target: ${target}, budget: ${budget} tokens)
351
+ ` : `composto v0.2.3 \u2014 context (budget: ${budget} tokens)
352
+ `;
353
+ console.log(header);
352
354
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
353
355
  console.log(` ${files.length} files
354
356
  `);
@@ -363,13 +365,34 @@ async function runContext(projectPath, budget) {
363
365
  const relPath = relative(projectPath, file);
364
366
  return { path: relPath, code, rawTokens: estimateTokens(code) };
365
367
  });
366
- const result = await packContext(fileInputs, { budget, hotspots });
368
+ const result = await packContext(fileInputs, { budget, hotspots, target });
369
+ if (target && !result.targetFile) {
370
+ console.log(` Warning: symbol "${target}" not found in any file. Showing general context.
371
+ `);
372
+ } else if (result.targetFile) {
373
+ console.log(` Target: ${result.targetFile} (contains ${target})`);
374
+ if (result.targetDowngraded) {
375
+ console.log(` Note: target file too large for raw mode \u2014 using L1 IR instead. Increase --budget for L3.`);
376
+ }
377
+ console.log();
378
+ }
379
+ const l3Entries = result.entries.filter((e) => e.layer === "L3");
367
380
  const l1Entries = result.entries.filter((e) => e.layer === "L1");
368
381
  const l0Entries = result.entries.filter((e) => e.layer === "L0");
382
+ if (l3Entries.length > 0) {
383
+ console.log(" == L3 (raw \u2014 target file) ==\n");
384
+ for (const entry of l3Entries) {
385
+ console.log(` [target] ${entry.path}`);
386
+ for (const line of entry.ir.split("\n")) {
387
+ console.log(` ${line}`);
388
+ }
389
+ console.log();
390
+ }
391
+ }
369
392
  if (l1Entries.length > 0) {
370
393
  console.log(" == L1 (detailed) ==\n");
371
394
  for (const entry of l1Entries) {
372
- const label = hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
395
+ const label = entry.isTarget ? "target" : hotspots.some((h) => h.file === entry.path) ? "hotspot" : "detail";
373
396
  console.log(` [${label}] ${entry.path}`);
374
397
  for (const line of entry.ir.split("\n")) {
375
398
  console.log(` ${line}`);
@@ -386,8 +409,12 @@ async function runContext(projectPath, budget) {
386
409
  }
387
410
  console.log();
388
411
  }
412
+ const parts = [];
413
+ if (result.filesAtL3 > 0) parts.push(`${result.filesAtL3} at L3 (raw)`);
414
+ if (result.filesAtL1 > 0) parts.push(`${result.filesAtL1} at L1`);
415
+ if (result.filesAtL0 > 0) parts.push(`${result.filesAtL0} at L0`);
389
416
  console.log(` Budget: ${result.totalTokens}/${result.budget} tokens`);
390
- console.log(` Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
417
+ console.log(` Files: ${parts.join(", ")}`);
391
418
  }
392
419
 
393
420
  // src/index.ts
@@ -430,24 +457,35 @@ switch (command) {
430
457
  break;
431
458
  }
432
459
  case "context": {
433
- const projectPath = resolve(args[1] ?? ".");
434
- const budgetFlag = args.indexOf("--budget");
435
- const budget = budgetFlag !== -1 && args[budgetFlag + 1] ? parseInt(args[budgetFlag + 1], 10) : 4e3;
436
- await runContext(projectPath, budget);
460
+ let parseFlag = function(name) {
461
+ const equalsForm = args.find((a) => a.startsWith(`--${name}=`));
462
+ if (equalsForm) return equalsForm.slice(name.length + 3);
463
+ const idx = args.indexOf(`--${name}`);
464
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
465
+ return void 0;
466
+ };
467
+ parseFlag2 = parseFlag;
468
+ const projectPath = resolve(args[1] && !args[1].startsWith("--") ? args[1] : ".");
469
+ const budgetStr = parseFlag("budget");
470
+ const budget = budgetStr ? parseInt(budgetStr, 10) : 4e3;
471
+ const target = parseFlag("target");
472
+ await runContext(projectPath, budget, target);
437
473
  break;
438
474
  }
439
475
  case "version":
440
- console.log("composto v0.1.0");
476
+ console.log("composto v0.2.3");
441
477
  break;
442
478
  default:
443
- console.log("composto v0.1.0 \u2014 less tokens, more insight\n");
479
+ console.log("composto v0.2.3 \u2014 less tokens, more insight\n");
444
480
  console.log("Commands:");
445
- console.log(" scan [path] Scan codebase for issues");
446
- console.log(" trends [path] Analyze codebase health trends");
447
- console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
448
- console.log(" benchmark [path] Benchmark IR token savings");
449
- console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
450
- console.log(" context [path] --budget N Smart context within token budget");
451
- console.log(" version Show version");
481
+ console.log(" scan [path] Scan codebase for issues");
482
+ console.log(" trends [path] Analyze codebase health trends");
483
+ console.log(" ir <file> [layer] Generate IR for a file (L0|L1|L2|L3)");
484
+ console.log(" benchmark [path] Benchmark IR token savings");
485
+ console.log(" benchmark-quality <file> Compare AI responses: raw vs IR");
486
+ console.log(" context [path] --budget N Smart context within token budget");
487
+ console.log(" context [path] --target <symbol> Target file as raw, surrounding as IR");
488
+ console.log(" version Show version");
452
489
  break;
453
490
  }
491
+ var parseFlag2;
@@ -12,7 +12,7 @@ import {
12
12
  packContext,
13
13
  runDetector,
14
14
  summarize
15
- } from "../chunk-MFNGSHTZ.js";
15
+ } from "../chunk-AARGW2GV.js";
16
16
 
17
17
  // src/mcp/server.ts
18
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -40,7 +40,7 @@ function collectFiles(dir, extensions) {
40
40
  }
41
41
  var server = new McpServer({
42
42
  name: "composto",
43
- version: "0.1.2"
43
+ version: "0.2.3"
44
44
  });
45
45
  server.tool(
46
46
  "composto_ir",
@@ -112,12 +112,13 @@ server.tool(
112
112
  );
113
113
  server.tool(
114
114
  "composto_context",
115
- "Pack maximum code context into a token budget. Hotspot files get detailed IR (L1), remaining files get structure only (L0). Use this when you need to understand a large codebase without exceeding context limits.",
115
+ "Pack maximum code context into a token budget. When target symbol is provided, its file is included as raw code (L3) while surrounding files get compressed IR. Perfect for 'fix this bug in X' or 'why does X return wrong value' \u2014 LLM sees exact code of target plus compressed context. Without target, hotspot files get L1, rest get L0.",
116
116
  {
117
117
  path: z.string().default(".").describe("Directory to pack"),
118
- budget: z.number().default(4e3).describe("Maximum tokens to use")
118
+ budget: z.number().default(4e3).describe("Maximum tokens to use"),
119
+ target: z.string().optional().describe("Target symbol (function/class/variable name). Its file will be included as raw code for implementation tasks.")
119
120
  },
120
- async ({ path, budget }) => {
121
+ async ({ path, budget, target }) => {
121
122
  const projectPath = resolve(path);
122
123
  const files = collectFiles(projectPath, ALL_EXTENSIONS);
123
124
  const config = loadConfig(projectPath);
@@ -131,11 +132,21 @@ server.tool(
131
132
  const relPath = relative(projectPath, file);
132
133
  return { path: relPath, code, rawTokens: estimateTokens(code) };
133
134
  });
134
- const result = await packContext(fileInputs, { budget, hotspots });
135
- const lines = [`Composto Context \u2014 ${result.totalTokens}/${result.budget} tokens
136
- `];
135
+ const result = await packContext(fileInputs, { budget, hotspots, target });
136
+ const lines = [`Composto Context \u2014 ${result.totalTokens}/${result.budget} tokens`];
137
+ if (target && result.targetFile) lines.push(`Target: ${target} in ${result.targetFile}`);
138
+ lines.push("");
139
+ const l3 = result.entries.filter((e) => e.layer === "L3");
137
140
  const l1 = result.entries.filter((e) => e.layer === "L1");
138
141
  const l0 = result.entries.filter((e) => e.layer === "L0");
142
+ if (l3.length > 0) {
143
+ lines.push("== L3 (raw \u2014 target file) ==\n");
144
+ for (const entry of l3) {
145
+ lines.push(`[target] ${entry.path}`);
146
+ lines.push(entry.ir);
147
+ lines.push("");
148
+ }
149
+ }
139
150
  if (l1.length > 0) {
140
151
  lines.push("== L1 (detailed) ==\n");
141
152
  for (const entry of l1) {
@@ -151,8 +162,12 @@ server.tool(
151
162
  lines.push(entry.ir);
152
163
  }
153
164
  }
165
+ const parts = [];
166
+ if (result.filesAtL3 > 0) parts.push(`${result.filesAtL3} at L3`);
167
+ if (result.filesAtL1 > 0) parts.push(`${result.filesAtL1} at L1`);
168
+ if (result.filesAtL0 > 0) parts.push(`${result.filesAtL0} at L0`);
154
169
  lines.push(`
155
- Files: ${result.filesAtL1} at L1, ${result.filesAtL0} at L0`);
170
+ Files: ${parts.join(", ")}`);
156
171
  return {
157
172
  content: [{ type: "text", text: lines.join("\n") }]
158
173
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "composto-ai",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Proactive AI team companion — less tokens, more insight",
5
5
  "type": "module",
6
6
  "bin": {