@jefuriiij/synthra 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  // src/server/http.ts
2
2
  import { serve } from "@hono/node-server";
3
3
  import { Hono } from "hono";
4
- import { writeFile as writeFile8 } from "fs/promises";
4
+ import { writeFile as writeFile9 } from "fs/promises";
5
5
 
6
6
  // src/activity/activity-log.ts
7
7
  import { appendFile, mkdir } from "fs/promises";
@@ -274,7 +274,7 @@ import { resolve } from "path";
274
274
  import { dirname as dirname2, join as join3, posix } from "path";
275
275
 
276
276
  // src/graph/types.ts
277
- var SCHEMA_VERSION = 1;
277
+ var SCHEMA_VERSION = 2;
278
278
 
279
279
  // src/scanner/hash.ts
280
280
  import { createHash } from "crypto";
@@ -592,14 +592,20 @@ async function buildGraph(root, parsed) {
592
592
  for (const p of parsed) filesByPath.set(p.file.relPath, true);
593
593
  const nodes = [];
594
594
  const edges = [];
595
+ const symbolsByFile = /* @__PURE__ */ new Map();
596
+ const callsByFile = /* @__PURE__ */ new Map();
595
597
  for (const p of parsed) {
596
598
  const fileNode = toFileNode(p);
597
599
  nodes.push(fileNode);
600
+ const fileSymNodes = [];
598
601
  for (const sym of p.symbols) {
599
602
  const symNode = toSymbolNode(p, sym);
600
603
  nodes.push(symNode);
604
+ fileSymNodes.push(symNode);
601
605
  edges.push({ from: fileNode.id, to: symNode.id, kind: "defines" });
602
606
  }
607
+ symbolsByFile.set(p.file.relPath, fileSymNodes);
608
+ callsByFile.set(p.file.relPath, p.calls);
603
609
  const importEdges = /* @__PURE__ */ new Set();
604
610
  for (const spec of p.imports) {
605
611
  const target = resolveImport(p.file.relPath, spec, filesByPath);
@@ -614,6 +620,7 @@ async function buildGraph(root, parsed) {
614
620
  edges.push({ from: fileNode.id, to: fileId(testTargetPath), kind: "tests" });
615
621
  }
616
622
  }
623
+ edges.push(...buildCallEdges(symbolsByFile, callsByFile));
617
624
  const symbolCount = nodes.filter((n) => n.kind === "symbol").length;
618
625
  const fileCount = nodes.length - symbolCount;
619
626
  return {
@@ -637,6 +644,49 @@ function buildSymbolIndex(graph) {
637
644
  }
638
645
  return out;
639
646
  }
647
+ function tightestContainer(syms, line) {
648
+ let best = null;
649
+ for (const s of syms) {
650
+ if (line < s.start_line || line > s.end_line) continue;
651
+ if (!best || s.end_line - s.start_line < best.end_line - best.start_line) best = s;
652
+ }
653
+ return best;
654
+ }
655
+ function buildCallEdges(symbolsByFile, callsByFile) {
656
+ const byName = /* @__PURE__ */ new Map();
657
+ for (const syms of symbolsByFile.values()) {
658
+ for (const s of syms) {
659
+ const list = byName.get(s.name);
660
+ if (list) list.push(s);
661
+ else byName.set(s.name, [s]);
662
+ }
663
+ }
664
+ const edges = [];
665
+ const seen = /* @__PURE__ */ new Set();
666
+ for (const [relPath, sites] of callsByFile) {
667
+ const fileSyms = symbolsByFile.get(relPath) ?? [];
668
+ for (const site of sites) {
669
+ const caller = tightestContainer(fileSyms, site.line);
670
+ if (!caller) continue;
671
+ let callee = fileSyms.find((s) => s.name === site.callee);
672
+ if (!callee) {
673
+ const cands = byName.get(site.callee) ?? [];
674
+ if (cands.length !== 1) continue;
675
+ callee = cands[0];
676
+ }
677
+ if (!callee || callee.id === caller.id) continue;
678
+ const key = `${caller.id}->${callee.id}`;
679
+ if (seen.has(key)) continue;
680
+ seen.add(key);
681
+ edges.push({ from: caller.id, to: callee.id, kind: "calls" });
682
+ }
683
+ }
684
+ return edges;
685
+ }
686
+
687
+ // src/scanner/parse-cache.ts
688
+ import { mkdir as mkdir2, readFile as readFile4, writeFile } from "fs/promises";
689
+ import { dirname as dirname3 } from "path";
640
690
 
641
691
  // src/scanner/parser.ts
642
692
  import { readFile as readFile3 } from "fs/promises";
@@ -655,10 +705,11 @@ function cleanImport(s) {
655
705
  async function runGenericParser(config, f, source) {
656
706
  let symbols = [];
657
707
  let imports = [];
708
+ const calls = [];
658
709
  try {
659
710
  const { parser, language } = await createParser(config.grammar);
660
711
  const tree = parser.parse(source);
661
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
712
+ if (!tree) return { file: f, source, symbols, imports, calls };
662
713
  const query = new Query(language, config.query);
663
714
  const matches = query.matches(tree.rootNode);
664
715
  for (const match of matches) {
@@ -683,6 +734,14 @@ async function runGenericParser(config, f, source) {
683
734
  });
684
735
  continue;
685
736
  }
737
+ if (config.callCapture && config.callCalleeCapture) {
738
+ const callNode = byName.get(config.callCapture);
739
+ const calleeNode = byName.get(config.callCalleeCapture);
740
+ if (callNode && calleeNode) {
741
+ calls.push({ callee: calleeNode.text, line: callNode.startPosition.row + 1 });
742
+ continue;
743
+ }
744
+ }
686
745
  if (config.importCapture) {
687
746
  const imp = byName.get(config.importCapture);
688
747
  if (imp) imports.push(cleanImport(imp.text));
@@ -698,7 +757,7 @@ async function runGenericParser(config, f, source) {
698
757
  imports = Array.from(new Set(imports)).filter((s) => s.length > 0);
699
758
  } catch {
700
759
  }
701
- return { file: f, source, symbols, imports, calls: [] };
760
+ return { file: f, source, symbols, imports, calls };
702
761
  }
703
762
 
704
763
  // src/scanner/parsers/c.ts
@@ -709,6 +768,7 @@ var QUERY = `
709
768
  (type_definition declarator: (type_identifier) @type.name) @type
710
769
  (preproc_include path: (string_literal) @import)
711
770
  (preproc_include path: (system_lib_string) @import)
771
+ (call_expression function: (identifier) @call.name) @call
712
772
  `;
713
773
  async function parseC(f, source) {
714
774
  return runGenericParser(
@@ -721,7 +781,9 @@ async function parseC(f, source) {
721
781
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
722
782
  { declCapture: "type", nameCapture: "type.name", kind: "type" }
723
783
  ],
724
- importCapture: "import"
784
+ importCapture: "import",
785
+ callCapture: "call",
786
+ callCalleeCapture: "call.name"
725
787
  },
726
788
  f,
727
789
  source
@@ -738,6 +800,9 @@ var QUERY2 = `
738
800
  (namespace_definition name: (namespace_identifier) @namespace.name) @namespace
739
801
  (preproc_include path: (string_literal) @import)
740
802
  (preproc_include path: (system_lib_string) @import)
803
+ (call_expression function: (identifier) @call.name) @call
804
+ (call_expression function: (field_expression field: (field_identifier) @call.name)) @call
805
+ (call_expression function: (qualified_identifier name: (identifier) @call.name)) @call
741
806
  `;
742
807
  async function parseCpp(f, source) {
743
808
  return runGenericParser(
@@ -752,7 +817,9 @@ async function parseCpp(f, source) {
752
817
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
753
818
  { declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
754
819
  ],
755
- importCapture: "import"
820
+ importCapture: "import",
821
+ callCapture: "call",
822
+ callCalleeCapture: "call.name"
756
823
  },
757
824
  f,
758
825
  source
@@ -768,6 +835,8 @@ var QUERY3 = `
768
835
  (method_declaration name: (identifier) @method.name) @method
769
836
  (namespace_declaration name: (_) @namespace.name) @namespace
770
837
  (using_directive (_) @import)
838
+ (invocation_expression function: (identifier) @call.name) @call
839
+ (invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
771
840
  `;
772
841
  async function parseCSharp(f, source) {
773
842
  return runGenericParser(
@@ -782,7 +851,9 @@ async function parseCSharp(f, source) {
782
851
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
783
852
  { declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
784
853
  ],
785
- importCapture: "import"
854
+ importCapture: "import",
855
+ callCapture: "call",
856
+ callCalleeCapture: "call.name"
786
857
  },
787
858
  f,
788
859
  source
@@ -887,6 +958,8 @@ var QUERY5 = `
887
958
  (method_declaration name: (field_identifier) @method.name) @method
888
959
  (type_spec name: (type_identifier) @type.name) @type
889
960
  (import_spec path: (interpreted_string_literal) @import)
961
+ (call_expression function: (identifier) @call.name) @call
962
+ (call_expression function: (selector_expression field: (field_identifier) @call.name)) @call
890
963
  `;
891
964
  async function parseGo(f, source) {
892
965
  return runGenericParser(
@@ -898,7 +971,9 @@ async function parseGo(f, source) {
898
971
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
899
972
  { declCapture: "type", nameCapture: "type.name", kind: "type" }
900
973
  ],
901
- importCapture: "import"
974
+ importCapture: "import",
975
+ callCapture: "call",
976
+ callCalleeCapture: "call.name"
902
977
  },
903
978
  f,
904
979
  source
@@ -963,6 +1038,7 @@ var QUERY6 = `
963
1038
  (method_declaration name: (identifier) @method.name) @method
964
1039
  (enum_declaration name: (identifier) @enum.name) @enum
965
1040
  (import_declaration (scoped_identifier) @import)
1041
+ (method_invocation name: (identifier) @call.name) @call
966
1042
  `;
967
1043
  async function parseJava(f, source) {
968
1044
  return runGenericParser(
@@ -975,7 +1051,9 @@ async function parseJava(f, source) {
975
1051
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
976
1052
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" }
977
1053
  ],
978
- importCapture: "import"
1054
+ importCapture: "import",
1055
+ callCapture: "call",
1056
+ callCalleeCapture: "call.name"
979
1057
  },
980
1058
  f,
981
1059
  source
@@ -988,6 +1066,7 @@ var QUERY7 = `
988
1066
  (class_declaration (type_identifier) @class.name) @class
989
1067
  (object_declaration (type_identifier) @object.name) @object
990
1068
  (import_header (identifier) @import)
1069
+ (call_expression (simple_identifier) @call.name) @call
991
1070
  `;
992
1071
  async function parseKotlin(f, source) {
993
1072
  return runGenericParser(
@@ -999,7 +1078,9 @@ async function parseKotlin(f, source) {
999
1078
  { declCapture: "class", nameCapture: "class.name", kind: "class" },
1000
1079
  { declCapture: "object", nameCapture: "object.name", kind: "class" }
1001
1080
  ],
1002
- importCapture: "import"
1081
+ importCapture: "import",
1082
+ callCapture: "call",
1083
+ callCalleeCapture: "call.name"
1003
1084
  },
1004
1085
  f,
1005
1086
  source
@@ -1013,6 +1094,9 @@ var QUERY8 = `
1013
1094
  (interface_declaration name: (name) @interface.name) @interface
1014
1095
  (trait_declaration name: (name) @trait.name) @trait
1015
1096
  (method_declaration name: (name) @method.name) @method
1097
+ (function_call_expression function: (name) @call.name) @call
1098
+ (member_call_expression name: (name) @call.name) @call
1099
+ (scoped_call_expression name: (name) @call.name) @call
1016
1100
  `;
1017
1101
  async function parsePhp(f, source) {
1018
1102
  return runGenericParser(
@@ -1025,7 +1109,9 @@ async function parsePhp(f, source) {
1025
1109
  { declCapture: "interface", nameCapture: "interface.name", kind: "interface" },
1026
1110
  { declCapture: "trait", nameCapture: "trait.name", kind: "class" },
1027
1111
  { declCapture: "method", nameCapture: "method.name", kind: "method" }
1028
- ]
1112
+ ],
1113
+ callCapture: "call",
1114
+ callCalleeCapture: "call.name"
1029
1115
  },
1030
1116
  f,
1031
1117
  source
@@ -1040,6 +1126,8 @@ var QUERY9 = `
1040
1126
  (import_statement name: (dotted_name) @import.module)
1041
1127
  (import_from_statement module_name: (dotted_name) @import.from)
1042
1128
  (import_from_statement module_name: (relative_import) @import.from)
1129
+ (call function: (identifier) @call.name) @call
1130
+ (call function: (attribute attribute: (identifier) @call.name)) @call
1043
1131
  `;
1044
1132
  function firstLine3(text, max = 200) {
1045
1133
  const line = text.split(/\r?\n/, 1)[0] ?? "";
@@ -1048,10 +1136,11 @@ function firstLine3(text, max = 200) {
1048
1136
  async function parsePython(f, source) {
1049
1137
  let symbols = [];
1050
1138
  let imports = [];
1139
+ const calls = [];
1051
1140
  try {
1052
1141
  const { parser, language } = await createParser("python");
1053
1142
  const tree = parser.parse(source);
1054
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
1143
+ if (!tree) return { file: f, source, symbols, imports, calls };
1055
1144
  const query = new Query3(language, QUERY9);
1056
1145
  const matches = query.matches(tree.rootNode);
1057
1146
  for (const match of matches) {
@@ -1084,7 +1173,15 @@ async function parsePython(f, source) {
1084
1173
  continue;
1085
1174
  }
1086
1175
  const importNode = byName.get("import.module") ?? byName.get("import.from");
1087
- if (importNode) imports.push(importNode.text);
1176
+ if (importNode) {
1177
+ imports.push(importNode.text);
1178
+ continue;
1179
+ }
1180
+ const callName = byName.get("call.name");
1181
+ const callNode = byName.get("call");
1182
+ if (callName && callNode) {
1183
+ calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
1184
+ }
1088
1185
  }
1089
1186
  const seen = /* @__PURE__ */ new Set();
1090
1187
  symbols = symbols.filter((s) => {
@@ -1096,7 +1193,7 @@ async function parsePython(f, source) {
1096
1193
  imports = Array.from(new Set(imports));
1097
1194
  } catch {
1098
1195
  }
1099
- return { file: f, source, symbols, imports, calls: [] };
1196
+ return { file: f, source, symbols, imports, calls };
1100
1197
  }
1101
1198
 
1102
1199
  // src/scanner/parsers/ruby.ts
@@ -1105,6 +1202,7 @@ var QUERY10 = `
1105
1202
  (singleton_method name: (identifier) @method.name) @method
1106
1203
  (class name: (constant) @class.name) @class
1107
1204
  (module name: (constant) @module.name) @module
1205
+ (call method: (identifier) @call.name) @call
1108
1206
  `;
1109
1207
  async function parseRuby(f, source) {
1110
1208
  return runGenericParser(
@@ -1116,7 +1214,9 @@ async function parseRuby(f, source) {
1116
1214
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
1117
1215
  { declCapture: "class", nameCapture: "class.name", kind: "class" },
1118
1216
  { declCapture: "module", nameCapture: "module.name", kind: "class" }
1119
- ]
1217
+ ],
1218
+ callCapture: "call",
1219
+ callCalleeCapture: "call.name"
1120
1220
  },
1121
1221
  f,
1122
1222
  source
@@ -1130,6 +1230,9 @@ var QUERY11 = `
1130
1230
  (enum_item name: (type_identifier) @enum.name) @enum
1131
1231
  (trait_item name: (type_identifier) @trait.name) @trait
1132
1232
  (impl_item type: (type_identifier) @impl.name) @impl
1233
+ (call_expression function: (identifier) @call.name) @call
1234
+ (call_expression function: (scoped_identifier name: (identifier) @call.name)) @call
1235
+ (call_expression function: (field_expression field: (field_identifier) @call.name)) @call
1133
1236
  `;
1134
1237
  async function parseRust(f, source) {
1135
1238
  return runGenericParser(
@@ -1142,7 +1245,9 @@ async function parseRust(f, source) {
1142
1245
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
1143
1246
  { declCapture: "trait", nameCapture: "trait.name", kind: "interface" },
1144
1247
  { declCapture: "impl", nameCapture: "impl.name", kind: "class" }
1145
- ]
1248
+ ],
1249
+ callCapture: "call",
1250
+ callCalleeCapture: "call.name"
1146
1251
  },
1147
1252
  f,
1148
1253
  source
@@ -1161,6 +1266,8 @@ var TS_QUERY = `
1161
1266
  (lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
1162
1267
  (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
1163
1268
  (import_statement source: (string) @import)
1269
+ (call_expression function: (identifier) @call.name) @call
1270
+ (call_expression function: (member_expression property: (property_identifier) @call.name)) @call
1164
1271
  `;
1165
1272
  var JS_QUERY = `
1166
1273
  (function_declaration name: (identifier) @function.name) @function
@@ -1170,6 +1277,8 @@ var JS_QUERY = `
1170
1277
  (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
1171
1278
  (import_statement source: (string) @import)
1172
1279
  (call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
1280
+ (call_expression function: (identifier) @call.name) @call
1281
+ (call_expression function: (member_expression property: (property_identifier) @call.name)) @call
1173
1282
  `;
1174
1283
  function grammarFor(ext) {
1175
1284
  if (ext === ".tsx" || ext === ".jsx") return "tsx";
@@ -1198,10 +1307,11 @@ async function parseTypeScript(f, source) {
1198
1307
  const grammar = grammarFor(f.ext);
1199
1308
  let symbols = [];
1200
1309
  let imports = [];
1310
+ const calls = [];
1201
1311
  try {
1202
1312
  const { parser, language } = await createParser(grammar);
1203
1313
  const tree = parser.parse(source);
1204
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
1314
+ if (!tree) return { file: f, source, symbols, imports, calls };
1205
1315
  const query = new Query4(language, queryFor(grammar));
1206
1316
  const matches = query.matches(tree.rootNode);
1207
1317
  for (const match of matches) {
@@ -1227,6 +1337,12 @@ async function parseTypeScript(f, source) {
1227
1337
  const requireSource = byName.get("require_source");
1228
1338
  if (requireFn && requireSource && requireFn.text === "require") {
1229
1339
  imports.push(unquote(requireSource.text));
1340
+ continue;
1341
+ }
1342
+ const callName = byName.get("call.name");
1343
+ const callNode = byName.get("call");
1344
+ if (callName && callNode) {
1345
+ calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
1230
1346
  }
1231
1347
  }
1232
1348
  const seen = /* @__PURE__ */ new Set();
@@ -1239,7 +1355,7 @@ async function parseTypeScript(f, source) {
1239
1355
  imports = Array.from(new Set(imports));
1240
1356
  } catch {
1241
1357
  }
1242
- return { file: f, source, symbols, imports, calls: [] };
1358
+ return { file: f, source, symbols, imports, calls };
1243
1359
  }
1244
1360
 
1245
1361
  // src/scanner/parsers/svelte.ts
@@ -1273,6 +1389,7 @@ async function parseSvelte(f, source) {
1273
1389
  });
1274
1390
  }
1275
1391
  for (const imp of parsed.imports) out.imports.push(imp);
1392
+ for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
1276
1393
  }
1277
1394
  out.symbols.push({
1278
1395
  name: f.relPath.split("/").pop()?.replace(/\.svelte$/i, "") ?? f.relPath,
@@ -1316,6 +1433,7 @@ async function parseVue(f, source) {
1316
1433
  });
1317
1434
  }
1318
1435
  for (const imp of parsed.imports) out.imports.push(imp);
1436
+ for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
1319
1437
  }
1320
1438
  out.symbols.push({
1321
1439
  name: f.relPath.split("/").pop()?.replace(/\.vue$/i, "") ?? f.relPath,
@@ -1372,13 +1490,7 @@ async function createParser(name) {
1372
1490
  function emptyParsed(file, source) {
1373
1491
  return { file, source, symbols: [], imports: [], calls: [] };
1374
1492
  }
1375
- async function parseFile(f) {
1376
- let source;
1377
- try {
1378
- source = await readFile3(f.absPath, "utf8");
1379
- } catch {
1380
- return emptyParsed(f, "");
1381
- }
1493
+ async function parseSource(f, source) {
1382
1494
  switch (f.ext) {
1383
1495
  case ".ts":
1384
1496
  case ".tsx":
@@ -1431,8 +1543,72 @@ async function parseFile(f) {
1431
1543
  }
1432
1544
  }
1433
1545
 
1546
+ // src/scanner/parse-cache.ts
1547
+ var PARSE_CACHE_VERSION = 2;
1548
+ function emptyParseCache() {
1549
+ return { schema_version: PARSE_CACHE_VERSION, files: {} };
1550
+ }
1551
+ async function readParseCache(path) {
1552
+ try {
1553
+ const raw = await readFile4(path, "utf8");
1554
+ const parsed = JSON.parse(raw);
1555
+ if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
1556
+ return emptyParseCache();
1557
+ }
1558
+ return { schema_version: PARSE_CACHE_VERSION, files: parsed.files };
1559
+ } catch {
1560
+ return emptyParseCache();
1561
+ }
1562
+ }
1563
+ async function writeParseCache(path, cache) {
1564
+ try {
1565
+ await mkdir2(dirname3(path), { recursive: true });
1566
+ await writeFile(path, `${JSON.stringify(cache)}
1567
+ `, "utf8");
1568
+ } catch {
1569
+ }
1570
+ }
1571
+ async function incrementalParse(parsable, prev, opts = {}) {
1572
+ const cache = emptyParseCache();
1573
+ const parsed = [];
1574
+ let reused = 0;
1575
+ let reparsed = 0;
1576
+ let parseErrors = 0;
1577
+ for (const f of parsable) {
1578
+ let source;
1579
+ try {
1580
+ source = await readFile4(f.absPath, "utf8");
1581
+ } catch {
1582
+ continue;
1583
+ }
1584
+ const hash = fileHash(source);
1585
+ const cached = opts.full ? void 0 : prev.files[f.relPath];
1586
+ if (cached && cached.hash === hash) {
1587
+ parsed.push({
1588
+ file: f,
1589
+ source,
1590
+ symbols: cached.symbols,
1591
+ imports: cached.imports,
1592
+ calls: cached.calls
1593
+ });
1594
+ cache.files[f.relPath] = cached;
1595
+ reused += 1;
1596
+ continue;
1597
+ }
1598
+ try {
1599
+ const p = await parseSource(f, source);
1600
+ parsed.push(p);
1601
+ cache.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
1602
+ reparsed += 1;
1603
+ } catch {
1604
+ parseErrors += 1;
1605
+ }
1606
+ }
1607
+ return { parsed, cache, reused, reparsed, parseErrors };
1608
+ }
1609
+
1434
1610
  // src/scanner/walker.ts
1435
- import { readFile as readFile4, readdir, stat } from "fs/promises";
1611
+ import { readFile as readFile5, readdir, stat } from "fs/promises";
1436
1612
  import { extname, join as join4, relative as relative2, sep as sep2 } from "path";
1437
1613
  import ignore2 from "ignore";
1438
1614
  var DEFAULT_IGNORE = [
@@ -1513,7 +1689,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
1513
1689
  ]);
1514
1690
  async function readIgnoreFile2(path) {
1515
1691
  try {
1516
- const text = await readFile4(path, "utf8");
1692
+ const text = await readFile5(path, "utf8");
1517
1693
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
1518
1694
  } catch {
1519
1695
  return [];
@@ -1568,15 +1744,15 @@ async function* walk(root, options = {}) {
1568
1744
  }
1569
1745
 
1570
1746
  // src/graph/store.ts
1571
- import { mkdir as mkdir2, readFile as readFile5, writeFile } from "fs/promises";
1572
- import { dirname as dirname3 } from "path";
1747
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
1748
+ import { dirname as dirname4 } from "path";
1573
1749
  async function writeJson(path, data, pretty) {
1574
- await mkdir2(dirname3(path), { recursive: true });
1750
+ await mkdir3(dirname4(path), { recursive: true });
1575
1751
  const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
1576
- await writeFile(path, text + "\n", "utf8");
1752
+ await writeFile2(path, text + "\n", "utf8");
1577
1753
  }
1578
1754
  async function readJson(path) {
1579
- const text = await readFile5(path, "utf8");
1755
+ const text = await readFile6(path, "utf8");
1580
1756
  return JSON.parse(text);
1581
1757
  }
1582
1758
  async function writeGraph(path, graph) {
@@ -1612,6 +1788,7 @@ function resolvePaths(projectRoot) {
1612
1788
  toolLog: join5(graphDir, "tool_log.jsonl"),
1613
1789
  accessLog: join5(graphDir, "access_log.jsonl"),
1614
1790
  learnStore: join5(graphDir, "learn_store.json"),
1791
+ parseCache: join5(graphDir, "parse_cache.json"),
1615
1792
  mcpPort: join5(graphDir, "mcp_port"),
1616
1793
  mcpServerLog: join5(graphDir, "mcp_server.log"),
1617
1794
  mcpServerErrLog: join5(graphDir, "mcp_server.err.log"),
@@ -1627,12 +1804,12 @@ function resolvePaths(projectRoot) {
1627
1804
  }
1628
1805
 
1629
1806
  // src/cli/bootstrap.ts
1630
- import { mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile3 } from "fs/promises";
1807
+ import { mkdir as mkdir4, readFile as readFile8, stat as stat2, writeFile as writeFile4 } from "fs/promises";
1631
1808
  import { basename as basename2 } from "path";
1632
1809
 
1633
1810
  // src/hooks/claude-md.ts
1634
- import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
1635
- import { basename, dirname as dirname4 } from "path";
1811
+ import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
1812
+ import { basename, dirname as dirname5 } from "path";
1636
1813
  var POLICY_VERSION = 6;
1637
1814
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
1638
1815
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
@@ -1794,14 +1971,14 @@ function onboardingSkeleton(projectName) {
1794
1971
  async function patchClaudeMd(path, projectName) {
1795
1972
  let existing;
1796
1973
  try {
1797
- existing = await readFile6(path, "utf8");
1974
+ existing = await readFile7(path, "utf8");
1798
1975
  } catch {
1799
1976
  existing = null;
1800
1977
  }
1801
1978
  const block = policyBlock();
1802
1979
  if (existing === null) {
1803
- const name = projectName || basename(dirname4(path)) || "this project";
1804
- await writeFile2(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
1980
+ const name = projectName || basename(dirname5(path)) || "this project";
1981
+ await writeFile3(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
1805
1982
  return { created: true, updated: false, skipped: false };
1806
1983
  }
1807
1984
  const stripped = existing.replace(ANY_BLOCK_RE, "");
@@ -1810,7 +1987,7 @@ async function patchClaudeMd(path, projectName) {
1810
1987
  if (hadBlock && desired === existing) {
1811
1988
  return { created: false, updated: false, skipped: true };
1812
1989
  }
1813
- await writeFile2(path, desired, "utf8");
1990
+ await writeFile3(path, desired, "utf8");
1814
1991
  return { created: false, updated: true, skipped: false };
1815
1992
  }
1816
1993
 
@@ -1835,13 +2012,13 @@ async function exists(path) {
1835
2012
  }
1836
2013
  async function ensureDir(path) {
1837
2014
  const had = await exists(path);
1838
- await mkdir3(path, { recursive: true });
2015
+ await mkdir4(path, { recursive: true });
1839
2016
  return !had;
1840
2017
  }
1841
2018
  async function patchGitignore(path) {
1842
2019
  let existing = "";
1843
2020
  try {
1844
- existing = await readFile7(path, "utf8");
2021
+ existing = await readFile8(path, "utf8");
1845
2022
  } catch {
1846
2023
  }
1847
2024
  const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
@@ -1850,7 +2027,7 @@ async function patchGitignore(path) {
1850
2027
  const block = missing.map((m) => `# ${m.comment}
1851
2028
  ${m.entry}`).join("\n") + "\n";
1852
2029
  const appendix = (existing.length === 0 || existing.endsWith("\n") ? "" : "\n") + (existing.length ? "\n" : "") + block;
1853
- await writeFile3(path, existing + appendix, "utf8");
2030
+ await writeFile4(path, existing + appendix, "utf8");
1854
2031
  return true;
1855
2032
  }
1856
2033
  async function bootstrap(paths) {
@@ -1924,25 +2101,22 @@ async function scanProject(projectRootRaw, opts = {}) {
1924
2101
  for await (const file of walk(projectRoot)) walked.push(file);
1925
2102
  if (verbose) log.info(` walked ${walked.length} files`);
1926
2103
  const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
1927
- const parsed = [];
1928
- let parseErrors = 0;
1929
- for (const file of parsable) {
1930
- try {
1931
- parsed.push(await parseFile(file));
1932
- } catch (err2) {
1933
- parseErrors += 1;
1934
- if (verbose) log.debug(` parse failed: ${file.relPath} \u2014 ${err2.message}`);
1935
- }
1936
- }
2104
+ const prevCache = await readParseCache(paths.parseCache);
2105
+ const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
2106
+ parsable,
2107
+ prevCache,
2108
+ { full: opts.full }
2109
+ );
1937
2110
  if (verbose) {
1938
2111
  log.info(
1939
- ` parsed ${parsed.length} files (${walked.length - parsable.length} skipped` + (parseErrors ? `, ${parseErrors} errored` : "") + ")"
2112
+ ` parsed ${parsed.length} files (${reused} reused \xB7 ${reparsed} reparsed` + (parseErrors ? `, ${parseErrors} errored` : "") + `; ${walked.length - parsable.length} non-code skipped)`
1940
2113
  );
1941
2114
  }
1942
2115
  const graph = await buildGraph(projectRoot, parsed);
1943
2116
  const symbolIndex = buildSymbolIndex(graph);
1944
2117
  await writeGraph(paths.infoGraph, graph);
1945
2118
  await writeSymbolIndex(paths.symbolIndex, symbolIndex);
2119
+ await writeParseCache(paths.parseCache, cache);
1946
2120
  if (verbose) {
1947
2121
  log.info(
1948
2122
  ` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
@@ -1961,8 +2135,8 @@ async function scanProject(projectRootRaw, opts = {}) {
1961
2135
  }
1962
2136
 
1963
2137
  // src/learn/store.ts
1964
- import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
1965
- import { dirname as dirname5 } from "path";
2138
+ import { appendFile as appendFile2, mkdir as mkdir5, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
2139
+ import { dirname as dirname6 } from "path";
1966
2140
 
1967
2141
  // src/learn/usage.ts
1968
2142
  var LEARN_SCHEMA_VERSION = 1;
@@ -2029,7 +2203,7 @@ function recomputeFromLog(events) {
2029
2203
  // src/learn/store.ts
2030
2204
  async function readLearnStore(path) {
2031
2205
  try {
2032
- const raw = await readFile8(path, "utf8");
2206
+ const raw = await readFile9(path, "utf8");
2033
2207
  const parsed = JSON.parse(raw);
2034
2208
  if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
2035
2209
  return emptyStore();
@@ -2045,14 +2219,14 @@ async function readLearnStore(path) {
2045
2219
  }
2046
2220
  async function writeLearnStore(path, store) {
2047
2221
  try {
2048
- await mkdir4(dirname5(path), { recursive: true });
2049
- await writeFile4(path, JSON.stringify(store, null, 2) + "\n", "utf8");
2222
+ await mkdir5(dirname6(path), { recursive: true });
2223
+ await writeFile5(path, JSON.stringify(store, null, 2) + "\n", "utf8");
2050
2224
  } catch {
2051
2225
  }
2052
2226
  }
2053
2227
  async function readAccessLog(path) {
2054
2228
  try {
2055
- const raw = await readFile8(path, "utf8");
2229
+ const raw = await readFile9(path, "utf8");
2056
2230
  const out = [];
2057
2231
  for (const line of raw.split("\n")) {
2058
2232
  const t = line.trim();
@@ -2072,7 +2246,7 @@ async function readAccessLog(path) {
2072
2246
  }
2073
2247
  async function appendAccess(path, ev) {
2074
2248
  try {
2075
- await mkdir4(dirname5(path), { recursive: true });
2249
+ await mkdir5(dirname6(path), { recursive: true });
2076
2250
  await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
2077
2251
  } catch {
2078
2252
  }
@@ -2136,8 +2310,8 @@ var LearnRuntime = class _LearnRuntime {
2136
2310
  };
2137
2311
 
2138
2312
  // src/server/mcp.ts
2139
- import { appendFile as appendFile3, mkdir as mkdir7 } from "fs/promises";
2140
- import { dirname as dirname8 } from "path";
2313
+ import { appendFile as appendFile3, mkdir as mkdir8 } from "fs/promises";
2314
+ import { dirname as dirname9 } from "path";
2141
2315
 
2142
2316
  // src/graph/rank.ts
2143
2317
  var KW_BASE_WEIGHT = 2;
@@ -2390,14 +2564,14 @@ async function retrieve(graph, query, options = {}) {
2390
2564
 
2391
2565
  // src/memory/branches.ts
2392
2566
  import { execFile as execFile2 } from "child_process";
2393
- import { readFile as readFile9 } from "fs/promises";
2567
+ import { readFile as readFile10 } from "fs/promises";
2394
2568
  import { join as join6 } from "path";
2395
2569
  import { promisify as promisify2 } from "util";
2396
2570
  var execFileAsync2 = promisify2(execFile2);
2397
2571
  async function currentBranch(projectRoot) {
2398
2572
  try {
2399
2573
  const headPath = join6(projectRoot, ".git", "HEAD");
2400
- const head = await readFile9(headPath, "utf8");
2574
+ const head = await readFile10(headPath, "utf8");
2401
2575
  const trimmed = head.trim();
2402
2576
  const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
2403
2577
  if (match?.[1]) return match[1];
@@ -2447,8 +2621,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
2447
2621
  }
2448
2622
 
2449
2623
  // src/memory/context-md.ts
2450
- import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
2451
- import { dirname as dirname6 } from "path";
2624
+ import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
2625
+ import { dirname as dirname7 } from "path";
2452
2626
  var MAX_BULLETS = 3;
2453
2627
  function deriveContextMd(entries, branch) {
2454
2628
  const tasks = entries.filter((e) => e.type === "task").reverse();
@@ -2491,17 +2665,17 @@ function formatContextMd(ctx) {
2491
2665
  return lines.join("\n");
2492
2666
  }
2493
2667
  async function writeContextMd(path, ctx) {
2494
- await mkdir5(dirname6(path), { recursive: true });
2495
- await writeFile5(path, formatContextMd(ctx), "utf8");
2668
+ await mkdir6(dirname7(path), { recursive: true });
2669
+ await writeFile6(path, formatContextMd(ctx), "utf8");
2496
2670
  }
2497
2671
 
2498
2672
  // src/memory/context-store.ts
2499
- import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
2500
- import { dirname as dirname7 } from "path";
2673
+ import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
2674
+ import { dirname as dirname8 } from "path";
2501
2675
  var SCHEMA_VERSION2 = 1;
2502
2676
  async function readEntries(path) {
2503
2677
  try {
2504
- const raw = await readFile11(path, "utf8");
2678
+ const raw = await readFile12(path, "utf8");
2505
2679
  const parsed = JSON.parse(raw);
2506
2680
  return Array.isArray(parsed.entries) ? parsed.entries : [];
2507
2681
  } catch {
@@ -2509,9 +2683,9 @@ async function readEntries(path) {
2509
2683
  }
2510
2684
  }
2511
2685
  async function writeEntries(path, entries) {
2512
- await mkdir6(dirname7(path), { recursive: true });
2686
+ await mkdir7(dirname8(path), { recursive: true });
2513
2687
  const store = { schema_version: SCHEMA_VERSION2, entries };
2514
- await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
2688
+ await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
2515
2689
  }
2516
2690
  async function appendEntry(path, entry) {
2517
2691
  const entries = await readEntries(path);
@@ -2888,7 +3062,7 @@ var TOOLS = [
2888
3062
  },
2889
3063
  {
2890
3064
  name: "blast_radius",
2891
- description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports + tests edges. Use BEFORE editing a widely-used file to see what could break. Symbol-level granularity is approximated at the file level (we don't track call edges in v0.1).",
3065
+ description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports, tests, and call edges (callers). Use BEFORE editing a widely-used file to see what could break. Call edges are name-resolved (precise within a file, unique-name across files) and projected to file granularity.",
2892
3066
  inputSchema: {
2893
3067
  type: "object",
2894
3068
  properties: {
@@ -2900,7 +3074,7 @@ var TOOLS = [
2900
3074
  },
2901
3075
  {
2902
3076
  name: "dead_code",
2903
- description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity (v0.1 limitation \u2014 symbol-level needs call-graph edges). Common entry-point patterns (main, index, app, CLI, bin/) are excluded heuristically.",
3077
+ description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity; symbol-level dead code (unused exports, on top of the call graph) is a planned follow-up. Common entry-point patterns (main, index, app, CLI, bin/) are excluded heuristically.",
2904
3078
  inputSchema: {
2905
3079
  type: "object",
2906
3080
  properties: {
@@ -2946,12 +3120,24 @@ function blastRadius(args, ctx) {
2946
3120
  const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
2947
3121
  const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
2948
3122
  if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
3123
+ const fileIdBySymbol = /* @__PURE__ */ new Map();
3124
+ for (const n of ctx.graph.nodes) {
3125
+ if (n.kind === "symbol") fileIdBySymbol.set(n.id, `file:${n.file}`);
3126
+ }
2949
3127
  const incoming = /* @__PURE__ */ new Map();
3128
+ const addIncoming = (to, from, kind) => {
3129
+ const list = incoming.get(to) ?? [];
3130
+ list.push({ from, kind });
3131
+ incoming.set(to, list);
3132
+ };
2950
3133
  for (const e of ctx.graph.edges) {
2951
- if (e.kind !== "imports" && e.kind !== "tests") continue;
2952
- const list = incoming.get(e.to) ?? [];
2953
- list.push({ from: e.from, kind: e.kind });
2954
- incoming.set(e.to, list);
3134
+ if (e.kind === "imports" || e.kind === "tests") {
3135
+ addIncoming(e.to, e.from, e.kind);
3136
+ } else if (e.kind === "calls") {
3137
+ const fromFile = fileIdBySymbol.get(e.from);
3138
+ const toFile = fileIdBySymbol.get(e.to);
3139
+ if (fromFile && toFile && fromFile !== toFile) addIncoming(toFile, fromFile, "calls");
3140
+ }
2955
3141
  }
2956
3142
  const visited = /* @__PURE__ */ new Set([root.id]);
2957
3143
  const hits = [];
@@ -3029,7 +3215,7 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
3029
3215
  }
3030
3216
  lines.push("");
3031
3217
  lines.push(
3032
- `_v0.1 caveat:_ this is file-level only. Symbol-level dead code (unused exports) needs call-graph edges, which land in v0.2.`
3218
+ `_caveat:_ this is file-level only. Symbol-level dead code (unused exports), built on the now-populated call graph, is a planned follow-up.`
3033
3219
  );
3034
3220
  return textContent(lines.join("\n"));
3035
3221
  }
@@ -3181,7 +3367,7 @@ async function contextRecall(args, ctx) {
3181
3367
  }
3182
3368
  async function logToolCall(ctx, tool) {
3183
3369
  try {
3184
- await mkdir7(dirname8(ctx.paths.toolLog), { recursive: true });
3370
+ await mkdir8(dirname9(ctx.paths.toolLog), { recursive: true });
3185
3371
  await appendFile3(
3186
3372
  ctx.paths.toolLog,
3187
3373
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
@@ -3299,12 +3485,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
3299
3485
  }
3300
3486
 
3301
3487
  // src/memory/session.ts
3302
- import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
3303
- import { dirname as dirname9 } from "path";
3488
+ import { mkdir as mkdir9, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
3489
+ import { dirname as dirname10 } from "path";
3304
3490
  var SESSION_SCHEMA_VERSION = 1;
3305
3491
  async function readSession(path) {
3306
3492
  try {
3307
- const raw = await readFile12(path, "utf8");
3493
+ const raw = await readFile13(path, "utf8");
3308
3494
  const parsed = JSON.parse(raw);
3309
3495
  if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
3310
3496
  return parsed;
@@ -3313,8 +3499,8 @@ async function readSession(path) {
3313
3499
  }
3314
3500
  }
3315
3501
  async function writeSession(path, state) {
3316
- await mkdir8(dirname9(path), { recursive: true });
3317
- await writeFile7(path, JSON.stringify(state, null, 2) + "\n", "utf8");
3502
+ await mkdir9(dirname10(path), { recursive: true });
3503
+ await writeFile8(path, JSON.stringify(state, null, 2) + "\n", "utf8");
3318
3504
  }
3319
3505
 
3320
3506
  // src/server/routes/context-update.ts
@@ -3359,8 +3545,8 @@ async function handleContextUpdate(req, ctx) {
3359
3545
  }
3360
3546
 
3361
3547
  // src/server/routes/gate.ts
3362
- import { appendFile as appendFile4, mkdir as mkdir9 } from "fs/promises";
3363
- import { dirname as dirname10 } from "path";
3548
+ import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
3549
+ import { dirname as dirname11 } from "path";
3364
3550
  var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3365
3551
  var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
3366
3552
  function extractQuery(toolName, input) {
@@ -3416,7 +3602,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
3416
3602
  }
3417
3603
  async function logDecision(ctx, toolName, query, decision, reason) {
3418
3604
  try {
3419
- await mkdir9(dirname10(ctx.paths.gateLog), { recursive: true });
3605
+ await mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
3420
3606
  const entry = {
3421
3607
  ts: (/* @__PURE__ */ new Date()).toISOString(),
3422
3608
  tool: toolName,
@@ -3488,15 +3674,15 @@ async function handleGate(req, ctx) {
3488
3674
  }
3489
3675
 
3490
3676
  // src/server/routes/log.ts
3491
- import { appendFile as appendFile5, mkdir as mkdir10 } from "fs/promises";
3492
- import { dirname as dirname11 } from "path";
3677
+ import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
3678
+ import { dirname as dirname12 } from "path";
3493
3679
  async function handleLog(entry, ctx) {
3494
3680
  if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
3495
3681
  throw new Error("log: input_tokens and output_tokens (number) are required");
3496
3682
  }
3497
3683
  const written_at = (/* @__PURE__ */ new Date()).toISOString();
3498
3684
  const record = { ...entry, written_at };
3499
- await mkdir10(dirname11(ctx.paths.tokenLog), { recursive: true });
3685
+ await mkdir11(dirname12(ctx.paths.tokenLog), { recursive: true });
3500
3686
  await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
3501
3687
  return { ok: true, written_at };
3502
3688
  }
@@ -3682,7 +3868,7 @@ async function startServer(paths, options = {}) {
3682
3868
  const port = options.port ?? await findFreePort();
3683
3869
  const app = buildApp(ctx, port);
3684
3870
  const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
3685
- await writeFile8(paths.mcpPort, String(port), "utf8");
3871
+ await writeFile9(paths.mcpPort, String(port), "utf8");
3686
3872
  const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
3687
3873
  const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
3688
3874
  await ctx.activity.add(e);