@dreamtree-org/graphify 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -35
- package/dist/chunk-6JLEILYF.js +207 -0
- package/dist/chunk-6JLEILYF.js.map +1 -0
- package/dist/{chunk-DG5FECXV.js → chunk-EW23BLJC.js} +557 -27
- package/dist/chunk-EW23BLJC.js.map +1 -0
- package/dist/chunk-YT7B6DOD.js +554 -0
- package/dist/chunk-YT7B6DOD.js.map +1 -0
- package/dist/chunk-ZPB37LLQ.js +155 -0
- package/dist/chunk-ZPB37LLQ.js.map +1 -0
- package/dist/cli/index.cjs +1939 -143
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +683 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1131 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -5
- package/dist/index.d.ts +333 -5
- package/dist/index.js +50 -5
- package/dist/mcp/server.cjs +444 -56
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +65 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mysql-EJ6XOWR4.js +8 -0
- package/dist/mysql-EJ6XOWR4.js.map +1 -0
- package/package.json +2 -1
- package/src/skill/SKILL.md +45 -10
- package/dist/chunk-5ANIDX3G.js +0 -364
- package/dist/chunk-5ANIDX3G.js.map +0 -1
- package/dist/chunk-DG5FECXV.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -31,23 +31,44 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
EXTRACTOR_REGISTRY: () => EXTRACTOR_REGISTRY,
|
|
34
|
+
ExtractionCache: () => ExtractionCache,
|
|
34
35
|
ExtractionResultSchema: () => ExtractionResultSchema,
|
|
35
36
|
ExtractionValidationError: () => ExtractionValidationError,
|
|
37
|
+
affectedBy: () => affectedBy,
|
|
36
38
|
analyze: () => analyze,
|
|
39
|
+
buildContextPack: () => buildContextPack,
|
|
37
40
|
buildGraph: () => buildGraph,
|
|
41
|
+
checkRules: () => checkRules,
|
|
38
42
|
cluster: () => cluster,
|
|
39
43
|
collectFiles: () => collectFiles,
|
|
44
|
+
diffGraphs: () => diffGraphs,
|
|
40
45
|
explainNode: () => explainNode,
|
|
41
46
|
exportGraph: () => exportGraph,
|
|
42
47
|
extract: () => extract,
|
|
48
|
+
extractMysql: () => extractMysql,
|
|
49
|
+
globToRegExp: () => globToRegExp,
|
|
50
|
+
graphForDirectory: () => graphForDirectory,
|
|
51
|
+
isTestFile: () => isTestFile,
|
|
43
52
|
loadGraph: () => loadGraph,
|
|
53
|
+
loadResults: () => loadResults,
|
|
54
|
+
mergeGraphs: () => mergeGraphs,
|
|
44
55
|
queryGraph: () => queryGraph,
|
|
56
|
+
rankForTask: () => rankForTask,
|
|
57
|
+
renderContextPack: () => renderContextPack,
|
|
58
|
+
renderLessons: () => renderLessons,
|
|
45
59
|
renderReport: () => renderReport,
|
|
60
|
+
renderReview: () => renderReview,
|
|
61
|
+
resolveCrossFileReferences: () => resolveCrossFileReferences,
|
|
46
62
|
resolveNode: () => resolveNode,
|
|
63
|
+
reviewRevisions: () => reviewRevisions,
|
|
47
64
|
runPipeline: () => runPipeline,
|
|
65
|
+
saveResult: () => saveResult,
|
|
48
66
|
scoreNodes: () => scoreNodes,
|
|
49
67
|
shortestPath: () => shortestPath,
|
|
50
|
-
|
|
68
|
+
testsForChangedFiles: () => testsForChangedFiles,
|
|
69
|
+
testsForNode: () => testsForNode,
|
|
70
|
+
validateExtraction: () => validateExtraction,
|
|
71
|
+
validateRules: () => validateRules
|
|
51
72
|
});
|
|
52
73
|
module.exports = __toCommonJS(index_exports);
|
|
53
74
|
|
|
@@ -293,7 +314,7 @@ async function createParser(grammarName) {
|
|
|
293
314
|
}
|
|
294
315
|
|
|
295
316
|
// src/extractors/csharp.ts
|
|
296
|
-
async function extractCSharp(filePath) {
|
|
317
|
+
async function extractCSharp(filePath, displayPath) {
|
|
297
318
|
const parser = await createParser("c_sharp");
|
|
298
319
|
const raw = await fs2.readFile(filePath);
|
|
299
320
|
const content = raw.toString("utf-8");
|
|
@@ -302,7 +323,7 @@ async function extractCSharp(filePath) {
|
|
|
302
323
|
throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
|
|
303
324
|
}
|
|
304
325
|
const root = tree.rootNode;
|
|
305
|
-
const sourceFile = toPosix(filePath);
|
|
326
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
306
327
|
const builder = new ExtractionBuilder();
|
|
307
328
|
const fileId = sourceFile;
|
|
308
329
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -506,7 +527,7 @@ function defaultPackageQualifier(importPath) {
|
|
|
506
527
|
const segments = importPath.split("/");
|
|
507
528
|
return segments[segments.length - 1] ?? importPath;
|
|
508
529
|
}
|
|
509
|
-
async function extractGo(filePath) {
|
|
530
|
+
async function extractGo(filePath, displayPath) {
|
|
510
531
|
const parser = await createParser("go");
|
|
511
532
|
const raw = await fs3.readFile(filePath);
|
|
512
533
|
const content = raw.toString("utf-8");
|
|
@@ -515,7 +536,7 @@ async function extractGo(filePath) {
|
|
|
515
536
|
throw new Error(`extractGo: parser produced no tree for ${filePath}`);
|
|
516
537
|
}
|
|
517
538
|
const root = tree.rootNode;
|
|
518
|
-
const sourceFile = toPosix(filePath);
|
|
539
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
519
540
|
const builder = new ExtractionBuilder();
|
|
520
541
|
const fileId = sourceFile;
|
|
521
542
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -713,7 +734,7 @@ async function extractGo(filePath) {
|
|
|
713
734
|
|
|
714
735
|
// src/extractors/java.ts
|
|
715
736
|
var fs4 = __toESM(require("fs/promises"), 1);
|
|
716
|
-
async function extractJava(filePath) {
|
|
737
|
+
async function extractJava(filePath, displayPath) {
|
|
717
738
|
const parser = await createParser("java");
|
|
718
739
|
const raw = await fs4.readFile(filePath);
|
|
719
740
|
const content = raw.toString("utf-8");
|
|
@@ -722,7 +743,7 @@ async function extractJava(filePath) {
|
|
|
722
743
|
throw new Error(`extractJava: parser produced no tree for ${filePath}`);
|
|
723
744
|
}
|
|
724
745
|
const root = tree.rootNode;
|
|
725
|
-
const sourceFile = toPosix(filePath);
|
|
746
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
726
747
|
const builder = new ExtractionBuilder();
|
|
727
748
|
const fileId = sourceFile;
|
|
728
749
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -914,7 +935,7 @@ async function extractJava(filePath) {
|
|
|
914
935
|
|
|
915
936
|
// src/extractors/python.ts
|
|
916
937
|
var fs5 = __toESM(require("fs/promises"), 1);
|
|
917
|
-
async function extractPython(filePath) {
|
|
938
|
+
async function extractPython(filePath, displayPath) {
|
|
918
939
|
const parser = await createParser("python");
|
|
919
940
|
const raw = await fs5.readFile(filePath);
|
|
920
941
|
const content = raw.toString("utf-8");
|
|
@@ -923,7 +944,7 @@ async function extractPython(filePath) {
|
|
|
923
944
|
throw new Error(`extractPython: parser produced no tree for ${filePath}`);
|
|
924
945
|
}
|
|
925
946
|
const root = tree.rootNode;
|
|
926
|
-
const sourceFile = toPosix(filePath);
|
|
947
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
927
948
|
const builder = new ExtractionBuilder();
|
|
928
949
|
const fileId = sourceFile;
|
|
929
950
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1158,7 +1179,7 @@ function stringLiteralContent(node) {
|
|
|
1158
1179
|
const content = namedChildren(node).find((c) => c.type === "string_content");
|
|
1159
1180
|
return content ? content.text : "";
|
|
1160
1181
|
}
|
|
1161
|
-
async function extractRuby(filePath) {
|
|
1182
|
+
async function extractRuby(filePath, displayPath) {
|
|
1162
1183
|
const parser = await createParser("ruby");
|
|
1163
1184
|
const raw = await fs6.readFile(filePath);
|
|
1164
1185
|
const content = raw.toString("utf-8");
|
|
@@ -1167,7 +1188,7 @@ async function extractRuby(filePath) {
|
|
|
1167
1188
|
throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
|
|
1168
1189
|
}
|
|
1169
1190
|
const root = tree.rootNode;
|
|
1170
|
-
const sourceFile = toPosix(filePath);
|
|
1191
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1171
1192
|
const builder = new ExtractionBuilder();
|
|
1172
1193
|
const fileId = sourceFile;
|
|
1173
1194
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1360,7 +1381,7 @@ function convertUsePath(segments) {
|
|
|
1360
1381
|
}
|
|
1361
1382
|
return segments.join("::");
|
|
1362
1383
|
}
|
|
1363
|
-
async function extractRust(filePath) {
|
|
1384
|
+
async function extractRust(filePath, displayPath) {
|
|
1364
1385
|
const parser = await createParser("rust");
|
|
1365
1386
|
const raw = await fs7.readFile(filePath);
|
|
1366
1387
|
const content = raw.toString("utf-8");
|
|
@@ -1369,7 +1390,7 @@ async function extractRust(filePath) {
|
|
|
1369
1390
|
throw new Error(`extractRust: parser produced no tree for ${filePath}`);
|
|
1370
1391
|
}
|
|
1371
1392
|
const root = tree.rootNode;
|
|
1372
|
-
const sourceFile = toPosix(filePath);
|
|
1393
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1373
1394
|
const builder = new ExtractionBuilder();
|
|
1374
1395
|
const fileId = sourceFile;
|
|
1375
1396
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1582,10 +1603,10 @@ async function extractRust(filePath) {
|
|
|
1582
1603
|
}
|
|
1583
1604
|
}
|
|
1584
1605
|
} else if (fn.type === "scoped_identifier") {
|
|
1585
|
-
const
|
|
1606
|
+
const path11 = fn.childForFieldName("path")?.text;
|
|
1586
1607
|
const name = fn.childForFieldName("name")?.text;
|
|
1587
|
-
if (
|
|
1588
|
-
const viaImport = importIndex.get(
|
|
1608
|
+
if (path11 && name) {
|
|
1609
|
+
const viaImport = importIndex.get(path11);
|
|
1589
1610
|
if (viaImport) {
|
|
1590
1611
|
targetId = viaImport.id;
|
|
1591
1612
|
confidence = "INFERRED";
|
|
@@ -1626,7 +1647,7 @@ function firstStringArgument(call) {
|
|
|
1626
1647
|
if (!first || first.type !== "string") return null;
|
|
1627
1648
|
return stripQuotes2(first.text);
|
|
1628
1649
|
}
|
|
1629
|
-
async function extractTypeScript(filePath) {
|
|
1650
|
+
async function extractTypeScript(filePath, displayPath) {
|
|
1630
1651
|
const ext = path3.extname(filePath);
|
|
1631
1652
|
const grammar = grammarForExtension(ext);
|
|
1632
1653
|
const parser = await createParser(grammar);
|
|
@@ -1637,7 +1658,7 @@ async function extractTypeScript(filePath) {
|
|
|
1637
1658
|
throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
|
|
1638
1659
|
}
|
|
1639
1660
|
const root = tree.rootNode;
|
|
1640
|
-
const sourceFile = toPosix(filePath);
|
|
1661
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1641
1662
|
const builder = new ExtractionBuilder();
|
|
1642
1663
|
const fileId = sourceFile;
|
|
1643
1664
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1954,13 +1975,13 @@ var EXTRACTOR_REGISTRY = {
|
|
|
1954
1975
|
".cs": extractCSharp,
|
|
1955
1976
|
".rb": extractRuby
|
|
1956
1977
|
};
|
|
1957
|
-
async function extract(filePath) {
|
|
1978
|
+
async function extract(filePath, displayPath) {
|
|
1958
1979
|
const ext = path4.extname(filePath);
|
|
1959
1980
|
const extractor = EXTRACTOR_REGISTRY[ext];
|
|
1960
1981
|
if (!extractor) {
|
|
1961
1982
|
return { nodes: [], edges: [] };
|
|
1962
1983
|
}
|
|
1963
|
-
return extractor(filePath);
|
|
1984
|
+
return extractor(filePath, displayPath);
|
|
1964
1985
|
}
|
|
1965
1986
|
|
|
1966
1987
|
// src/build.ts
|
|
@@ -2065,6 +2086,107 @@ function buildGraph(extractions) {
|
|
|
2065
2086
|
return graph;
|
|
2066
2087
|
}
|
|
2067
2088
|
|
|
2089
|
+
// src/resolve.ts
|
|
2090
|
+
var CODE_EXTENSIONS2 = [
|
|
2091
|
+
".ts",
|
|
2092
|
+
".tsx",
|
|
2093
|
+
".mts",
|
|
2094
|
+
".cts",
|
|
2095
|
+
".js",
|
|
2096
|
+
".jsx",
|
|
2097
|
+
".mjs",
|
|
2098
|
+
".cjs",
|
|
2099
|
+
".py",
|
|
2100
|
+
".go",
|
|
2101
|
+
".rs",
|
|
2102
|
+
".java",
|
|
2103
|
+
".cs",
|
|
2104
|
+
".rb"
|
|
2105
|
+
];
|
|
2106
|
+
function isOpaque(id) {
|
|
2107
|
+
return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
|
|
2108
|
+
}
|
|
2109
|
+
function stripKnownExtension(p) {
|
|
2110
|
+
for (const ext of CODE_EXTENSIONS2) {
|
|
2111
|
+
if (p.endsWith(ext)) return p.slice(0, -ext.length);
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
function candidatePaths(guessed) {
|
|
2116
|
+
const stem = stripKnownExtension(guessed) ?? guessed;
|
|
2117
|
+
const candidates = [];
|
|
2118
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
|
|
2119
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
|
|
2120
|
+
if (stem !== guessed) {
|
|
2121
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
|
|
2122
|
+
}
|
|
2123
|
+
return candidates.filter((c) => c !== guessed);
|
|
2124
|
+
}
|
|
2125
|
+
function uniqueMatch(guessed, candidates, realIds) {
|
|
2126
|
+
const matches = /* @__PURE__ */ new Set();
|
|
2127
|
+
for (const candidate of candidates) {
|
|
2128
|
+
if (realIds.has(candidate)) matches.add(candidate);
|
|
2129
|
+
}
|
|
2130
|
+
if (matches.size !== 1) return null;
|
|
2131
|
+
return [...matches][0];
|
|
2132
|
+
}
|
|
2133
|
+
function resolveCrossFileReferences(extractions) {
|
|
2134
|
+
const realIds = /* @__PURE__ */ new Set();
|
|
2135
|
+
const realFiles = /* @__PURE__ */ new Set();
|
|
2136
|
+
for (const extraction of extractions) {
|
|
2137
|
+
for (const node of extraction.nodes) realIds.add(node.id);
|
|
2138
|
+
for (const node of extraction.nodes) {
|
|
2139
|
+
const sep3 = node.id.indexOf("::");
|
|
2140
|
+
if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
|
|
2141
|
+
}
|
|
2142
|
+
for (const edge of extraction.edges) {
|
|
2143
|
+
if (edge.relation === "contains") realFiles.add(edge.source);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2147
|
+
const resolveId = (id) => {
|
|
2148
|
+
if (isOpaque(id)) return null;
|
|
2149
|
+
const cached = remap.get(id);
|
|
2150
|
+
if (cached !== void 0) return cached;
|
|
2151
|
+
const sep3 = id.indexOf("::");
|
|
2152
|
+
let resolved;
|
|
2153
|
+
if (sep3 > 0) {
|
|
2154
|
+
if (realIds.has(id)) return null;
|
|
2155
|
+
const guessedPath = id.slice(0, sep3);
|
|
2156
|
+
const name = id.slice(sep3);
|
|
2157
|
+
const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
|
|
2158
|
+
resolved = uniqueMatch(id, candidates, realIds);
|
|
2159
|
+
} else {
|
|
2160
|
+
if (realFiles.has(id)) return null;
|
|
2161
|
+
resolved = uniqueMatch(id, candidatePaths(id), realFiles);
|
|
2162
|
+
}
|
|
2163
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
2164
|
+
return resolved;
|
|
2165
|
+
};
|
|
2166
|
+
let resolvedEndpoints = 0;
|
|
2167
|
+
for (const extraction of extractions) {
|
|
2168
|
+
for (const edge of extraction.edges) {
|
|
2169
|
+
const source = resolveId(edge.source);
|
|
2170
|
+
if (source !== null) {
|
|
2171
|
+
edge.source = source;
|
|
2172
|
+
resolvedEndpoints++;
|
|
2173
|
+
}
|
|
2174
|
+
const target = resolveId(edge.target);
|
|
2175
|
+
if (target !== null) {
|
|
2176
|
+
edge.target = target;
|
|
2177
|
+
resolvedEndpoints++;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
let droppedPlaceholders = 0;
|
|
2182
|
+
for (const extraction of extractions) {
|
|
2183
|
+
const before = extraction.nodes.length;
|
|
2184
|
+
extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
|
|
2185
|
+
droppedPlaceholders += before - extraction.nodes.length;
|
|
2186
|
+
}
|
|
2187
|
+
return { resolvedEndpoints, droppedPlaceholders };
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2068
2190
|
// src/cluster.ts
|
|
2069
2191
|
var import_node_crypto = require("crypto");
|
|
2070
2192
|
var import_graphology2 = __toESM(require("graphology"), 1);
|
|
@@ -2345,7 +2467,37 @@ var nodePath = __toESM(require("path"), 1);
|
|
|
2345
2467
|
var MAX_FETCH_BYTES = 50 * 1024 * 1024;
|
|
2346
2468
|
var MAX_TEXT_BYTES = 10 * 1024 * 1024;
|
|
2347
2469
|
var MAX_LABEL_LEN = 256;
|
|
2348
|
-
|
|
2470
|
+
var DEFAULT_MYSQL_PORT = 3306;
|
|
2471
|
+
function validateDsn(dsn) {
|
|
2472
|
+
let parsed;
|
|
2473
|
+
try {
|
|
2474
|
+
parsed = new URL(dsn);
|
|
2475
|
+
} catch {
|
|
2476
|
+
throw new Error("Invalid DSN \u2014 expected mysql://user:pass@host:port/database");
|
|
2477
|
+
}
|
|
2478
|
+
if (parsed.protocol !== "mysql:") {
|
|
2479
|
+
throw new Error(`DSN scheme not supported: "${parsed.protocol}" (only mysql: is supported)`);
|
|
2480
|
+
}
|
|
2481
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
|
|
2482
|
+
if (!database || database.includes("/")) {
|
|
2483
|
+
throw new Error("DSN must name exactly one database, e.g. mysql://localhost:3306/mydb");
|
|
2484
|
+
}
|
|
2485
|
+
if (!parsed.hostname) {
|
|
2486
|
+
throw new Error("DSN must include a host, e.g. mysql://localhost:3306/mydb");
|
|
2487
|
+
}
|
|
2488
|
+
const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;
|
|
2489
|
+
return {
|
|
2490
|
+
safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,
|
|
2491
|
+
connection: {
|
|
2492
|
+
host: parsed.hostname,
|
|
2493
|
+
port,
|
|
2494
|
+
user: decodeURIComponent(parsed.username) || "root",
|
|
2495
|
+
password: decodeURIComponent(parsed.password),
|
|
2496
|
+
database
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function validateGraphPath(path11, base) {
|
|
2349
2501
|
const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
|
|
2350
2502
|
let resolvedBase;
|
|
2351
2503
|
try {
|
|
@@ -2353,7 +2505,7 @@ function validateGraphPath(path8, base) {
|
|
|
2353
2505
|
} catch {
|
|
2354
2506
|
throw new Error(`Base directory does not exist: ${baseDir}`);
|
|
2355
2507
|
}
|
|
2356
|
-
const candidate = nodePath.isAbsolute(
|
|
2508
|
+
const candidate = nodePath.isAbsolute(path11) ? path11 : nodePath.join(resolvedBase, path11);
|
|
2357
2509
|
const resolvedCandidate = nodePath.resolve(candidate);
|
|
2358
2510
|
let realCandidate = resolvedCandidate;
|
|
2359
2511
|
try {
|
|
@@ -2363,7 +2515,7 @@ function validateGraphPath(path8, base) {
|
|
|
2363
2515
|
const relative4 = nodePath.relative(resolvedBase, realCandidate);
|
|
2364
2516
|
const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
|
|
2365
2517
|
if (escapes) {
|
|
2366
|
-
throw new Error(`Path escapes graphify-out/: ${
|
|
2518
|
+
throw new Error(`Path escapes graphify-out/: ${path11}`);
|
|
2367
2519
|
}
|
|
2368
2520
|
return realCandidate;
|
|
2369
2521
|
}
|
|
@@ -2506,28 +2658,82 @@ async function exportGraph(graph, options, report) {
|
|
|
2506
2658
|
}
|
|
2507
2659
|
|
|
2508
2660
|
// src/pipeline.ts
|
|
2661
|
+
var fs12 = __toESM(require("fs/promises"), 1);
|
|
2662
|
+
var path7 = __toESM(require("path"), 1);
|
|
2663
|
+
|
|
2664
|
+
// src/extractionCache.ts
|
|
2665
|
+
var import_node_crypto3 = require("crypto");
|
|
2666
|
+
var fs11 = __toESM(require("fs/promises"), 1);
|
|
2509
2667
|
var path6 = __toESM(require("path"), 1);
|
|
2668
|
+
var ExtractionCache = class {
|
|
2669
|
+
dir;
|
|
2670
|
+
constructor(outDir) {
|
|
2671
|
+
this.dir = path6.join(outDir, "cache");
|
|
2672
|
+
}
|
|
2673
|
+
key(relFile, content) {
|
|
2674
|
+
return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
2675
|
+
}
|
|
2676
|
+
async get(key) {
|
|
2677
|
+
try {
|
|
2678
|
+
const raw = await fs11.readFile(path6.join(this.dir, `${key}.json`), "utf-8");
|
|
2679
|
+
const parsed = JSON.parse(raw);
|
|
2680
|
+
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
|
|
2681
|
+
return parsed;
|
|
2682
|
+
} catch {
|
|
2683
|
+
return null;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
async put(key, extraction) {
|
|
2687
|
+
await fs11.mkdir(this.dir, { recursive: true });
|
|
2688
|
+
await fs11.writeFile(path6.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
// src/pipeline.ts
|
|
2510
2693
|
async function runPipeline(root, options = {}) {
|
|
2511
2694
|
const progress = options.onProgress ?? (() => {
|
|
2512
2695
|
});
|
|
2513
|
-
const resolvedRoot =
|
|
2696
|
+
const resolvedRoot = path7.resolve(root);
|
|
2514
2697
|
progress(`Scanning ${resolvedRoot} ...`);
|
|
2515
2698
|
const manifest = collectFiles(resolvedRoot);
|
|
2516
2699
|
progress(
|
|
2517
2700
|
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
2518
2701
|
);
|
|
2702
|
+
const outDir = options.outDir ?? path7.join(resolvedRoot, "graphify-out");
|
|
2703
|
+
const cache = new ExtractionCache(outDir);
|
|
2519
2704
|
progress("Extracting...");
|
|
2520
2705
|
const extractions = [];
|
|
2706
|
+
let cacheHits = 0;
|
|
2521
2707
|
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2522
|
-
const filePath =
|
|
2708
|
+
const filePath = path7.relative(process.cwd(), path7.join(resolvedRoot, relFile)) || relFile;
|
|
2523
2709
|
try {
|
|
2524
|
-
const
|
|
2710
|
+
const key = cache.key(relFile, await fs12.readFile(path7.join(resolvedRoot, relFile)));
|
|
2711
|
+
let extraction = options.update ? await cache.get(key) : null;
|
|
2712
|
+
if (extraction) {
|
|
2713
|
+
cacheHits++;
|
|
2714
|
+
} else {
|
|
2715
|
+
extraction = await extract(filePath, relFile);
|
|
2716
|
+
await cache.put(key, extraction);
|
|
2717
|
+
}
|
|
2525
2718
|
extractions.push(extraction);
|
|
2526
2719
|
} catch (error) {
|
|
2527
2720
|
progress(` extraction failed for ${relFile}: ${error.message}`);
|
|
2528
2721
|
throw error;
|
|
2529
2722
|
}
|
|
2530
2723
|
}
|
|
2724
|
+
if (options.update) {
|
|
2725
|
+
progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
|
|
2726
|
+
}
|
|
2727
|
+
if (options.extraExtractions && options.extraExtractions.length > 0) {
|
|
2728
|
+
extractions.push(...options.extraExtractions);
|
|
2729
|
+
}
|
|
2730
|
+
progress("Resolving cross-file references...");
|
|
2731
|
+
const resolveStats = resolveCrossFileReferences(extractions);
|
|
2732
|
+
if (resolveStats.resolvedEndpoints > 0) {
|
|
2733
|
+
progress(
|
|
2734
|
+
` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2531
2737
|
progress("Building graph...");
|
|
2532
2738
|
const graph = buildGraph(extractions);
|
|
2533
2739
|
progress(`Clustering (${options.algorithm ?? "louvain"})...`);
|
|
@@ -2536,7 +2742,6 @@ async function runPipeline(root, options = {}) {
|
|
|
2536
2742
|
const analysis = analyze(graph);
|
|
2537
2743
|
progress("Rendering report...");
|
|
2538
2744
|
const report = renderReport(graph, analysis);
|
|
2539
|
-
const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
|
|
2540
2745
|
progress(`Exporting to ${outDir} ...`);
|
|
2541
2746
|
await exportGraph(
|
|
2542
2747
|
graph,
|
|
@@ -2555,12 +2760,12 @@ async function runPipeline(root, options = {}) {
|
|
|
2555
2760
|
}
|
|
2556
2761
|
|
|
2557
2762
|
// src/graphStore.ts
|
|
2558
|
-
var
|
|
2559
|
-
var
|
|
2763
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
2764
|
+
var path8 = __toESM(require("path"), 1);
|
|
2560
2765
|
var import_graphology3 = __toESM(require("graphology"), 1);
|
|
2561
|
-
async function loadGraph(outDir =
|
|
2766
|
+
async function loadGraph(outDir = path8.join(process.cwd(), "graphify-out")) {
|
|
2562
2767
|
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2563
|
-
const raw = await
|
|
2768
|
+
const raw = await fs13.readFile(jsonPath, "utf-8");
|
|
2564
2769
|
const data = JSON.parse(raw);
|
|
2565
2770
|
return import_graphology3.default.from(data);
|
|
2566
2771
|
}
|
|
@@ -2597,8 +2802,48 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
2597
2802
|
"does",
|
|
2598
2803
|
"that's"
|
|
2599
2804
|
]);
|
|
2805
|
+
function splitCamelCase(text) {
|
|
2806
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
2807
|
+
}
|
|
2600
2808
|
function tokenize(text) {
|
|
2601
|
-
return text.toLowerCase().split(/[^a-z0-
|
|
2809
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1 && !STOPWORDS.has(token));
|
|
2810
|
+
}
|
|
2811
|
+
function subtokenize(text) {
|
|
2812
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1);
|
|
2813
|
+
}
|
|
2814
|
+
var FUZZY_MIN_TOKEN_LEN = 3;
|
|
2815
|
+
var FUZZY_THRESHOLD = 0.7;
|
|
2816
|
+
var SUBTOKEN_MATCH_SCORE = 1;
|
|
2817
|
+
var SUBSTRING_MATCH_SCORE = 0.6;
|
|
2818
|
+
var FUZZY_MATCH_WEIGHT = 0.5;
|
|
2819
|
+
function osaDistance(a, b) {
|
|
2820
|
+
let prev2 = [];
|
|
2821
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
2822
|
+
let curr = new Array(b.length + 1).fill(0);
|
|
2823
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2824
|
+
curr[0] = i;
|
|
2825
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2826
|
+
const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2827
|
+
let best = Math.min(
|
|
2828
|
+
prev[j] + 1,
|
|
2829
|
+
curr[j - 1] + 1,
|
|
2830
|
+
prev[j - 1] + substitution
|
|
2831
|
+
);
|
|
2832
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
2833
|
+
best = Math.min(best, prev2[j - 2] + 1);
|
|
2834
|
+
}
|
|
2835
|
+
curr[j] = best;
|
|
2836
|
+
}
|
|
2837
|
+
[prev2, prev, curr] = [prev, curr, new Array(b.length + 1).fill(0)];
|
|
2838
|
+
}
|
|
2839
|
+
return prev[b.length];
|
|
2840
|
+
}
|
|
2841
|
+
function fuzzySimilarity(a, b) {
|
|
2842
|
+
if (a === b) return 1;
|
|
2843
|
+
const maxLen = Math.max(a.length, b.length);
|
|
2844
|
+
if (maxLen === 0) return 1;
|
|
2845
|
+
if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;
|
|
2846
|
+
return 1 - osaDistance(a, b) / maxLen;
|
|
2602
2847
|
}
|
|
2603
2848
|
function scoreNodes(graph, query) {
|
|
2604
2849
|
const tokens = tokenize(query);
|
|
@@ -2606,9 +2851,25 @@ function scoreNodes(graph, query) {
|
|
|
2606
2851
|
graph.forEachNode((id, attributes) => {
|
|
2607
2852
|
const label = attributes.label ?? id;
|
|
2608
2853
|
const haystack = `${label} ${id}`.toLowerCase();
|
|
2854
|
+
const subtokens = new Set(subtokenize(`${label} ${id}`));
|
|
2609
2855
|
let score = 0;
|
|
2610
2856
|
for (const token of tokens) {
|
|
2611
|
-
if (
|
|
2857
|
+
if (subtokens.has(token)) {
|
|
2858
|
+
score += SUBTOKEN_MATCH_SCORE;
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
if (haystack.includes(token)) {
|
|
2862
|
+
score += SUBSTRING_MATCH_SCORE;
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (token.length >= FUZZY_MIN_TOKEN_LEN) {
|
|
2866
|
+
let best = 0;
|
|
2867
|
+
for (const subtoken of subtokens) {
|
|
2868
|
+
const similarity = fuzzySimilarity(token, subtoken);
|
|
2869
|
+
if (similarity > best) best = similarity;
|
|
2870
|
+
}
|
|
2871
|
+
if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;
|
|
2872
|
+
}
|
|
2612
2873
|
}
|
|
2613
2874
|
if (score > 0) matches.push({ id, label, score });
|
|
2614
2875
|
});
|
|
@@ -2622,22 +2883,36 @@ function resolveNode(graph, query) {
|
|
|
2622
2883
|
var DEFAULT_MAX_DEPTH = 2;
|
|
2623
2884
|
var DEFAULT_MAX_SEEDS = 5;
|
|
2624
2885
|
var DEFAULT_BUDGET = 40;
|
|
2886
|
+
var DEFAULT_TOKEN_BUDGET = 2e3;
|
|
2887
|
+
var NODES_PER_TOKEN = 1 / 10;
|
|
2888
|
+
function nodeTokenCost(graph, id) {
|
|
2889
|
+
const attrs = graph.getNodeAttributes(id);
|
|
2890
|
+
const label = attrs.label ?? id;
|
|
2891
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
2892
|
+
return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);
|
|
2893
|
+
}
|
|
2625
2894
|
function queryGraph(graph, question, options = {}) {
|
|
2626
2895
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
2627
2896
|
const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;
|
|
2628
|
-
const
|
|
2897
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
|
|
2898
|
+
const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));
|
|
2629
2899
|
const allMatches = scoreNodes(graph, question);
|
|
2630
2900
|
const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];
|
|
2631
2901
|
const visited = /* @__PURE__ */ new Map();
|
|
2632
2902
|
const frontier = seeds.map((s) => ({ id: s.id, depth: 0 }));
|
|
2633
|
-
|
|
2634
|
-
|
|
2903
|
+
let spentTokens = 0;
|
|
2904
|
+
for (const seed of seeds) {
|
|
2905
|
+
visited.set(seed.id, 0);
|
|
2906
|
+
spentTokens += nodeTokenCost(graph, seed.id);
|
|
2907
|
+
}
|
|
2908
|
+
while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {
|
|
2635
2909
|
const current = options.dfs ? frontier.pop() : frontier.shift();
|
|
2636
2910
|
if (current.depth >= maxDepth) continue;
|
|
2637
2911
|
const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));
|
|
2638
2912
|
for (const neighbor of neighbors) {
|
|
2639
|
-
if (visited.has(neighbor) || visited.size >= budget) continue;
|
|
2913
|
+
if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;
|
|
2640
2914
|
visited.set(neighbor, current.depth + 1);
|
|
2915
|
+
spentTokens += nodeTokenCost(graph, neighbor);
|
|
2641
2916
|
frontier.push({ id: neighbor, depth: current.depth + 1 });
|
|
2642
2917
|
}
|
|
2643
2918
|
}
|
|
@@ -2665,7 +2940,30 @@ function queryGraph(graph, question, options = {}) {
|
|
|
2665
2940
|
edges.sort(
|
|
2666
2941
|
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
|
|
2667
2942
|
);
|
|
2668
|
-
return { seeds, visited: visitedNodes, edges };
|
|
2943
|
+
return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);
|
|
2944
|
+
}
|
|
2945
|
+
function enforceTokenBudget(result, tokenBudget) {
|
|
2946
|
+
const cost = (value) => Math.ceil(JSON.stringify(value).length / 4);
|
|
2947
|
+
const visited = [...result.visited];
|
|
2948
|
+
let edges = [...result.edges];
|
|
2949
|
+
let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);
|
|
2950
|
+
const seedIds = new Set(result.seeds.map((s) => s.id));
|
|
2951
|
+
while (total > tokenBudget && visited.length > 0) {
|
|
2952
|
+
const last = visited[visited.length - 1];
|
|
2953
|
+
if (seedIds.has(last.id)) break;
|
|
2954
|
+
visited.pop();
|
|
2955
|
+
total -= cost(last);
|
|
2956
|
+
const remaining = [];
|
|
2957
|
+
for (const edge of edges) {
|
|
2958
|
+
if (edge.source === last.id || edge.target === last.id) {
|
|
2959
|
+
total -= cost(edge);
|
|
2960
|
+
} else {
|
|
2961
|
+
remaining.push(edge);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
edges = remaining;
|
|
2965
|
+
}
|
|
2966
|
+
return { seeds: result.seeds, visited, edges };
|
|
2669
2967
|
}
|
|
2670
2968
|
function shortestPath(graph, fromQuery, toQuery) {
|
|
2671
2969
|
const from = resolveNode(graph, fromQuery);
|
|
@@ -2691,25 +2989,25 @@ function shortestPath(graph, fromQuery, toQuery) {
|
|
|
2691
2989
|
if (!visited.has(to.id)) {
|
|
2692
2990
|
return { from, to, found: false, path: [], edges: [] };
|
|
2693
2991
|
}
|
|
2694
|
-
const
|
|
2992
|
+
const path11 = [to.id];
|
|
2695
2993
|
let cursor = to.id;
|
|
2696
2994
|
while (cursor !== from.id) {
|
|
2697
2995
|
const prev = predecessor.get(cursor);
|
|
2698
2996
|
if (!prev) break;
|
|
2699
|
-
|
|
2997
|
+
path11.unshift(prev);
|
|
2700
2998
|
cursor = prev;
|
|
2701
2999
|
}
|
|
2702
3000
|
const edges = [];
|
|
2703
|
-
for (let i = 0; i <
|
|
2704
|
-
const a =
|
|
2705
|
-
const b =
|
|
3001
|
+
for (let i = 0; i < path11.length - 1; i++) {
|
|
3002
|
+
const a = path11[i];
|
|
3003
|
+
const b = path11[i + 1];
|
|
2706
3004
|
const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
|
|
2707
3005
|
if (key) {
|
|
2708
3006
|
const attrs = graph.getEdgeAttributes(key);
|
|
2709
3007
|
edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
|
|
2710
3008
|
}
|
|
2711
3009
|
}
|
|
2712
|
-
return { from, to, found: true, path:
|
|
3010
|
+
return { from, to, found: true, path: path11, edges };
|
|
2713
3011
|
}
|
|
2714
3012
|
function explainNode(graph, query) {
|
|
2715
3013
|
const match = resolveNode(graph, query);
|
|
@@ -2735,25 +3033,813 @@ function explainNode(graph, query) {
|
|
|
2735
3033
|
incoming
|
|
2736
3034
|
};
|
|
2737
3035
|
}
|
|
3036
|
+
|
|
3037
|
+
// src/impact.ts
|
|
3038
|
+
var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
3039
|
+
"calls",
|
|
3040
|
+
"imports",
|
|
3041
|
+
"imports_from",
|
|
3042
|
+
"inherits",
|
|
3043
|
+
"implements",
|
|
3044
|
+
"mixes_in",
|
|
3045
|
+
"embeds",
|
|
3046
|
+
"references",
|
|
3047
|
+
"re_exports",
|
|
3048
|
+
"method",
|
|
3049
|
+
"contains"
|
|
3050
|
+
]);
|
|
3051
|
+
var DEFAULT_IMPACT_DEPTH = 3;
|
|
3052
|
+
var DEFAULT_IMPACT_LIMIT = 200;
|
|
3053
|
+
var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
3054
|
+
function affectedBy(graph, nodeQuery, options = {}) {
|
|
3055
|
+
const target = resolveNode(graph, nodeQuery);
|
|
3056
|
+
if (!target) return null;
|
|
3057
|
+
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
3058
|
+
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
3059
|
+
const affected = [];
|
|
3060
|
+
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
3061
|
+
let frontier = [target.id];
|
|
3062
|
+
let truncated = false;
|
|
3063
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
3064
|
+
const nextFrontier = /* @__PURE__ */ new Map();
|
|
3065
|
+
for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {
|
|
3066
|
+
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
3067
|
+
if (visited.has(source)) return;
|
|
3068
|
+
const relation = edgeAttrs.relation;
|
|
3069
|
+
if (!DEPENDENCY_RELATIONS.has(relation)) return;
|
|
3070
|
+
const confidence = edgeAttrs.confidence;
|
|
3071
|
+
const existing = nextFrontier.get(source);
|
|
3072
|
+
if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
|
|
3073
|
+
const attrs = graph.getNodeAttributes(source);
|
|
3074
|
+
nextFrontier.set(source, {
|
|
3075
|
+
id: source,
|
|
3076
|
+
label: attrs.label ?? source,
|
|
3077
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
3078
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
3079
|
+
depth,
|
|
3080
|
+
via: { relation, confidence, dependsOn: current }
|
|
3081
|
+
});
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3085
|
+
for (const node of roundNodes) {
|
|
3086
|
+
if (affected.length >= limit) {
|
|
3087
|
+
truncated = true;
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
visited.add(node.id);
|
|
3091
|
+
affected.push(node);
|
|
3092
|
+
}
|
|
3093
|
+
frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);
|
|
3094
|
+
}
|
|
3095
|
+
const byConfidence = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
|
|
3096
|
+
for (const node of affected) byConfidence[node.via.confidence] += 1;
|
|
3097
|
+
return { target, affected, byConfidence, maxDepth, truncated };
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/merge.ts
|
|
3101
|
+
var import_graphology4 = __toESM(require("graphology"), 1);
|
|
3102
|
+
var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
3103
|
+
function strongerConfidence2(a, b) {
|
|
3104
|
+
return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
|
|
3105
|
+
}
|
|
3106
|
+
function isShared(id) {
|
|
3107
|
+
return id.startsWith("external:") || id.startsWith("module:");
|
|
3108
|
+
}
|
|
3109
|
+
function namespaced(project, id) {
|
|
3110
|
+
return isShared(id) ? id : `${project}/${id}`;
|
|
3111
|
+
}
|
|
3112
|
+
function mergeGraphs(entries) {
|
|
3113
|
+
const names = entries.map((e) => e.name);
|
|
3114
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
3115
|
+
if (duplicate !== void 0) {
|
|
3116
|
+
throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
|
|
3117
|
+
}
|
|
3118
|
+
const merged = new import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
|
|
3119
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
3120
|
+
for (const { name, graph } of sorted) {
|
|
3121
|
+
const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
3122
|
+
for (const id of nodeIds) {
|
|
3123
|
+
const attrs = graph.getNodeAttributes(id);
|
|
3124
|
+
const newId = namespaced(name, id);
|
|
3125
|
+
if (merged.hasNode(newId)) {
|
|
3126
|
+
merged.setNodeAttribute(newId, "project", "(shared)");
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
3130
|
+
merged.addNode(newId, {
|
|
3131
|
+
label: attrs.label ?? id,
|
|
3132
|
+
sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
|
|
3133
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
3134
|
+
project: name
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
|
|
3138
|
+
for (const edgeKey of edgeKeys) {
|
|
3139
|
+
const attrs = graph.getEdgeAttributes(edgeKey);
|
|
3140
|
+
const source = namespaced(name, graph.source(edgeKey));
|
|
3141
|
+
const target = namespaced(name, graph.target(edgeKey));
|
|
3142
|
+
const relation = String(attrs.relation);
|
|
3143
|
+
const confidence = attrs.confidence;
|
|
3144
|
+
const key = `${source}|${relation}|${target}`;
|
|
3145
|
+
if (merged.hasEdge(key)) {
|
|
3146
|
+
const existing = merged.getEdgeAttribute(key, "confidence");
|
|
3147
|
+
merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
merged.addEdgeWithKey(key, source, target, { relation, confidence });
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
return merged;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/extractors/mysql.ts
|
|
3157
|
+
var schemaId = (schema) => `db:${schema}`;
|
|
3158
|
+
var tableId = (schema, table) => `db:${schema}::${table}`;
|
|
3159
|
+
var columnId = (schema, table, column) => `db:${schema}::${table}.${column}`;
|
|
3160
|
+
async function defaultQueryFn(dsn) {
|
|
3161
|
+
const { connection } = validateDsn(dsn);
|
|
3162
|
+
const mysql = await import("mysql2/promise");
|
|
3163
|
+
const conn = await mysql.createConnection({
|
|
3164
|
+
host: connection.host,
|
|
3165
|
+
port: connection.port,
|
|
3166
|
+
user: connection.user,
|
|
3167
|
+
password: connection.password,
|
|
3168
|
+
database: connection.database
|
|
3169
|
+
});
|
|
3170
|
+
return {
|
|
3171
|
+
query: async (sql, params) => {
|
|
3172
|
+
const [rows] = await conn.execute(sql, [...params]);
|
|
3173
|
+
return rows;
|
|
3174
|
+
},
|
|
3175
|
+
close: () => conn.end()
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
async function extractMysql(dsn, queryFn) {
|
|
3179
|
+
const { safeDisplay, connection } = validateDsn(dsn);
|
|
3180
|
+
const schema = connection.database;
|
|
3181
|
+
let query = queryFn;
|
|
3182
|
+
let close;
|
|
3183
|
+
if (!query) {
|
|
3184
|
+
const live = await defaultQueryFn(dsn);
|
|
3185
|
+
query = live.query;
|
|
3186
|
+
close = live.close;
|
|
3187
|
+
}
|
|
3188
|
+
try {
|
|
3189
|
+
const nodes = [];
|
|
3190
|
+
const edges = [];
|
|
3191
|
+
nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });
|
|
3192
|
+
const tableRows = await query(
|
|
3193
|
+
"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
3194
|
+
[schema]
|
|
3195
|
+
);
|
|
3196
|
+
const tables = tableRows.map((row) => ({
|
|
3197
|
+
name: String(row.TABLE_NAME),
|
|
3198
|
+
isView: String(row.TABLE_TYPE).toUpperCase() === "VIEW"
|
|
3199
|
+
}));
|
|
3200
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
3201
|
+
for (const table of tables) {
|
|
3202
|
+
nodes.push({
|
|
3203
|
+
id: tableId(schema, table.name),
|
|
3204
|
+
label: `${table.isView ? "view" : "table"} ${table.name}`,
|
|
3205
|
+
sourceFile: safeDisplay,
|
|
3206
|
+
sourceLocation: table.name
|
|
3207
|
+
});
|
|
3208
|
+
edges.push({
|
|
3209
|
+
source: schemaId(schema),
|
|
3210
|
+
target: tableId(schema, table.name),
|
|
3211
|
+
relation: "contains",
|
|
3212
|
+
confidence: "EXTRACTED"
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
const columnRows = await query(
|
|
3216
|
+
"SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION",
|
|
3217
|
+
[schema]
|
|
3218
|
+
);
|
|
3219
|
+
for (const row of columnRows) {
|
|
3220
|
+
const table = String(row.TABLE_NAME);
|
|
3221
|
+
const column = String(row.COLUMN_NAME);
|
|
3222
|
+
if (!tableNames.has(table)) continue;
|
|
3223
|
+
nodes.push({
|
|
3224
|
+
id: columnId(schema, table, column),
|
|
3225
|
+
label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,
|
|
3226
|
+
sourceFile: safeDisplay,
|
|
3227
|
+
sourceLocation: `${table}.${column}`
|
|
3228
|
+
});
|
|
3229
|
+
edges.push({
|
|
3230
|
+
source: tableId(schema, table),
|
|
3231
|
+
target: columnId(schema, table, column),
|
|
3232
|
+
relation: "contains",
|
|
3233
|
+
confidence: "EXTRACTED"
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
const fkRows = await query(
|
|
3237
|
+
"SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL ORDER BY TABLE_NAME, COLUMN_NAME",
|
|
3238
|
+
[schema]
|
|
3239
|
+
);
|
|
3240
|
+
for (const row of fkRows) {
|
|
3241
|
+
const table = String(row.TABLE_NAME);
|
|
3242
|
+
const column = String(row.COLUMN_NAME);
|
|
3243
|
+
const referenced = String(row.REFERENCED_TABLE_NAME);
|
|
3244
|
+
if (!tableNames.has(table) || !tableNames.has(referenced)) continue;
|
|
3245
|
+
edges.push({
|
|
3246
|
+
source: columnId(schema, table, column),
|
|
3247
|
+
target: tableId(schema, referenced),
|
|
3248
|
+
relation: "references",
|
|
3249
|
+
confidence: "EXTRACTED"
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
const viewNames = tables.filter((t) => t.isView).map((t) => t.name);
|
|
3253
|
+
if (viewNames.length > 0) {
|
|
3254
|
+
await addViewEdges(query, schema, viewNames, tableNames, edges);
|
|
3255
|
+
}
|
|
3256
|
+
return { nodes, edges };
|
|
3257
|
+
} finally {
|
|
3258
|
+
await close?.();
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
async function addViewEdges(query, schema, viewNames, tableNames, edges) {
|
|
3262
|
+
try {
|
|
3263
|
+
const usageRows = await query(
|
|
3264
|
+
"SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME",
|
|
3265
|
+
[schema, schema]
|
|
3266
|
+
);
|
|
3267
|
+
for (const row of usageRows) {
|
|
3268
|
+
const view = String(row.VIEW_NAME);
|
|
3269
|
+
const table = String(row.TABLE_NAME);
|
|
3270
|
+
if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;
|
|
3271
|
+
edges.push({
|
|
3272
|
+
source: tableId(schema, view),
|
|
3273
|
+
target: tableId(schema, table),
|
|
3274
|
+
relation: "references",
|
|
3275
|
+
confidence: "EXTRACTED"
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
return;
|
|
3279
|
+
} catch {
|
|
3280
|
+
}
|
|
3281
|
+
const definitionRows = await query(
|
|
3282
|
+
"SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
3283
|
+
[schema]
|
|
3284
|
+
);
|
|
3285
|
+
for (const row of definitionRows) {
|
|
3286
|
+
const view = String(row.TABLE_NAME);
|
|
3287
|
+
if (!viewNames.includes(view)) continue;
|
|
3288
|
+
const definition = String(row.VIEW_DEFINITION ?? "").toLowerCase();
|
|
3289
|
+
for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {
|
|
3290
|
+
if (table === view) continue;
|
|
3291
|
+
if (new RegExp(`\\b${table.toLowerCase()}\\b`).test(definition)) {
|
|
3292
|
+
edges.push({
|
|
3293
|
+
source: tableId(schema, view),
|
|
3294
|
+
target: tableId(schema, table),
|
|
3295
|
+
relation: "references",
|
|
3296
|
+
confidence: "INFERRED"
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
// src/memory.ts
|
|
3304
|
+
var import_node_crypto4 = require("crypto");
|
|
3305
|
+
var fs14 = __toESM(require("fs/promises"), 1);
|
|
3306
|
+
var path9 = __toESM(require("path"), 1);
|
|
3307
|
+
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
3308
|
+
var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
|
|
3309
|
+
function validateResult(value) {
|
|
3310
|
+
if (!value.question) throw new Error("save-result requires --question");
|
|
3311
|
+
if (!value.answer) throw new Error("save-result requires --answer");
|
|
3312
|
+
if (!OUTCOMES.has(value.outcome)) {
|
|
3313
|
+
throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
|
|
3314
|
+
}
|
|
3315
|
+
if (!TYPES.has(value.type)) {
|
|
3316
|
+
throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
|
|
3317
|
+
}
|
|
3318
|
+
if (value.outcome === "corrected" && !value.correction) {
|
|
3319
|
+
throw new Error("--outcome corrected requires --correction with what the right answer was");
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
3323
|
+
validateResult(entry);
|
|
3324
|
+
await fs14.mkdir(memoryDir, { recursive: true });
|
|
3325
|
+
const saved = { ...entry, savedAt: now.toISOString() };
|
|
3326
|
+
const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
3327
|
+
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
3328
|
+
const filePath = path9.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
3329
|
+
await fs14.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
3330
|
+
return filePath;
|
|
3331
|
+
}
|
|
3332
|
+
async function loadResults(memoryDir) {
|
|
3333
|
+
let files;
|
|
3334
|
+
try {
|
|
3335
|
+
files = (await fs14.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
|
|
3336
|
+
} catch {
|
|
3337
|
+
return [];
|
|
3338
|
+
}
|
|
3339
|
+
const results = [];
|
|
3340
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
3341
|
+
try {
|
|
3342
|
+
const parsed = JSON.parse(await fs14.readFile(path9.join(memoryDir, file), "utf-8"));
|
|
3343
|
+
if (parsed.question && parsed.savedAt) results.push(parsed);
|
|
3344
|
+
} catch {
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
return results;
|
|
3348
|
+
}
|
|
3349
|
+
function renderLessons(results, options = {}) {
|
|
3350
|
+
const halfLife = options.halfLifeDays ?? 30;
|
|
3351
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3352
|
+
const minCorroboration = options.minCorroboration ?? 2;
|
|
3353
|
+
const weightOf = (savedAt) => {
|
|
3354
|
+
const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
|
|
3355
|
+
return Math.pow(0.5, ageDays / halfLife);
|
|
3356
|
+
};
|
|
3357
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3358
|
+
const corrections = [];
|
|
3359
|
+
for (const result of results) {
|
|
3360
|
+
const weight = weightOf(result.savedAt);
|
|
3361
|
+
for (const node of result.nodes ?? []) {
|
|
3362
|
+
const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
|
|
3363
|
+
if (result.outcome === "useful") {
|
|
3364
|
+
signal.useful += weight;
|
|
3365
|
+
signal.usefulCount++;
|
|
3366
|
+
} else if (result.outcome === "dead_end") {
|
|
3367
|
+
signal.deadEnd += weight;
|
|
3368
|
+
signal.deadEndCount++;
|
|
3369
|
+
}
|
|
3370
|
+
signals.set(node, signal);
|
|
3371
|
+
}
|
|
3372
|
+
if (result.outcome === "corrected" && result.correction) {
|
|
3373
|
+
corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
const byScore = (kind) => [...signals.entries()].filter(([, s]) => kind === "useful" ? s.usefulCount >= minCorroboration : s.deadEndCount > 0).sort((a, b) => b[1][kind] - a[1][kind] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
3377
|
+
const lines = [
|
|
3378
|
+
"# Graph lessons",
|
|
3379
|
+
"",
|
|
3380
|
+
`_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
|
|
3381
|
+
"",
|
|
3382
|
+
"## Nodes that keep answering",
|
|
3383
|
+
""
|
|
3384
|
+
];
|
|
3385
|
+
const useful = byScore("useful");
|
|
3386
|
+
if (useful.length === 0) {
|
|
3387
|
+
lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
|
|
3388
|
+
} else {
|
|
3389
|
+
for (const [node, s] of useful) {
|
|
3390
|
+
lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
lines.push("", "## Dead ends", "");
|
|
3394
|
+
const deadEnds = byScore("deadEnd");
|
|
3395
|
+
if (deadEnds.length === 0) {
|
|
3396
|
+
lines.push("(none recorded)");
|
|
3397
|
+
} else {
|
|
3398
|
+
for (const [node, s] of deadEnds) {
|
|
3399
|
+
lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
lines.push("", "## Corrections", "");
|
|
3403
|
+
if (corrections.length === 0) {
|
|
3404
|
+
lines.push("(none recorded)");
|
|
3405
|
+
} else {
|
|
3406
|
+
corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
3407
|
+
for (const c of corrections.slice(0, 20)) {
|
|
3408
|
+
lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
|
|
3409
|
+
lines.push(` Right answer: ${c.correction}`);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
lines.push("");
|
|
3413
|
+
return lines.join("\n");
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
// src/context.ts
|
|
3417
|
+
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
3418
|
+
var DEFAULT_MAX_SEEDS2 = 8;
|
|
3419
|
+
var DEFAULT_MAX_SNIPPET_LINES = 60;
|
|
3420
|
+
var NEIGHBOR_DECAY = 0.4;
|
|
3421
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
3422
|
+
function lineNumberOf(sourceLocation) {
|
|
3423
|
+
const match = /^L(\d+)$/.exec(sourceLocation);
|
|
3424
|
+
return match ? Number(match[1]) : null;
|
|
3425
|
+
}
|
|
3426
|
+
function isReadableFile(sourceFile) {
|
|
3427
|
+
return sourceFile !== "" && !sourceFile.startsWith("<") && !sourceFile.includes("://");
|
|
3428
|
+
}
|
|
3429
|
+
function rankForTask(graph, task, maxSeeds = DEFAULT_MAX_SEEDS2) {
|
|
3430
|
+
const seeds = scoreNodes(graph, task).slice(0, maxSeeds);
|
|
3431
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
3432
|
+
for (const seed of seeds) {
|
|
3433
|
+
ranked.set(seed.id, { id: seed.id, score: seed.score, reason: "matches the task" });
|
|
3434
|
+
}
|
|
3435
|
+
for (const seed of seeds) {
|
|
3436
|
+
graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {
|
|
3437
|
+
const neighbor = source === seed.id ? target : source;
|
|
3438
|
+
if (ranked.has(neighbor)) return;
|
|
3439
|
+
const relation = String(attrs.relation);
|
|
3440
|
+
const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;
|
|
3441
|
+
ranked.set(neighbor, {
|
|
3442
|
+
id: neighbor,
|
|
3443
|
+
score: seed.score * NEIGHBOR_DECAY,
|
|
3444
|
+
reason: `${direction} ${seed.label}`
|
|
3445
|
+
});
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
3449
|
+
}
|
|
3450
|
+
function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
|
|
3451
|
+
const nextStart = entityStartsInFile.find((l) => l > startLine);
|
|
3452
|
+
const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
|
|
3453
|
+
return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
|
|
3454
|
+
}
|
|
3455
|
+
async function buildContextPack(graph, task, readFile13, options = {}) {
|
|
3456
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
3457
|
+
const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
|
|
3458
|
+
const ranked = rankForTask(graph, task, options.maxSeeds);
|
|
3459
|
+
const entityStarts = /* @__PURE__ */ new Map();
|
|
3460
|
+
graph.forEachNode((_id, attrs) => {
|
|
3461
|
+
const file = attrs.sourceFile;
|
|
3462
|
+
const line = lineNumberOf(attrs.sourceLocation ?? "");
|
|
3463
|
+
if (file && line !== null) {
|
|
3464
|
+
const starts = entityStarts.get(file) ?? [];
|
|
3465
|
+
starts.push(line);
|
|
3466
|
+
entityStarts.set(file, starts);
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);
|
|
3470
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
3471
|
+
const readLines = async (file) => {
|
|
3472
|
+
const cached = fileCache.get(file);
|
|
3473
|
+
if (cached !== void 0) return cached;
|
|
3474
|
+
let lines;
|
|
3475
|
+
try {
|
|
3476
|
+
lines = (await readFile13(file)).split("\n");
|
|
3477
|
+
} catch {
|
|
3478
|
+
lines = null;
|
|
3479
|
+
}
|
|
3480
|
+
fileCache.set(file, lines);
|
|
3481
|
+
return lines;
|
|
3482
|
+
};
|
|
3483
|
+
const snippets = [];
|
|
3484
|
+
const overflow = [];
|
|
3485
|
+
const coveredRanges = /* @__PURE__ */ new Map();
|
|
3486
|
+
let tokens = 0;
|
|
3487
|
+
for (const node of ranked) {
|
|
3488
|
+
const attrs = graph.getNodeAttributes(node.id);
|
|
3489
|
+
const file = attrs.sourceFile ?? "";
|
|
3490
|
+
const startLine = lineNumberOf(attrs.sourceLocation ?? "");
|
|
3491
|
+
if (!isReadableFile(file) || startLine === null) continue;
|
|
3492
|
+
const lines = await readLines(file);
|
|
3493
|
+
if (lines === null) continue;
|
|
3494
|
+
const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);
|
|
3495
|
+
const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);
|
|
3496
|
+
if (covered) continue;
|
|
3497
|
+
const code = lines.slice(start - 1, end).join("\n");
|
|
3498
|
+
const cost = tokensOf(code) + 15;
|
|
3499
|
+
if (tokens + cost > tokenBudget) {
|
|
3500
|
+
overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
tokens += cost;
|
|
3504
|
+
snippets.push({
|
|
3505
|
+
nodeId: node.id,
|
|
3506
|
+
label: attrs.label ?? node.id,
|
|
3507
|
+
file,
|
|
3508
|
+
startLine: start,
|
|
3509
|
+
endLine: end,
|
|
3510
|
+
code,
|
|
3511
|
+
reason: node.reason,
|
|
3512
|
+
score: node.score
|
|
3513
|
+
});
|
|
3514
|
+
const ranges = coveredRanges.get(file) ?? [];
|
|
3515
|
+
ranges.push({ start, end });
|
|
3516
|
+
coveredRanges.set(file, ranges);
|
|
3517
|
+
}
|
|
3518
|
+
return { task, snippets, tokens, tokenBudget, overflow };
|
|
3519
|
+
}
|
|
3520
|
+
function renderContextPack(pack) {
|
|
3521
|
+
const lines = [
|
|
3522
|
+
`# Context pack: ${pack.task}`,
|
|
3523
|
+
"",
|
|
3524
|
+
`_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,
|
|
3525
|
+
""
|
|
3526
|
+
];
|
|
3527
|
+
if (pack.snippets.length === 0) {
|
|
3528
|
+
lines.push("No matching code found \u2014 try different keywords.");
|
|
3529
|
+
return lines.join("\n");
|
|
3530
|
+
}
|
|
3531
|
+
for (const snippet of pack.snippets) {
|
|
3532
|
+
lines.push(`## ${snippet.label} \u2014 \`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\``);
|
|
3533
|
+
lines.push(`_${snippet.reason}_`);
|
|
3534
|
+
lines.push("```");
|
|
3535
|
+
lines.push(snippet.code);
|
|
3536
|
+
lines.push("```");
|
|
3537
|
+
lines.push("");
|
|
3538
|
+
}
|
|
3539
|
+
if (pack.overflow.length > 0) {
|
|
3540
|
+
lines.push(`## Didn't fit the budget (${pack.overflow.length})`);
|
|
3541
|
+
for (const item of pack.overflow.slice(0, 15)) {
|
|
3542
|
+
lines.push(`- ${item.nodeId} (${item.file})`);
|
|
3543
|
+
}
|
|
3544
|
+
lines.push("");
|
|
3545
|
+
}
|
|
3546
|
+
return lines.join("\n");
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/testmap.ts
|
|
3550
|
+
var TEST_FILE_PATTERNS = [
|
|
3551
|
+
/(^|\/)tests?\//,
|
|
3552
|
+
// tests/ or test/ directory
|
|
3553
|
+
/(^|\/)__tests__\//,
|
|
3554
|
+
/(^|\/)spec\//,
|
|
3555
|
+
/\.test\.[cm]?[jt]sx?$/,
|
|
3556
|
+
/\.spec\.[cm]?[jt]sx?$/,
|
|
3557
|
+
/(^|\/)test_[^/]+\.py$/,
|
|
3558
|
+
/_test\.py$/,
|
|
3559
|
+
/_test\.go$/,
|
|
3560
|
+
/Tests?\.(java|cs)$/,
|
|
3561
|
+
/_spec\.rb$/,
|
|
3562
|
+
/_test\.rb$/
|
|
3563
|
+
];
|
|
3564
|
+
function isTestFile(path11) {
|
|
3565
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path11));
|
|
3566
|
+
}
|
|
3567
|
+
var TEST_REACH_DEPTH = 4;
|
|
3568
|
+
var TEST_REACH_LIMIT = 2e3;
|
|
3569
|
+
function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
3570
|
+
const best = /* @__PURE__ */ new Map();
|
|
3571
|
+
let affectedNodeCount = 0;
|
|
3572
|
+
for (const id of targetIds) {
|
|
3573
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
3574
|
+
if (!impact) continue;
|
|
3575
|
+
affectedNodeCount += impact.affected.length;
|
|
3576
|
+
for (const node of impact.affected) {
|
|
3577
|
+
if (!isTestFile(node.sourceFile)) continue;
|
|
3578
|
+
const existing = best.get(node.sourceFile);
|
|
3579
|
+
if (!existing || node.depth < existing.depth) {
|
|
3580
|
+
best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
const testFiles = [...best.entries()].map(([file, info]) => ({ file, depth: info.depth, via: info.via })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
3585
|
+
return { target: targetLabel, testFiles, affectedNodeCount };
|
|
3586
|
+
}
|
|
3587
|
+
function testsForNode(graph, nodeQuery) {
|
|
3588
|
+
const match = resolveNode(graph, nodeQuery);
|
|
3589
|
+
if (!match) return null;
|
|
3590
|
+
return selectionFromImpact(graph, [match.id], match.id);
|
|
3591
|
+
}
|
|
3592
|
+
function testsForChangedFiles(graph, changedFiles) {
|
|
3593
|
+
const fileIds = [];
|
|
3594
|
+
const selfSelected = /* @__PURE__ */ new Map();
|
|
3595
|
+
for (const file of changedFiles) {
|
|
3596
|
+
if (isTestFile(file)) {
|
|
3597
|
+
selfSelected.set(file, { depth: 0, via: "(changed directly)" });
|
|
3598
|
+
continue;
|
|
3599
|
+
}
|
|
3600
|
+
if (graph.hasNode(file)) fileIds.push(file);
|
|
3601
|
+
}
|
|
3602
|
+
const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
|
|
3603
|
+
for (const [file, info] of selfSelected) {
|
|
3604
|
+
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
3605
|
+
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
3609
|
+
return selection;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
// src/diff.ts
|
|
3613
|
+
var import_node_child_process = require("child_process");
|
|
3614
|
+
var fs15 = __toESM(require("fs/promises"), 1);
|
|
3615
|
+
var os = __toESM(require("os"), 1);
|
|
3616
|
+
var path10 = __toESM(require("path"), 1);
|
|
3617
|
+
var import_node_util = require("util");
|
|
3618
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
3619
|
+
async function graphForDirectory(root) {
|
|
3620
|
+
const manifest = collectFiles(root);
|
|
3621
|
+
const extractions = [];
|
|
3622
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
3623
|
+
extractions.push(await extract(path10.join(root, relFile), relFile));
|
|
3624
|
+
}
|
|
3625
|
+
resolveCrossFileReferences(extractions);
|
|
3626
|
+
return buildGraph(extractions);
|
|
3627
|
+
}
|
|
3628
|
+
async function checkoutRev(repoRoot, rev) {
|
|
3629
|
+
const dest = await fs15.mkdtemp(path10.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
|
|
3630
|
+
const { stdout } = await execFileAsync(
|
|
3631
|
+
"git",
|
|
3632
|
+
["-C", repoRoot, "archive", "--format=tar", rev],
|
|
3633
|
+
{ encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
|
|
3634
|
+
);
|
|
3635
|
+
await new Promise((resolve5, reject) => {
|
|
3636
|
+
const tar = (0, import_node_child_process.execFile)("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve5());
|
|
3637
|
+
tar.stdin?.end(stdout);
|
|
3638
|
+
});
|
|
3639
|
+
return dest;
|
|
3640
|
+
}
|
|
3641
|
+
function edgeSignature(graph, id) {
|
|
3642
|
+
const signature = /* @__PURE__ */ new Set();
|
|
3643
|
+
graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
|
|
3644
|
+
graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
|
|
3645
|
+
return signature;
|
|
3646
|
+
}
|
|
3647
|
+
function describeSymbol(graph, id) {
|
|
3648
|
+
const attrs = graph.getNodeAttributes(id);
|
|
3649
|
+
const impact = affectedBy(graph, id, { maxDepth: 3 });
|
|
3650
|
+
const tests = testsForNode(graph, id);
|
|
3651
|
+
return {
|
|
3652
|
+
id,
|
|
3653
|
+
label: attrs.label ?? id,
|
|
3654
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
3655
|
+
blastRadius: impact?.affected.length ?? 0,
|
|
3656
|
+
tests: tests?.testFiles.map((t) => t.file) ?? []
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function entityIds(graph) {
|
|
3660
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3661
|
+
graph.forEachNode((id) => {
|
|
3662
|
+
if (id.includes("::")) ids.add(id);
|
|
3663
|
+
});
|
|
3664
|
+
return ids;
|
|
3665
|
+
}
|
|
3666
|
+
function diffGraphs(baseGraph, headGraph, base, head) {
|
|
3667
|
+
const baseIds = entityIds(baseGraph);
|
|
3668
|
+
const headIds = entityIds(headGraph);
|
|
3669
|
+
const added = [];
|
|
3670
|
+
const removed = [];
|
|
3671
|
+
const rewired = [];
|
|
3672
|
+
for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
|
|
3673
|
+
if (!baseIds.has(id)) {
|
|
3674
|
+
added.push(describeSymbol(headGraph, id));
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
const before = edgeSignature(baseGraph, id);
|
|
3678
|
+
const after = edgeSignature(headGraph, id);
|
|
3679
|
+
const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
|
|
3680
|
+
const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
|
|
3681
|
+
if (gained.length > 0 || lost.length > 0) {
|
|
3682
|
+
rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
|
|
3686
|
+
if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
|
|
3687
|
+
}
|
|
3688
|
+
return { base, head, added, removed, rewired };
|
|
3689
|
+
}
|
|
3690
|
+
async function reviewRevisions(repoRoot, base, head = "HEAD") {
|
|
3691
|
+
const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
|
|
3692
|
+
try {
|
|
3693
|
+
const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
|
|
3694
|
+
return diffGraphs(baseGraph, headGraph, base, head);
|
|
3695
|
+
} finally {
|
|
3696
|
+
await Promise.all([
|
|
3697
|
+
fs15.rm(baseDir, { recursive: true, force: true }),
|
|
3698
|
+
fs15.rm(headDir, { recursive: true, force: true })
|
|
3699
|
+
]);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
function renderReview(diff) {
|
|
3703
|
+
const lines = [
|
|
3704
|
+
`# Structural review: ${diff.base}..${diff.head}`,
|
|
3705
|
+
"",
|
|
3706
|
+
`${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
|
|
3707
|
+
""
|
|
3708
|
+
];
|
|
3709
|
+
const section = (title, symbols) => {
|
|
3710
|
+
lines.push(`## ${title} (${symbols.length})`, "");
|
|
3711
|
+
if (symbols.length === 0) {
|
|
3712
|
+
lines.push("(none)", "");
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
for (const s of symbols) {
|
|
3716
|
+
const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
|
|
3717
|
+
lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
|
|
3718
|
+
const r = s;
|
|
3719
|
+
if (r.gainedEdges) {
|
|
3720
|
+
for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
|
|
3721
|
+
for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
lines.push("");
|
|
3725
|
+
};
|
|
3726
|
+
section("Removed \u2014 check every caller", diff.removed);
|
|
3727
|
+
section("Rewired \u2014 dependencies changed", diff.rewired);
|
|
3728
|
+
section("Added", diff.added);
|
|
3729
|
+
const allTests = /* @__PURE__ */ new Set();
|
|
3730
|
+
for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
|
|
3731
|
+
for (const t of s.tests) allTests.add(t);
|
|
3732
|
+
}
|
|
3733
|
+
lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
|
|
3734
|
+
for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
|
|
3735
|
+
lines.push("");
|
|
3736
|
+
return lines.join("\n");
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
// src/check.ts
|
|
3740
|
+
var DEPENDENCY_RELATIONS2 = /* @__PURE__ */ new Set([
|
|
3741
|
+
"calls",
|
|
3742
|
+
"imports",
|
|
3743
|
+
"imports_from",
|
|
3744
|
+
"inherits",
|
|
3745
|
+
"implements",
|
|
3746
|
+
"mixes_in",
|
|
3747
|
+
"embeds",
|
|
3748
|
+
"re_exports"
|
|
3749
|
+
]);
|
|
3750
|
+
function globToRegExp(glob) {
|
|
3751
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3752
|
+
const pattern = escaped.replace(
|
|
3753
|
+
/\*\*|\*|\?/g,
|
|
3754
|
+
(token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
|
|
3755
|
+
);
|
|
3756
|
+
return new RegExp(`^${pattern}$`);
|
|
3757
|
+
}
|
|
3758
|
+
function validateRules(value) {
|
|
3759
|
+
const config = value;
|
|
3760
|
+
if (!config || !Array.isArray(config.rules)) {
|
|
3761
|
+
throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
|
|
3762
|
+
}
|
|
3763
|
+
for (const rule of config.rules) {
|
|
3764
|
+
if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
|
|
3765
|
+
throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return config;
|
|
3769
|
+
}
|
|
3770
|
+
function checkRules(graph, config) {
|
|
3771
|
+
const compiled = config.rules.map((rule) => ({
|
|
3772
|
+
rule,
|
|
3773
|
+
from: globToRegExp(rule.from),
|
|
3774
|
+
disallow: rule.disallow.map(globToRegExp)
|
|
3775
|
+
}));
|
|
3776
|
+
const violations = [];
|
|
3777
|
+
graph.forEachEdge((_edge, attrs, source, target) => {
|
|
3778
|
+
const relation = String(attrs.relation);
|
|
3779
|
+
if (!DEPENDENCY_RELATIONS2.has(relation)) return;
|
|
3780
|
+
const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
|
|
3781
|
+
const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
|
|
3782
|
+
if (!fromFile || !toFile || fromFile === toFile) return;
|
|
3783
|
+
for (const { rule, from, disallow } of compiled) {
|
|
3784
|
+
if (!from.test(fromFile)) continue;
|
|
3785
|
+
if (disallow.some((d) => d.test(toFile))) {
|
|
3786
|
+
violations.push({
|
|
3787
|
+
rule: rule.name,
|
|
3788
|
+
reason: rule.reason,
|
|
3789
|
+
fromFile,
|
|
3790
|
+
fromNode: source,
|
|
3791
|
+
toFile,
|
|
3792
|
+
toNode: target,
|
|
3793
|
+
relation
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
violations.sort(
|
|
3799
|
+
(a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
|
|
3800
|
+
);
|
|
3801
|
+
return violations;
|
|
3802
|
+
}
|
|
2738
3803
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2739
3804
|
0 && (module.exports = {
|
|
2740
3805
|
EXTRACTOR_REGISTRY,
|
|
3806
|
+
ExtractionCache,
|
|
2741
3807
|
ExtractionResultSchema,
|
|
2742
3808
|
ExtractionValidationError,
|
|
3809
|
+
affectedBy,
|
|
2743
3810
|
analyze,
|
|
3811
|
+
buildContextPack,
|
|
2744
3812
|
buildGraph,
|
|
3813
|
+
checkRules,
|
|
2745
3814
|
cluster,
|
|
2746
3815
|
collectFiles,
|
|
3816
|
+
diffGraphs,
|
|
2747
3817
|
explainNode,
|
|
2748
3818
|
exportGraph,
|
|
2749
3819
|
extract,
|
|
3820
|
+
extractMysql,
|
|
3821
|
+
globToRegExp,
|
|
3822
|
+
graphForDirectory,
|
|
3823
|
+
isTestFile,
|
|
2750
3824
|
loadGraph,
|
|
3825
|
+
loadResults,
|
|
3826
|
+
mergeGraphs,
|
|
2751
3827
|
queryGraph,
|
|
3828
|
+
rankForTask,
|
|
3829
|
+
renderContextPack,
|
|
3830
|
+
renderLessons,
|
|
2752
3831
|
renderReport,
|
|
3832
|
+
renderReview,
|
|
3833
|
+
resolveCrossFileReferences,
|
|
2753
3834
|
resolveNode,
|
|
3835
|
+
reviewRevisions,
|
|
2754
3836
|
runPipeline,
|
|
3837
|
+
saveResult,
|
|
2755
3838
|
scoreNodes,
|
|
2756
3839
|
shortestPath,
|
|
2757
|
-
|
|
3840
|
+
testsForChangedFiles,
|
|
3841
|
+
testsForNode,
|
|
3842
|
+
validateExtraction,
|
|
3843
|
+
validateRules
|
|
2758
3844
|
});
|
|
2759
3845
|
//# sourceMappingURL=index.cjs.map
|