@jefuriiij/synthra 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.
- package/CHANGELOG.md +47 -0
- package/dist/cli/index.js +527 -251
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +128 -11
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +309 -95
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1572
|
-
import { dirname as
|
|
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
|
|
1750
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
1575
1751
|
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
1576
|
-
await
|
|
1752
|
+
await writeFile2(path, text + "\n", "utf8");
|
|
1577
1753
|
}
|
|
1578
1754
|
async function readJson(path) {
|
|
1579
|
-
const text = await
|
|
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
|
|
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
|
|
1635
|
-
import { basename, dirname as
|
|
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
|
|
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(
|
|
1804
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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 (${
|
|
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
|
|
1965
|
-
import { dirname as
|
|
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
|
|
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
|
|
2049
|
-
await
|
|
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
|
|
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
|
|
2249
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2076
2250
|
await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
|
|
2077
2251
|
} catch {
|
|
2078
2252
|
}
|
|
@@ -2136,10 +2310,11 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
2136
2310
|
};
|
|
2137
2311
|
|
|
2138
2312
|
// src/server/mcp.ts
|
|
2139
|
-
import { appendFile as appendFile3, mkdir as
|
|
2140
|
-
import { dirname as
|
|
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
|
|
2317
|
+
var KW_BASE_WEIGHT = 2;
|
|
2143
2318
|
var USAGE_BOOST_CAP_DEFAULT = 4;
|
|
2144
2319
|
function usageBoostCap() {
|
|
2145
2320
|
const env = Number(process.env.SYN_LEARN_BOOST_CAP);
|
|
@@ -2229,14 +2404,41 @@ function scoreFiles(inputs) {
|
|
|
2229
2404
|
const importsFrom = indexImportEdges(inputs.graph);
|
|
2230
2405
|
const seeds = new Set(inputs.sessionKnownPaths ?? []);
|
|
2231
2406
|
for (const p of inputs.recentlyEditedPaths ?? []) seeds.add(p);
|
|
2407
|
+
const corpusSize = inputs.candidates.length;
|
|
2408
|
+
const queryDf = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const f of inputs.candidates) {
|
|
2410
|
+
for (const kw of f.keywords) {
|
|
2411
|
+
if (qTokens.has(kw)) queryDf.set(kw, (queryDf.get(kw) ?? 0) + 1);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
const idf = (token) => {
|
|
2415
|
+
const n = queryDf.get(token) ?? 0;
|
|
2416
|
+
if (n <= 0) return 0;
|
|
2417
|
+
return Math.log(1 + (corpusSize - n + 0.5) / (n + 0.5));
|
|
2418
|
+
};
|
|
2419
|
+
let idfSum = 0;
|
|
2420
|
+
let idfCount = 0;
|
|
2421
|
+
for (const t of qTokens) {
|
|
2422
|
+
const v = idf(t);
|
|
2423
|
+
if (v > 0) {
|
|
2424
|
+
idfSum += v;
|
|
2425
|
+
idfCount += 1;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
const refIdf = idfCount > 0 ? idfSum / idfCount : 1;
|
|
2232
2429
|
const scored = [];
|
|
2233
2430
|
for (const file of inputs.candidates) {
|
|
2234
2431
|
const reasons = [];
|
|
2235
2432
|
let score2 = 0;
|
|
2236
2433
|
let kwHits = 0;
|
|
2237
|
-
|
|
2434
|
+
let kwScore = 0;
|
|
2435
|
+
for (const kw of file.keywords) {
|
|
2436
|
+
if (!qTokens.has(kw)) continue;
|
|
2437
|
+
kwHits += 1;
|
|
2438
|
+
kwScore += KW_BASE_WEIGHT * (idf(kw) / refIdf);
|
|
2439
|
+
}
|
|
2238
2440
|
if (kwHits) {
|
|
2239
|
-
score2 +=
|
|
2441
|
+
score2 += kwScore;
|
|
2240
2442
|
reasons.push(`kw=${kwHits}`);
|
|
2241
2443
|
}
|
|
2242
2444
|
const symbols = symbolsByFile.get(file.path) ?? [];
|
|
@@ -2362,14 +2564,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
2362
2564
|
|
|
2363
2565
|
// src/memory/branches.ts
|
|
2364
2566
|
import { execFile as execFile2 } from "child_process";
|
|
2365
|
-
import { readFile as
|
|
2567
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2366
2568
|
import { join as join6 } from "path";
|
|
2367
2569
|
import { promisify as promisify2 } from "util";
|
|
2368
2570
|
var execFileAsync2 = promisify2(execFile2);
|
|
2369
2571
|
async function currentBranch(projectRoot) {
|
|
2370
2572
|
try {
|
|
2371
2573
|
const headPath = join6(projectRoot, ".git", "HEAD");
|
|
2372
|
-
const head = await
|
|
2574
|
+
const head = await readFile10(headPath, "utf8");
|
|
2373
2575
|
const trimmed = head.trim();
|
|
2374
2576
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2375
2577
|
if (match?.[1]) return match[1];
|
|
@@ -2419,8 +2621,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
2419
2621
|
}
|
|
2420
2622
|
|
|
2421
2623
|
// src/memory/context-md.ts
|
|
2422
|
-
import { mkdir as
|
|
2423
|
-
import { dirname as
|
|
2624
|
+
import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
2625
|
+
import { dirname as dirname7 } from "path";
|
|
2424
2626
|
var MAX_BULLETS = 3;
|
|
2425
2627
|
function deriveContextMd(entries, branch) {
|
|
2426
2628
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -2463,17 +2665,17 @@ function formatContextMd(ctx) {
|
|
|
2463
2665
|
return lines.join("\n");
|
|
2464
2666
|
}
|
|
2465
2667
|
async function writeContextMd(path, ctx) {
|
|
2466
|
-
await
|
|
2467
|
-
await
|
|
2668
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
2669
|
+
await writeFile6(path, formatContextMd(ctx), "utf8");
|
|
2468
2670
|
}
|
|
2469
2671
|
|
|
2470
2672
|
// src/memory/context-store.ts
|
|
2471
|
-
import { mkdir as
|
|
2472
|
-
import { dirname as
|
|
2673
|
+
import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
|
|
2674
|
+
import { dirname as dirname8 } from "path";
|
|
2473
2675
|
var SCHEMA_VERSION2 = 1;
|
|
2474
2676
|
async function readEntries(path) {
|
|
2475
2677
|
try {
|
|
2476
|
-
const raw = await
|
|
2678
|
+
const raw = await readFile12(path, "utf8");
|
|
2477
2679
|
const parsed = JSON.parse(raw);
|
|
2478
2680
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
2479
2681
|
} catch {
|
|
@@ -2481,9 +2683,9 @@ async function readEntries(path) {
|
|
|
2481
2683
|
}
|
|
2482
2684
|
}
|
|
2483
2685
|
async function writeEntries(path, entries) {
|
|
2484
|
-
await
|
|
2686
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
2485
2687
|
const store = { schema_version: SCHEMA_VERSION2, entries };
|
|
2486
|
-
await
|
|
2688
|
+
await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
2487
2689
|
}
|
|
2488
2690
|
async function appendEntry(path, entry) {
|
|
2489
2691
|
const entries = await readEntries(path);
|
|
@@ -2860,7 +3062,7 @@ var TOOLS = [
|
|
|
2860
3062
|
},
|
|
2861
3063
|
{
|
|
2862
3064
|
name: "blast_radius",
|
|
2863
|
-
description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports
|
|
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.",
|
|
2864
3066
|
inputSchema: {
|
|
2865
3067
|
type: "object",
|
|
2866
3068
|
properties: {
|
|
@@ -2872,7 +3074,7 @@ var TOOLS = [
|
|
|
2872
3074
|
},
|
|
2873
3075
|
{
|
|
2874
3076
|
name: "dead_code",
|
|
2875
|
-
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 (
|
|
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.",
|
|
2876
3078
|
inputSchema: {
|
|
2877
3079
|
type: "object",
|
|
2878
3080
|
properties: {
|
|
@@ -2918,12 +3120,24 @@ function blastRadius(args, ctx) {
|
|
|
2918
3120
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
2919
3121
|
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
2920
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
|
+
}
|
|
2921
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
|
+
};
|
|
2922
3133
|
for (const e of ctx.graph.edges) {
|
|
2923
|
-
if (e.kind
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
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
|
+
}
|
|
2927
3141
|
}
|
|
2928
3142
|
const visited = /* @__PURE__ */ new Set([root.id]);
|
|
2929
3143
|
const hits = [];
|
|
@@ -3001,7 +3215,7 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
3001
3215
|
}
|
|
3002
3216
|
lines.push("");
|
|
3003
3217
|
lines.push(
|
|
3004
|
-
`
|
|
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.`
|
|
3005
3219
|
);
|
|
3006
3220
|
return textContent(lines.join("\n"));
|
|
3007
3221
|
}
|
|
@@ -3153,7 +3367,7 @@ async function contextRecall(args, ctx) {
|
|
|
3153
3367
|
}
|
|
3154
3368
|
async function logToolCall(ctx, tool) {
|
|
3155
3369
|
try {
|
|
3156
|
-
await
|
|
3370
|
+
await mkdir8(dirname9(ctx.paths.toolLog), { recursive: true });
|
|
3157
3371
|
await appendFile3(
|
|
3158
3372
|
ctx.paths.toolLog,
|
|
3159
3373
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
@@ -3271,12 +3485,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
3271
3485
|
}
|
|
3272
3486
|
|
|
3273
3487
|
// src/memory/session.ts
|
|
3274
|
-
import { mkdir as
|
|
3275
|
-
import { dirname as
|
|
3488
|
+
import { mkdir as mkdir9, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
|
|
3489
|
+
import { dirname as dirname10 } from "path";
|
|
3276
3490
|
var SESSION_SCHEMA_VERSION = 1;
|
|
3277
3491
|
async function readSession(path) {
|
|
3278
3492
|
try {
|
|
3279
|
-
const raw = await
|
|
3493
|
+
const raw = await readFile13(path, "utf8");
|
|
3280
3494
|
const parsed = JSON.parse(raw);
|
|
3281
3495
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
3282
3496
|
return parsed;
|
|
@@ -3285,8 +3499,8 @@ async function readSession(path) {
|
|
|
3285
3499
|
}
|
|
3286
3500
|
}
|
|
3287
3501
|
async function writeSession(path, state) {
|
|
3288
|
-
await
|
|
3289
|
-
await
|
|
3502
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
3503
|
+
await writeFile8(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3290
3504
|
}
|
|
3291
3505
|
|
|
3292
3506
|
// src/server/routes/context-update.ts
|
|
@@ -3331,8 +3545,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
3331
3545
|
}
|
|
3332
3546
|
|
|
3333
3547
|
// src/server/routes/gate.ts
|
|
3334
|
-
import { appendFile as appendFile4, mkdir as
|
|
3335
|
-
import { dirname as
|
|
3548
|
+
import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
|
|
3549
|
+
import { dirname as dirname11 } from "path";
|
|
3336
3550
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
3337
3551
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
3338
3552
|
function extractQuery(toolName, input) {
|
|
@@ -3388,7 +3602,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
3388
3602
|
}
|
|
3389
3603
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
3390
3604
|
try {
|
|
3391
|
-
await
|
|
3605
|
+
await mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
|
|
3392
3606
|
const entry = {
|
|
3393
3607
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3394
3608
|
tool: toolName,
|
|
@@ -3460,15 +3674,15 @@ async function handleGate(req, ctx) {
|
|
|
3460
3674
|
}
|
|
3461
3675
|
|
|
3462
3676
|
// src/server/routes/log.ts
|
|
3463
|
-
import { appendFile as appendFile5, mkdir as
|
|
3464
|
-
import { dirname as
|
|
3677
|
+
import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
|
|
3678
|
+
import { dirname as dirname12 } from "path";
|
|
3465
3679
|
async function handleLog(entry, ctx) {
|
|
3466
3680
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
3467
3681
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
3468
3682
|
}
|
|
3469
3683
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3470
3684
|
const record = { ...entry, written_at };
|
|
3471
|
-
await
|
|
3685
|
+
await mkdir11(dirname12(ctx.paths.tokenLog), { recursive: true });
|
|
3472
3686
|
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
3473
3687
|
return { ok: true, written_at };
|
|
3474
3688
|
}
|
|
@@ -3654,7 +3868,7 @@ async function startServer(paths, options = {}) {
|
|
|
3654
3868
|
const port = options.port ?? await findFreePort();
|
|
3655
3869
|
const app = buildApp(ctx, port);
|
|
3656
3870
|
const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
3657
|
-
await
|
|
3871
|
+
await writeFile9(paths.mcpPort, String(port), "utf8");
|
|
3658
3872
|
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
3659
3873
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
3660
3874
|
await ctx.activity.add(e);
|