@dreamtree-org/graphify 1.0.0 → 1.1.2
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 +110 -35
- package/dist/chunk-6JLEILYF.js +207 -0
- package/dist/chunk-6JLEILYF.js.map +1 -0
- package/dist/{chunk-DG5FECXV.js → chunk-PXVI2L45.js} +614 -39
- package/dist/chunk-PXVI2L45.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 +1996 -147
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +693 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1192 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +343 -5
- package/dist/index.d.ts +343 -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 path12 = fn.childForFieldName("path")?.text;
|
|
1586
1607
|
const name = fn.childForFieldName("name")?.text;
|
|
1587
|
-
if (
|
|
1588
|
-
const viaImport = importIndex.get(
|
|
1608
|
+
if (path12 && name) {
|
|
1609
|
+
const viaImport = importIndex.get(path12);
|
|
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,150 @@ function buildGraph(extractions) {
|
|
|
2065
2086
|
return graph;
|
|
2066
2087
|
}
|
|
2067
2088
|
|
|
2089
|
+
// src/resolve.ts
|
|
2090
|
+
var fs9 = __toESM(require("fs/promises"), 1);
|
|
2091
|
+
var path5 = __toESM(require("path"), 1);
|
|
2092
|
+
var CODE_EXTENSIONS2 = [
|
|
2093
|
+
".ts",
|
|
2094
|
+
".tsx",
|
|
2095
|
+
".mts",
|
|
2096
|
+
".cts",
|
|
2097
|
+
".js",
|
|
2098
|
+
".jsx",
|
|
2099
|
+
".mjs",
|
|
2100
|
+
".cjs",
|
|
2101
|
+
".py",
|
|
2102
|
+
".go",
|
|
2103
|
+
".rs",
|
|
2104
|
+
".java",
|
|
2105
|
+
".cs",
|
|
2106
|
+
".rb"
|
|
2107
|
+
];
|
|
2108
|
+
function isOpaque(id) {
|
|
2109
|
+
return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
|
|
2110
|
+
}
|
|
2111
|
+
var SELF_IMPORT_STEMS = (subpath) => subpath === "" ? ["index", "src/index", "lib/index", "source/index", "src/main"] : [
|
|
2112
|
+
subpath,
|
|
2113
|
+
`src/${subpath}`,
|
|
2114
|
+
`lib/${subpath}`,
|
|
2115
|
+
`source/${subpath}`,
|
|
2116
|
+
`${subpath}/index`,
|
|
2117
|
+
`src/${subpath}/index`,
|
|
2118
|
+
`lib/${subpath}/index`
|
|
2119
|
+
];
|
|
2120
|
+
function selfImportCandidates(spec, selfNames) {
|
|
2121
|
+
const selfName = selfNames.find((n) => spec === n || spec.startsWith(`${n}/`));
|
|
2122
|
+
if (selfName === void 0) return null;
|
|
2123
|
+
const subpath = spec === selfName ? "" : spec.slice(selfName.length + 1);
|
|
2124
|
+
return SELF_IMPORT_STEMS(subpath).flatMap((stem) => CODE_EXTENSIONS2.map((ext) => `${stem}${ext}`));
|
|
2125
|
+
}
|
|
2126
|
+
async function readSelfNames(root) {
|
|
2127
|
+
try {
|
|
2128
|
+
const pkg = JSON.parse(await fs9.readFile(path5.join(root, "package.json"), "utf-8"));
|
|
2129
|
+
return typeof pkg.name === "string" && pkg.name.length > 0 ? [pkg.name] : [];
|
|
2130
|
+
} catch {
|
|
2131
|
+
return [];
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
function stripKnownExtension(p) {
|
|
2135
|
+
for (const ext of CODE_EXTENSIONS2) {
|
|
2136
|
+
if (p.endsWith(ext)) return p.slice(0, -ext.length);
|
|
2137
|
+
}
|
|
2138
|
+
return null;
|
|
2139
|
+
}
|
|
2140
|
+
function candidatePaths(guessed) {
|
|
2141
|
+
const stem = stripKnownExtension(guessed) ?? guessed;
|
|
2142
|
+
const candidates = [];
|
|
2143
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
|
|
2144
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
|
|
2145
|
+
if (stem !== guessed) {
|
|
2146
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
|
|
2147
|
+
}
|
|
2148
|
+
return candidates.filter((c) => c !== guessed);
|
|
2149
|
+
}
|
|
2150
|
+
function uniqueMatch(guessed, candidates, realIds) {
|
|
2151
|
+
const matches = /* @__PURE__ */ new Set();
|
|
2152
|
+
for (const candidate of candidates) {
|
|
2153
|
+
if (realIds.has(candidate)) matches.add(candidate);
|
|
2154
|
+
}
|
|
2155
|
+
if (matches.size !== 1) return null;
|
|
2156
|
+
return [...matches][0];
|
|
2157
|
+
}
|
|
2158
|
+
function resolveCrossFileReferences(extractions, options = {}) {
|
|
2159
|
+
const selfNames = options.selfNames ?? [];
|
|
2160
|
+
const realIds = /* @__PURE__ */ new Set();
|
|
2161
|
+
const realFiles = /* @__PURE__ */ new Set();
|
|
2162
|
+
for (const extraction of extractions) {
|
|
2163
|
+
for (const node of extraction.nodes) realIds.add(node.id);
|
|
2164
|
+
const first = extraction.nodes[0];
|
|
2165
|
+
if (first && first.id === first.sourceFile && !isOpaque(first.id)) {
|
|
2166
|
+
realFiles.add(first.id);
|
|
2167
|
+
}
|
|
2168
|
+
for (const node of extraction.nodes) {
|
|
2169
|
+
const sep3 = node.id.indexOf("::");
|
|
2170
|
+
if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
|
|
2171
|
+
}
|
|
2172
|
+
for (const edge of extraction.edges) {
|
|
2173
|
+
if (edge.relation === "contains") realFiles.add(edge.source);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2177
|
+
const resolveId = (id) => {
|
|
2178
|
+
if (id.startsWith("module:") && selfNames.length > 0) {
|
|
2179
|
+
const cachedSelf = remap.get(id);
|
|
2180
|
+
if (cachedSelf !== void 0) return cachedSelf;
|
|
2181
|
+
const candidates = selfImportCandidates(id.slice("module:".length), selfNames);
|
|
2182
|
+
if (candidates !== null) {
|
|
2183
|
+
const resolved2 = uniqueMatch(id, candidates, realFiles);
|
|
2184
|
+
if (resolved2 !== null) {
|
|
2185
|
+
remap.set(id, resolved2);
|
|
2186
|
+
return resolved2;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
if (isOpaque(id)) return null;
|
|
2192
|
+
const cached = remap.get(id);
|
|
2193
|
+
if (cached !== void 0) return cached;
|
|
2194
|
+
const sep3 = id.indexOf("::");
|
|
2195
|
+
let resolved;
|
|
2196
|
+
if (sep3 > 0) {
|
|
2197
|
+
if (realIds.has(id)) return null;
|
|
2198
|
+
const guessedPath = id.slice(0, sep3);
|
|
2199
|
+
const name = id.slice(sep3);
|
|
2200
|
+
const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
|
|
2201
|
+
resolved = uniqueMatch(id, candidates, realIds);
|
|
2202
|
+
} else {
|
|
2203
|
+
if (realFiles.has(id)) return null;
|
|
2204
|
+
resolved = uniqueMatch(id, candidatePaths(id), realFiles);
|
|
2205
|
+
}
|
|
2206
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
2207
|
+
return resolved;
|
|
2208
|
+
};
|
|
2209
|
+
let resolvedEndpoints = 0;
|
|
2210
|
+
for (const extraction of extractions) {
|
|
2211
|
+
for (const edge of extraction.edges) {
|
|
2212
|
+
const source = resolveId(edge.source);
|
|
2213
|
+
if (source !== null) {
|
|
2214
|
+
edge.source = source;
|
|
2215
|
+
resolvedEndpoints++;
|
|
2216
|
+
}
|
|
2217
|
+
const target = resolveId(edge.target);
|
|
2218
|
+
if (target !== null) {
|
|
2219
|
+
edge.target = target;
|
|
2220
|
+
resolvedEndpoints++;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
let droppedPlaceholders = 0;
|
|
2225
|
+
for (const extraction of extractions) {
|
|
2226
|
+
const before = extraction.nodes.length;
|
|
2227
|
+
extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
|
|
2228
|
+
droppedPlaceholders += before - extraction.nodes.length;
|
|
2229
|
+
}
|
|
2230
|
+
return { resolvedEndpoints, droppedPlaceholders };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2068
2233
|
// src/cluster.ts
|
|
2069
2234
|
var import_node_crypto = require("crypto");
|
|
2070
2235
|
var import_graphology2 = __toESM(require("graphology"), 1);
|
|
@@ -2331,39 +2496,69 @@ function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
|
|
|
2331
2496
|
|
|
2332
2497
|
// src/export.ts
|
|
2333
2498
|
var import_node_module2 = require("module");
|
|
2334
|
-
var
|
|
2335
|
-
var
|
|
2499
|
+
var fs11 = __toESM(require("fs/promises"), 1);
|
|
2500
|
+
var path6 = __toESM(require("path"), 1);
|
|
2336
2501
|
|
|
2337
2502
|
// src/security.ts
|
|
2338
2503
|
var import_node_crypto2 = require("crypto");
|
|
2339
2504
|
var dns = __toESM(require("dns"), 1);
|
|
2340
2505
|
var http = __toESM(require("http"), 1);
|
|
2341
2506
|
var https = __toESM(require("https"), 1);
|
|
2342
|
-
var
|
|
2507
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
2343
2508
|
var net = __toESM(require("net"), 1);
|
|
2344
2509
|
var nodePath = __toESM(require("path"), 1);
|
|
2345
2510
|
var MAX_FETCH_BYTES = 50 * 1024 * 1024;
|
|
2346
2511
|
var MAX_TEXT_BYTES = 10 * 1024 * 1024;
|
|
2347
2512
|
var MAX_LABEL_LEN = 256;
|
|
2348
|
-
|
|
2513
|
+
var DEFAULT_MYSQL_PORT = 3306;
|
|
2514
|
+
function validateDsn(dsn) {
|
|
2515
|
+
let parsed;
|
|
2516
|
+
try {
|
|
2517
|
+
parsed = new URL(dsn);
|
|
2518
|
+
} catch {
|
|
2519
|
+
throw new Error("Invalid DSN \u2014 expected mysql://user:pass@host:port/database");
|
|
2520
|
+
}
|
|
2521
|
+
if (parsed.protocol !== "mysql:") {
|
|
2522
|
+
throw new Error(`DSN scheme not supported: "${parsed.protocol}" (only mysql: is supported)`);
|
|
2523
|
+
}
|
|
2524
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
|
|
2525
|
+
if (!database || database.includes("/")) {
|
|
2526
|
+
throw new Error("DSN must name exactly one database, e.g. mysql://localhost:3306/mydb");
|
|
2527
|
+
}
|
|
2528
|
+
if (!parsed.hostname) {
|
|
2529
|
+
throw new Error("DSN must include a host, e.g. mysql://localhost:3306/mydb");
|
|
2530
|
+
}
|
|
2531
|
+
const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;
|
|
2532
|
+
return {
|
|
2533
|
+
safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,
|
|
2534
|
+
connection: {
|
|
2535
|
+
host: parsed.hostname,
|
|
2536
|
+
port,
|
|
2537
|
+
user: decodeURIComponent(parsed.username) || "root",
|
|
2538
|
+
password: decodeURIComponent(parsed.password),
|
|
2539
|
+
database
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
function validateGraphPath(path12, base) {
|
|
2349
2544
|
const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
|
|
2350
2545
|
let resolvedBase;
|
|
2351
2546
|
try {
|
|
2352
|
-
resolvedBase =
|
|
2547
|
+
resolvedBase = fs10.realpathSync(baseDir);
|
|
2353
2548
|
} catch {
|
|
2354
2549
|
throw new Error(`Base directory does not exist: ${baseDir}`);
|
|
2355
2550
|
}
|
|
2356
|
-
const candidate = nodePath.isAbsolute(
|
|
2551
|
+
const candidate = nodePath.isAbsolute(path12) ? path12 : nodePath.join(resolvedBase, path12);
|
|
2357
2552
|
const resolvedCandidate = nodePath.resolve(candidate);
|
|
2358
2553
|
let realCandidate = resolvedCandidate;
|
|
2359
2554
|
try {
|
|
2360
|
-
realCandidate =
|
|
2555
|
+
realCandidate = fs10.realpathSync(resolvedCandidate);
|
|
2361
2556
|
} catch {
|
|
2362
2557
|
}
|
|
2363
2558
|
const relative4 = nodePath.relative(resolvedBase, realCandidate);
|
|
2364
2559
|
const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
|
|
2365
2560
|
if (escapes) {
|
|
2366
|
-
throw new Error(`Path escapes graphify-out/: ${
|
|
2561
|
+
throw new Error(`Path escapes graphify-out/: ${path12}`);
|
|
2367
2562
|
}
|
|
2368
2563
|
return realCandidate;
|
|
2369
2564
|
}
|
|
@@ -2380,10 +2575,10 @@ function escapeHtml(text) {
|
|
|
2380
2575
|
var require3 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
2381
2576
|
function resolveVisNetworkAssets() {
|
|
2382
2577
|
const packageJsonPath = require3.resolve("vis-network/package.json");
|
|
2383
|
-
const root =
|
|
2578
|
+
const root = path6.dirname(packageJsonPath);
|
|
2384
2579
|
return {
|
|
2385
|
-
jsPath:
|
|
2386
|
-
cssPath:
|
|
2580
|
+
jsPath: path6.join(root, "standalone", "umd", "vis-network.min.js"),
|
|
2581
|
+
cssPath: path6.join(root, "styles", "vis-network.min.css")
|
|
2387
2582
|
};
|
|
2388
2583
|
}
|
|
2389
2584
|
var COMMUNITY_PALETTE = [
|
|
@@ -2442,8 +2637,8 @@ community: ${communityLabel}` : ""}`,
|
|
|
2442
2637
|
async function renderHtml(graph) {
|
|
2443
2638
|
const { jsPath, cssPath } = resolveVisNetworkAssets();
|
|
2444
2639
|
const [visJs, visCss] = await Promise.all([
|
|
2445
|
-
|
|
2446
|
-
|
|
2640
|
+
fs11.readFile(jsPath, "utf-8"),
|
|
2641
|
+
fs11.readFile(cssPath, "utf-8")
|
|
2447
2642
|
]);
|
|
2448
2643
|
const { nodes, edges } = buildVisNodesAndEdges(graph);
|
|
2449
2644
|
const dataJson = safeInlineJson({ nodes, edges });
|
|
@@ -2483,17 +2678,17 @@ ${visJs}
|
|
|
2483
2678
|
`;
|
|
2484
2679
|
}
|
|
2485
2680
|
async function exportGraph(graph, options, report) {
|
|
2486
|
-
const outDir =
|
|
2487
|
-
await
|
|
2681
|
+
const outDir = path6.resolve(options.outDir);
|
|
2682
|
+
await fs11.mkdir(outDir, { recursive: true });
|
|
2488
2683
|
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2489
|
-
await
|
|
2684
|
+
await fs11.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
|
|
2490
2685
|
if (options.html !== false) {
|
|
2491
2686
|
const htmlPath = validateGraphPath("graph.html", outDir);
|
|
2492
|
-
await
|
|
2687
|
+
await fs11.writeFile(htmlPath, await renderHtml(graph), "utf-8");
|
|
2493
2688
|
}
|
|
2494
2689
|
if (report !== void 0) {
|
|
2495
2690
|
const reportPath = validateGraphPath("GRAPH_REPORT.md", outDir);
|
|
2496
|
-
await
|
|
2691
|
+
await fs11.writeFile(reportPath, report, "utf-8");
|
|
2497
2692
|
}
|
|
2498
2693
|
const notImplemented = [];
|
|
2499
2694
|
if (options.svg) notImplemented.push("--svg");
|
|
@@ -2506,28 +2701,84 @@ async function exportGraph(graph, options, report) {
|
|
|
2506
2701
|
}
|
|
2507
2702
|
|
|
2508
2703
|
// src/pipeline.ts
|
|
2509
|
-
var
|
|
2704
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
2705
|
+
var path8 = __toESM(require("path"), 1);
|
|
2706
|
+
|
|
2707
|
+
// src/extractionCache.ts
|
|
2708
|
+
var import_node_crypto3 = require("crypto");
|
|
2709
|
+
var fs12 = __toESM(require("fs/promises"), 1);
|
|
2710
|
+
var path7 = __toESM(require("path"), 1);
|
|
2711
|
+
var ExtractionCache = class {
|
|
2712
|
+
dir;
|
|
2713
|
+
constructor(outDir) {
|
|
2714
|
+
this.dir = path7.join(outDir, "cache");
|
|
2715
|
+
}
|
|
2716
|
+
key(relFile, content) {
|
|
2717
|
+
return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
2718
|
+
}
|
|
2719
|
+
async get(key) {
|
|
2720
|
+
try {
|
|
2721
|
+
const raw = await fs12.readFile(path7.join(this.dir, `${key}.json`), "utf-8");
|
|
2722
|
+
const parsed = JSON.parse(raw);
|
|
2723
|
+
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
|
|
2724
|
+
return parsed;
|
|
2725
|
+
} catch {
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
async put(key, extraction) {
|
|
2730
|
+
await fs12.mkdir(this.dir, { recursive: true });
|
|
2731
|
+
await fs12.writeFile(path7.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
|
|
2735
|
+
// src/pipeline.ts
|
|
2510
2736
|
async function runPipeline(root, options = {}) {
|
|
2511
2737
|
const progress = options.onProgress ?? (() => {
|
|
2512
2738
|
});
|
|
2513
|
-
const resolvedRoot =
|
|
2739
|
+
const resolvedRoot = path8.resolve(root);
|
|
2514
2740
|
progress(`Scanning ${resolvedRoot} ...`);
|
|
2515
2741
|
const manifest = collectFiles(resolvedRoot);
|
|
2516
2742
|
progress(
|
|
2517
2743
|
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
2518
2744
|
);
|
|
2745
|
+
const outDir = options.outDir ?? path8.join(resolvedRoot, "graphify-out");
|
|
2746
|
+
const cache = new ExtractionCache(outDir);
|
|
2519
2747
|
progress("Extracting...");
|
|
2520
2748
|
const extractions = [];
|
|
2749
|
+
let cacheHits = 0;
|
|
2521
2750
|
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2522
|
-
const filePath =
|
|
2751
|
+
const filePath = path8.relative(process.cwd(), path8.join(resolvedRoot, relFile)) || relFile;
|
|
2523
2752
|
try {
|
|
2524
|
-
const
|
|
2753
|
+
const key = cache.key(relFile, await fs13.readFile(path8.join(resolvedRoot, relFile)));
|
|
2754
|
+
let extraction = options.update ? await cache.get(key) : null;
|
|
2755
|
+
if (extraction) {
|
|
2756
|
+
cacheHits++;
|
|
2757
|
+
} else {
|
|
2758
|
+
extraction = await extract(filePath, relFile);
|
|
2759
|
+
await cache.put(key, extraction);
|
|
2760
|
+
}
|
|
2525
2761
|
extractions.push(extraction);
|
|
2526
2762
|
} catch (error) {
|
|
2527
2763
|
progress(` extraction failed for ${relFile}: ${error.message}`);
|
|
2528
2764
|
throw error;
|
|
2529
2765
|
}
|
|
2530
2766
|
}
|
|
2767
|
+
if (options.update) {
|
|
2768
|
+
progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
|
|
2769
|
+
}
|
|
2770
|
+
if (options.extraExtractions && options.extraExtractions.length > 0) {
|
|
2771
|
+
extractions.push(...options.extraExtractions);
|
|
2772
|
+
}
|
|
2773
|
+
progress("Resolving cross-file references...");
|
|
2774
|
+
const resolveStats = resolveCrossFileReferences(extractions, {
|
|
2775
|
+
selfNames: await readSelfNames(resolvedRoot)
|
|
2776
|
+
});
|
|
2777
|
+
if (resolveStats.resolvedEndpoints > 0) {
|
|
2778
|
+
progress(
|
|
2779
|
+
` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2531
2782
|
progress("Building graph...");
|
|
2532
2783
|
const graph = buildGraph(extractions);
|
|
2533
2784
|
progress(`Clustering (${options.algorithm ?? "louvain"})...`);
|
|
@@ -2536,7 +2787,6 @@ async function runPipeline(root, options = {}) {
|
|
|
2536
2787
|
const analysis = analyze(graph);
|
|
2537
2788
|
progress("Rendering report...");
|
|
2538
2789
|
const report = renderReport(graph, analysis);
|
|
2539
|
-
const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
|
|
2540
2790
|
progress(`Exporting to ${outDir} ...`);
|
|
2541
2791
|
await exportGraph(
|
|
2542
2792
|
graph,
|
|
@@ -2555,12 +2805,12 @@ async function runPipeline(root, options = {}) {
|
|
|
2555
2805
|
}
|
|
2556
2806
|
|
|
2557
2807
|
// src/graphStore.ts
|
|
2558
|
-
var
|
|
2559
|
-
var
|
|
2808
|
+
var fs14 = __toESM(require("fs/promises"), 1);
|
|
2809
|
+
var path9 = __toESM(require("path"), 1);
|
|
2560
2810
|
var import_graphology3 = __toESM(require("graphology"), 1);
|
|
2561
|
-
async function loadGraph(outDir =
|
|
2811
|
+
async function loadGraph(outDir = path9.join(process.cwd(), "graphify-out")) {
|
|
2562
2812
|
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2563
|
-
const raw = await
|
|
2813
|
+
const raw = await fs14.readFile(jsonPath, "utf-8");
|
|
2564
2814
|
const data = JSON.parse(raw);
|
|
2565
2815
|
return import_graphology3.default.from(data);
|
|
2566
2816
|
}
|
|
@@ -2597,8 +2847,48 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
2597
2847
|
"does",
|
|
2598
2848
|
"that's"
|
|
2599
2849
|
]);
|
|
2850
|
+
function splitCamelCase(text) {
|
|
2851
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
2852
|
+
}
|
|
2600
2853
|
function tokenize(text) {
|
|
2601
|
-
return text.toLowerCase().split(/[^a-z0-
|
|
2854
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1 && !STOPWORDS.has(token));
|
|
2855
|
+
}
|
|
2856
|
+
function subtokenize(text) {
|
|
2857
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1);
|
|
2858
|
+
}
|
|
2859
|
+
var FUZZY_MIN_TOKEN_LEN = 3;
|
|
2860
|
+
var FUZZY_THRESHOLD = 0.7;
|
|
2861
|
+
var SUBTOKEN_MATCH_SCORE = 1;
|
|
2862
|
+
var SUBSTRING_MATCH_SCORE = 0.6;
|
|
2863
|
+
var FUZZY_MATCH_WEIGHT = 0.5;
|
|
2864
|
+
function osaDistance(a, b) {
|
|
2865
|
+
let prev2 = [];
|
|
2866
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
2867
|
+
let curr = new Array(b.length + 1).fill(0);
|
|
2868
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2869
|
+
curr[0] = i;
|
|
2870
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2871
|
+
const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2872
|
+
let best = Math.min(
|
|
2873
|
+
prev[j] + 1,
|
|
2874
|
+
curr[j - 1] + 1,
|
|
2875
|
+
prev[j - 1] + substitution
|
|
2876
|
+
);
|
|
2877
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
2878
|
+
best = Math.min(best, prev2[j - 2] + 1);
|
|
2879
|
+
}
|
|
2880
|
+
curr[j] = best;
|
|
2881
|
+
}
|
|
2882
|
+
[prev2, prev, curr] = [prev, curr, new Array(b.length + 1).fill(0)];
|
|
2883
|
+
}
|
|
2884
|
+
return prev[b.length];
|
|
2885
|
+
}
|
|
2886
|
+
function fuzzySimilarity(a, b) {
|
|
2887
|
+
if (a === b) return 1;
|
|
2888
|
+
const maxLen = Math.max(a.length, b.length);
|
|
2889
|
+
if (maxLen === 0) return 1;
|
|
2890
|
+
if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;
|
|
2891
|
+
return 1 - osaDistance(a, b) / maxLen;
|
|
2602
2892
|
}
|
|
2603
2893
|
function scoreNodes(graph, query) {
|
|
2604
2894
|
const tokens = tokenize(query);
|
|
@@ -2606,9 +2896,25 @@ function scoreNodes(graph, query) {
|
|
|
2606
2896
|
graph.forEachNode((id, attributes) => {
|
|
2607
2897
|
const label = attributes.label ?? id;
|
|
2608
2898
|
const haystack = `${label} ${id}`.toLowerCase();
|
|
2899
|
+
const subtokens = new Set(subtokenize(`${label} ${id}`));
|
|
2609
2900
|
let score = 0;
|
|
2610
2901
|
for (const token of tokens) {
|
|
2611
|
-
if (
|
|
2902
|
+
if (subtokens.has(token)) {
|
|
2903
|
+
score += SUBTOKEN_MATCH_SCORE;
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
if (haystack.includes(token)) {
|
|
2907
|
+
score += SUBSTRING_MATCH_SCORE;
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
if (token.length >= FUZZY_MIN_TOKEN_LEN) {
|
|
2911
|
+
let best = 0;
|
|
2912
|
+
for (const subtoken of subtokens) {
|
|
2913
|
+
const similarity = fuzzySimilarity(token, subtoken);
|
|
2914
|
+
if (similarity > best) best = similarity;
|
|
2915
|
+
}
|
|
2916
|
+
if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;
|
|
2917
|
+
}
|
|
2612
2918
|
}
|
|
2613
2919
|
if (score > 0) matches.push({ id, label, score });
|
|
2614
2920
|
});
|
|
@@ -2622,22 +2928,36 @@ function resolveNode(graph, query) {
|
|
|
2622
2928
|
var DEFAULT_MAX_DEPTH = 2;
|
|
2623
2929
|
var DEFAULT_MAX_SEEDS = 5;
|
|
2624
2930
|
var DEFAULT_BUDGET = 40;
|
|
2931
|
+
var DEFAULT_TOKEN_BUDGET = 2e3;
|
|
2932
|
+
var NODES_PER_TOKEN = 1 / 10;
|
|
2933
|
+
function nodeTokenCost(graph, id) {
|
|
2934
|
+
const attrs = graph.getNodeAttributes(id);
|
|
2935
|
+
const label = attrs.label ?? id;
|
|
2936
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
2937
|
+
return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);
|
|
2938
|
+
}
|
|
2625
2939
|
function queryGraph(graph, question, options = {}) {
|
|
2626
2940
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
2627
2941
|
const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;
|
|
2628
|
-
const
|
|
2942
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
|
|
2943
|
+
const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));
|
|
2629
2944
|
const allMatches = scoreNodes(graph, question);
|
|
2630
2945
|
const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];
|
|
2631
2946
|
const visited = /* @__PURE__ */ new Map();
|
|
2632
2947
|
const frontier = seeds.map((s) => ({ id: s.id, depth: 0 }));
|
|
2633
|
-
|
|
2634
|
-
|
|
2948
|
+
let spentTokens = 0;
|
|
2949
|
+
for (const seed of seeds) {
|
|
2950
|
+
visited.set(seed.id, 0);
|
|
2951
|
+
spentTokens += nodeTokenCost(graph, seed.id);
|
|
2952
|
+
}
|
|
2953
|
+
while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {
|
|
2635
2954
|
const current = options.dfs ? frontier.pop() : frontier.shift();
|
|
2636
2955
|
if (current.depth >= maxDepth) continue;
|
|
2637
2956
|
const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));
|
|
2638
2957
|
for (const neighbor of neighbors) {
|
|
2639
|
-
if (visited.has(neighbor) || visited.size >= budget) continue;
|
|
2958
|
+
if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;
|
|
2640
2959
|
visited.set(neighbor, current.depth + 1);
|
|
2960
|
+
spentTokens += nodeTokenCost(graph, neighbor);
|
|
2641
2961
|
frontier.push({ id: neighbor, depth: current.depth + 1 });
|
|
2642
2962
|
}
|
|
2643
2963
|
}
|
|
@@ -2665,7 +2985,30 @@ function queryGraph(graph, question, options = {}) {
|
|
|
2665
2985
|
edges.sort(
|
|
2666
2986
|
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
|
|
2667
2987
|
);
|
|
2668
|
-
return { seeds, visited: visitedNodes, edges };
|
|
2988
|
+
return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);
|
|
2989
|
+
}
|
|
2990
|
+
function enforceTokenBudget(result, tokenBudget) {
|
|
2991
|
+
const cost = (value) => Math.ceil(JSON.stringify(value).length / 4);
|
|
2992
|
+
const visited = [...result.visited];
|
|
2993
|
+
let edges = [...result.edges];
|
|
2994
|
+
let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);
|
|
2995
|
+
const seedIds = new Set(result.seeds.map((s) => s.id));
|
|
2996
|
+
while (total > tokenBudget && visited.length > 0) {
|
|
2997
|
+
const last = visited[visited.length - 1];
|
|
2998
|
+
if (seedIds.has(last.id)) break;
|
|
2999
|
+
visited.pop();
|
|
3000
|
+
total -= cost(last);
|
|
3001
|
+
const remaining = [];
|
|
3002
|
+
for (const edge of edges) {
|
|
3003
|
+
if (edge.source === last.id || edge.target === last.id) {
|
|
3004
|
+
total -= cost(edge);
|
|
3005
|
+
} else {
|
|
3006
|
+
remaining.push(edge);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
edges = remaining;
|
|
3010
|
+
}
|
|
3011
|
+
return { seeds: result.seeds, visited, edges };
|
|
2669
3012
|
}
|
|
2670
3013
|
function shortestPath(graph, fromQuery, toQuery) {
|
|
2671
3014
|
const from = resolveNode(graph, fromQuery);
|
|
@@ -2691,25 +3034,25 @@ function shortestPath(graph, fromQuery, toQuery) {
|
|
|
2691
3034
|
if (!visited.has(to.id)) {
|
|
2692
3035
|
return { from, to, found: false, path: [], edges: [] };
|
|
2693
3036
|
}
|
|
2694
|
-
const
|
|
3037
|
+
const path12 = [to.id];
|
|
2695
3038
|
let cursor = to.id;
|
|
2696
3039
|
while (cursor !== from.id) {
|
|
2697
3040
|
const prev = predecessor.get(cursor);
|
|
2698
3041
|
if (!prev) break;
|
|
2699
|
-
|
|
3042
|
+
path12.unshift(prev);
|
|
2700
3043
|
cursor = prev;
|
|
2701
3044
|
}
|
|
2702
3045
|
const edges = [];
|
|
2703
|
-
for (let i = 0; i <
|
|
2704
|
-
const a =
|
|
2705
|
-
const b =
|
|
3046
|
+
for (let i = 0; i < path12.length - 1; i++) {
|
|
3047
|
+
const a = path12[i];
|
|
3048
|
+
const b = path12[i + 1];
|
|
2706
3049
|
const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
|
|
2707
3050
|
if (key) {
|
|
2708
3051
|
const attrs = graph.getEdgeAttributes(key);
|
|
2709
3052
|
edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
|
|
2710
3053
|
}
|
|
2711
3054
|
}
|
|
2712
|
-
return { from, to, found: true, path:
|
|
3055
|
+
return { from, to, found: true, path: path12, edges };
|
|
2713
3056
|
}
|
|
2714
3057
|
function explainNode(graph, query) {
|
|
2715
3058
|
const match = resolveNode(graph, query);
|
|
@@ -2735,25 +3078,813 @@ function explainNode(graph, query) {
|
|
|
2735
3078
|
incoming
|
|
2736
3079
|
};
|
|
2737
3080
|
}
|
|
3081
|
+
|
|
3082
|
+
// src/impact.ts
|
|
3083
|
+
var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
3084
|
+
"calls",
|
|
3085
|
+
"imports",
|
|
3086
|
+
"imports_from",
|
|
3087
|
+
"inherits",
|
|
3088
|
+
"implements",
|
|
3089
|
+
"mixes_in",
|
|
3090
|
+
"embeds",
|
|
3091
|
+
"references",
|
|
3092
|
+
"re_exports",
|
|
3093
|
+
"method",
|
|
3094
|
+
"contains"
|
|
3095
|
+
]);
|
|
3096
|
+
var DEFAULT_IMPACT_DEPTH = 3;
|
|
3097
|
+
var DEFAULT_IMPACT_LIMIT = 200;
|
|
3098
|
+
var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
3099
|
+
function affectedBy(graph, nodeQuery, options = {}) {
|
|
3100
|
+
const target = resolveNode(graph, nodeQuery);
|
|
3101
|
+
if (!target) return null;
|
|
3102
|
+
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
3103
|
+
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
3104
|
+
const affected = [];
|
|
3105
|
+
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
3106
|
+
let frontier = [target.id];
|
|
3107
|
+
let truncated = false;
|
|
3108
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
3109
|
+
const nextFrontier = /* @__PURE__ */ new Map();
|
|
3110
|
+
for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {
|
|
3111
|
+
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
3112
|
+
if (visited.has(source)) return;
|
|
3113
|
+
const relation = edgeAttrs.relation;
|
|
3114
|
+
if (!DEPENDENCY_RELATIONS.has(relation)) return;
|
|
3115
|
+
const confidence = edgeAttrs.confidence;
|
|
3116
|
+
const existing = nextFrontier.get(source);
|
|
3117
|
+
if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
|
|
3118
|
+
const attrs = graph.getNodeAttributes(source);
|
|
3119
|
+
nextFrontier.set(source, {
|
|
3120
|
+
id: source,
|
|
3121
|
+
label: attrs.label ?? source,
|
|
3122
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
3123
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
3124
|
+
depth,
|
|
3125
|
+
via: { relation, confidence, dependsOn: current }
|
|
3126
|
+
});
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3130
|
+
for (const node of roundNodes) {
|
|
3131
|
+
if (affected.length >= limit) {
|
|
3132
|
+
truncated = true;
|
|
3133
|
+
break;
|
|
3134
|
+
}
|
|
3135
|
+
visited.add(node.id);
|
|
3136
|
+
affected.push(node);
|
|
3137
|
+
}
|
|
3138
|
+
frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);
|
|
3139
|
+
}
|
|
3140
|
+
const byConfidence = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
|
|
3141
|
+
for (const node of affected) byConfidence[node.via.confidence] += 1;
|
|
3142
|
+
return { target, affected, byConfidence, maxDepth, truncated };
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// src/merge.ts
|
|
3146
|
+
var import_graphology4 = __toESM(require("graphology"), 1);
|
|
3147
|
+
var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
3148
|
+
function strongerConfidence2(a, b) {
|
|
3149
|
+
return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
|
|
3150
|
+
}
|
|
3151
|
+
function isShared(id) {
|
|
3152
|
+
return id.startsWith("external:") || id.startsWith("module:");
|
|
3153
|
+
}
|
|
3154
|
+
function namespaced(project, id) {
|
|
3155
|
+
return isShared(id) ? id : `${project}/${id}`;
|
|
3156
|
+
}
|
|
3157
|
+
function mergeGraphs(entries) {
|
|
3158
|
+
const names = entries.map((e) => e.name);
|
|
3159
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
3160
|
+
if (duplicate !== void 0) {
|
|
3161
|
+
throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
|
|
3162
|
+
}
|
|
3163
|
+
const merged = new import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
|
|
3164
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
3165
|
+
for (const { name, graph } of sorted) {
|
|
3166
|
+
const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
3167
|
+
for (const id of nodeIds) {
|
|
3168
|
+
const attrs = graph.getNodeAttributes(id);
|
|
3169
|
+
const newId = namespaced(name, id);
|
|
3170
|
+
if (merged.hasNode(newId)) {
|
|
3171
|
+
merged.setNodeAttribute(newId, "project", "(shared)");
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
3174
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
3175
|
+
merged.addNode(newId, {
|
|
3176
|
+
label: attrs.label ?? id,
|
|
3177
|
+
sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
|
|
3178
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
3179
|
+
project: name
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
|
|
3183
|
+
for (const edgeKey of edgeKeys) {
|
|
3184
|
+
const attrs = graph.getEdgeAttributes(edgeKey);
|
|
3185
|
+
const source = namespaced(name, graph.source(edgeKey));
|
|
3186
|
+
const target = namespaced(name, graph.target(edgeKey));
|
|
3187
|
+
const relation = String(attrs.relation);
|
|
3188
|
+
const confidence = attrs.confidence;
|
|
3189
|
+
const key = `${source}|${relation}|${target}`;
|
|
3190
|
+
if (merged.hasEdge(key)) {
|
|
3191
|
+
const existing = merged.getEdgeAttribute(key, "confidence");
|
|
3192
|
+
merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
merged.addEdgeWithKey(key, source, target, { relation, confidence });
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return merged;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// src/extractors/mysql.ts
|
|
3202
|
+
var schemaId = (schema) => `db:${schema}`;
|
|
3203
|
+
var tableId = (schema, table) => `db:${schema}::${table}`;
|
|
3204
|
+
var columnId = (schema, table, column) => `db:${schema}::${table}.${column}`;
|
|
3205
|
+
async function defaultQueryFn(dsn) {
|
|
3206
|
+
const { connection } = validateDsn(dsn);
|
|
3207
|
+
const mysql = await import("mysql2/promise");
|
|
3208
|
+
const conn = await mysql.createConnection({
|
|
3209
|
+
host: connection.host,
|
|
3210
|
+
port: connection.port,
|
|
3211
|
+
user: connection.user,
|
|
3212
|
+
password: connection.password,
|
|
3213
|
+
database: connection.database
|
|
3214
|
+
});
|
|
3215
|
+
return {
|
|
3216
|
+
query: async (sql, params) => {
|
|
3217
|
+
const [rows] = await conn.execute(sql, [...params]);
|
|
3218
|
+
return rows;
|
|
3219
|
+
},
|
|
3220
|
+
close: () => conn.end()
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
async function extractMysql(dsn, queryFn) {
|
|
3224
|
+
const { safeDisplay, connection } = validateDsn(dsn);
|
|
3225
|
+
const schema = connection.database;
|
|
3226
|
+
let query = queryFn;
|
|
3227
|
+
let close;
|
|
3228
|
+
if (!query) {
|
|
3229
|
+
const live = await defaultQueryFn(dsn);
|
|
3230
|
+
query = live.query;
|
|
3231
|
+
close = live.close;
|
|
3232
|
+
}
|
|
3233
|
+
try {
|
|
3234
|
+
const nodes = [];
|
|
3235
|
+
const edges = [];
|
|
3236
|
+
nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });
|
|
3237
|
+
const tableRows = await query(
|
|
3238
|
+
"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
3239
|
+
[schema]
|
|
3240
|
+
);
|
|
3241
|
+
const tables = tableRows.map((row) => ({
|
|
3242
|
+
name: String(row.TABLE_NAME),
|
|
3243
|
+
isView: String(row.TABLE_TYPE).toUpperCase() === "VIEW"
|
|
3244
|
+
}));
|
|
3245
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
3246
|
+
for (const table of tables) {
|
|
3247
|
+
nodes.push({
|
|
3248
|
+
id: tableId(schema, table.name),
|
|
3249
|
+
label: `${table.isView ? "view" : "table"} ${table.name}`,
|
|
3250
|
+
sourceFile: safeDisplay,
|
|
3251
|
+
sourceLocation: table.name
|
|
3252
|
+
});
|
|
3253
|
+
edges.push({
|
|
3254
|
+
source: schemaId(schema),
|
|
3255
|
+
target: tableId(schema, table.name),
|
|
3256
|
+
relation: "contains",
|
|
3257
|
+
confidence: "EXTRACTED"
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
const columnRows = await query(
|
|
3261
|
+
"SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION",
|
|
3262
|
+
[schema]
|
|
3263
|
+
);
|
|
3264
|
+
for (const row of columnRows) {
|
|
3265
|
+
const table = String(row.TABLE_NAME);
|
|
3266
|
+
const column = String(row.COLUMN_NAME);
|
|
3267
|
+
if (!tableNames.has(table)) continue;
|
|
3268
|
+
nodes.push({
|
|
3269
|
+
id: columnId(schema, table, column),
|
|
3270
|
+
label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,
|
|
3271
|
+
sourceFile: safeDisplay,
|
|
3272
|
+
sourceLocation: `${table}.${column}`
|
|
3273
|
+
});
|
|
3274
|
+
edges.push({
|
|
3275
|
+
source: tableId(schema, table),
|
|
3276
|
+
target: columnId(schema, table, column),
|
|
3277
|
+
relation: "contains",
|
|
3278
|
+
confidence: "EXTRACTED"
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
const fkRows = await query(
|
|
3282
|
+
"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",
|
|
3283
|
+
[schema]
|
|
3284
|
+
);
|
|
3285
|
+
for (const row of fkRows) {
|
|
3286
|
+
const table = String(row.TABLE_NAME);
|
|
3287
|
+
const column = String(row.COLUMN_NAME);
|
|
3288
|
+
const referenced = String(row.REFERENCED_TABLE_NAME);
|
|
3289
|
+
if (!tableNames.has(table) || !tableNames.has(referenced)) continue;
|
|
3290
|
+
edges.push({
|
|
3291
|
+
source: columnId(schema, table, column),
|
|
3292
|
+
target: tableId(schema, referenced),
|
|
3293
|
+
relation: "references",
|
|
3294
|
+
confidence: "EXTRACTED"
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
const viewNames = tables.filter((t) => t.isView).map((t) => t.name);
|
|
3298
|
+
if (viewNames.length > 0) {
|
|
3299
|
+
await addViewEdges(query, schema, viewNames, tableNames, edges);
|
|
3300
|
+
}
|
|
3301
|
+
return { nodes, edges };
|
|
3302
|
+
} finally {
|
|
3303
|
+
await close?.();
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
async function addViewEdges(query, schema, viewNames, tableNames, edges) {
|
|
3307
|
+
try {
|
|
3308
|
+
const usageRows = await query(
|
|
3309
|
+
"SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME",
|
|
3310
|
+
[schema, schema]
|
|
3311
|
+
);
|
|
3312
|
+
for (const row of usageRows) {
|
|
3313
|
+
const view = String(row.VIEW_NAME);
|
|
3314
|
+
const table = String(row.TABLE_NAME);
|
|
3315
|
+
if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;
|
|
3316
|
+
edges.push({
|
|
3317
|
+
source: tableId(schema, view),
|
|
3318
|
+
target: tableId(schema, table),
|
|
3319
|
+
relation: "references",
|
|
3320
|
+
confidence: "EXTRACTED"
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
return;
|
|
3324
|
+
} catch {
|
|
3325
|
+
}
|
|
3326
|
+
const definitionRows = await query(
|
|
3327
|
+
"SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
3328
|
+
[schema]
|
|
3329
|
+
);
|
|
3330
|
+
for (const row of definitionRows) {
|
|
3331
|
+
const view = String(row.TABLE_NAME);
|
|
3332
|
+
if (!viewNames.includes(view)) continue;
|
|
3333
|
+
const definition = String(row.VIEW_DEFINITION ?? "").toLowerCase();
|
|
3334
|
+
for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {
|
|
3335
|
+
if (table === view) continue;
|
|
3336
|
+
if (new RegExp(`\\b${table.toLowerCase()}\\b`).test(definition)) {
|
|
3337
|
+
edges.push({
|
|
3338
|
+
source: tableId(schema, view),
|
|
3339
|
+
target: tableId(schema, table),
|
|
3340
|
+
relation: "references",
|
|
3341
|
+
confidence: "INFERRED"
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/memory.ts
|
|
3349
|
+
var import_node_crypto4 = require("crypto");
|
|
3350
|
+
var fs15 = __toESM(require("fs/promises"), 1);
|
|
3351
|
+
var path10 = __toESM(require("path"), 1);
|
|
3352
|
+
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
3353
|
+
var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
|
|
3354
|
+
function validateResult(value) {
|
|
3355
|
+
if (!value.question) throw new Error("save-result requires --question");
|
|
3356
|
+
if (!value.answer) throw new Error("save-result requires --answer");
|
|
3357
|
+
if (!OUTCOMES.has(value.outcome)) {
|
|
3358
|
+
throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
|
|
3359
|
+
}
|
|
3360
|
+
if (!TYPES.has(value.type)) {
|
|
3361
|
+
throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
|
|
3362
|
+
}
|
|
3363
|
+
if (value.outcome === "corrected" && !value.correction) {
|
|
3364
|
+
throw new Error("--outcome corrected requires --correction with what the right answer was");
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
3368
|
+
validateResult(entry);
|
|
3369
|
+
await fs15.mkdir(memoryDir, { recursive: true });
|
|
3370
|
+
const saved = { ...entry, savedAt: now.toISOString() };
|
|
3371
|
+
const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
3372
|
+
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
3373
|
+
const filePath = path10.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
3374
|
+
await fs15.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
3375
|
+
return filePath;
|
|
3376
|
+
}
|
|
3377
|
+
async function loadResults(memoryDir) {
|
|
3378
|
+
let files;
|
|
3379
|
+
try {
|
|
3380
|
+
files = (await fs15.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
|
|
3381
|
+
} catch {
|
|
3382
|
+
return [];
|
|
3383
|
+
}
|
|
3384
|
+
const results = [];
|
|
3385
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
3386
|
+
try {
|
|
3387
|
+
const parsed = JSON.parse(await fs15.readFile(path10.join(memoryDir, file), "utf-8"));
|
|
3388
|
+
if (parsed.question && parsed.savedAt) results.push(parsed);
|
|
3389
|
+
} catch {
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return results;
|
|
3393
|
+
}
|
|
3394
|
+
function renderLessons(results, options = {}) {
|
|
3395
|
+
const halfLife = options.halfLifeDays ?? 30;
|
|
3396
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3397
|
+
const minCorroboration = options.minCorroboration ?? 2;
|
|
3398
|
+
const weightOf = (savedAt) => {
|
|
3399
|
+
const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
|
|
3400
|
+
return Math.pow(0.5, ageDays / halfLife);
|
|
3401
|
+
};
|
|
3402
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3403
|
+
const corrections = [];
|
|
3404
|
+
for (const result of results) {
|
|
3405
|
+
const weight = weightOf(result.savedAt);
|
|
3406
|
+
for (const node of result.nodes ?? []) {
|
|
3407
|
+
const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
|
|
3408
|
+
if (result.outcome === "useful") {
|
|
3409
|
+
signal.useful += weight;
|
|
3410
|
+
signal.usefulCount++;
|
|
3411
|
+
} else if (result.outcome === "dead_end") {
|
|
3412
|
+
signal.deadEnd += weight;
|
|
3413
|
+
signal.deadEndCount++;
|
|
3414
|
+
}
|
|
3415
|
+
signals.set(node, signal);
|
|
3416
|
+
}
|
|
3417
|
+
if (result.outcome === "corrected" && result.correction) {
|
|
3418
|
+
corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
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);
|
|
3422
|
+
const lines = [
|
|
3423
|
+
"# Graph lessons",
|
|
3424
|
+
"",
|
|
3425
|
+
`_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
|
|
3426
|
+
"",
|
|
3427
|
+
"## Nodes that keep answering",
|
|
3428
|
+
""
|
|
3429
|
+
];
|
|
3430
|
+
const useful = byScore("useful");
|
|
3431
|
+
if (useful.length === 0) {
|
|
3432
|
+
lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
|
|
3433
|
+
} else {
|
|
3434
|
+
for (const [node, s] of useful) {
|
|
3435
|
+
lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
lines.push("", "## Dead ends", "");
|
|
3439
|
+
const deadEnds = byScore("deadEnd");
|
|
3440
|
+
if (deadEnds.length === 0) {
|
|
3441
|
+
lines.push("(none recorded)");
|
|
3442
|
+
} else {
|
|
3443
|
+
for (const [node, s] of deadEnds) {
|
|
3444
|
+
lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
lines.push("", "## Corrections", "");
|
|
3448
|
+
if (corrections.length === 0) {
|
|
3449
|
+
lines.push("(none recorded)");
|
|
3450
|
+
} else {
|
|
3451
|
+
corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
3452
|
+
for (const c of corrections.slice(0, 20)) {
|
|
3453
|
+
lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
|
|
3454
|
+
lines.push(` Right answer: ${c.correction}`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
lines.push("");
|
|
3458
|
+
return lines.join("\n");
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// src/context.ts
|
|
3462
|
+
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
3463
|
+
var DEFAULT_MAX_SEEDS2 = 8;
|
|
3464
|
+
var DEFAULT_MAX_SNIPPET_LINES = 60;
|
|
3465
|
+
var NEIGHBOR_DECAY = 0.4;
|
|
3466
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
3467
|
+
function lineNumberOf(sourceLocation) {
|
|
3468
|
+
const match = /^L(\d+)$/.exec(sourceLocation);
|
|
3469
|
+
return match ? Number(match[1]) : null;
|
|
3470
|
+
}
|
|
3471
|
+
function isReadableFile(sourceFile) {
|
|
3472
|
+
return sourceFile !== "" && !sourceFile.startsWith("<") && !sourceFile.includes("://");
|
|
3473
|
+
}
|
|
3474
|
+
function rankForTask(graph, task, maxSeeds = DEFAULT_MAX_SEEDS2) {
|
|
3475
|
+
const seeds = scoreNodes(graph, task).slice(0, maxSeeds);
|
|
3476
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
3477
|
+
for (const seed of seeds) {
|
|
3478
|
+
ranked.set(seed.id, { id: seed.id, score: seed.score, reason: "matches the task" });
|
|
3479
|
+
}
|
|
3480
|
+
for (const seed of seeds) {
|
|
3481
|
+
graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {
|
|
3482
|
+
const neighbor = source === seed.id ? target : source;
|
|
3483
|
+
if (ranked.has(neighbor)) return;
|
|
3484
|
+
const relation = String(attrs.relation);
|
|
3485
|
+
const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;
|
|
3486
|
+
ranked.set(neighbor, {
|
|
3487
|
+
id: neighbor,
|
|
3488
|
+
score: seed.score * NEIGHBOR_DECAY,
|
|
3489
|
+
reason: `${direction} ${seed.label}`
|
|
3490
|
+
});
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
3494
|
+
}
|
|
3495
|
+
function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
|
|
3496
|
+
const nextStart = entityStartsInFile.find((l) => l > startLine);
|
|
3497
|
+
const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
|
|
3498
|
+
return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
|
|
3499
|
+
}
|
|
3500
|
+
async function buildContextPack(graph, task, readFile14, options = {}) {
|
|
3501
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
3502
|
+
const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
|
|
3503
|
+
const ranked = rankForTask(graph, task, options.maxSeeds);
|
|
3504
|
+
const entityStarts = /* @__PURE__ */ new Map();
|
|
3505
|
+
graph.forEachNode((_id, attrs) => {
|
|
3506
|
+
const file = attrs.sourceFile;
|
|
3507
|
+
const line = lineNumberOf(attrs.sourceLocation ?? "");
|
|
3508
|
+
if (file && line !== null) {
|
|
3509
|
+
const starts = entityStarts.get(file) ?? [];
|
|
3510
|
+
starts.push(line);
|
|
3511
|
+
entityStarts.set(file, starts);
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);
|
|
3515
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
3516
|
+
const readLines = async (file) => {
|
|
3517
|
+
const cached = fileCache.get(file);
|
|
3518
|
+
if (cached !== void 0) return cached;
|
|
3519
|
+
let lines;
|
|
3520
|
+
try {
|
|
3521
|
+
lines = (await readFile14(file)).split("\n");
|
|
3522
|
+
} catch {
|
|
3523
|
+
lines = null;
|
|
3524
|
+
}
|
|
3525
|
+
fileCache.set(file, lines);
|
|
3526
|
+
return lines;
|
|
3527
|
+
};
|
|
3528
|
+
const snippets = [];
|
|
3529
|
+
const overflow = [];
|
|
3530
|
+
const coveredRanges = /* @__PURE__ */ new Map();
|
|
3531
|
+
let tokens = 0;
|
|
3532
|
+
for (const node of ranked) {
|
|
3533
|
+
const attrs = graph.getNodeAttributes(node.id);
|
|
3534
|
+
const file = attrs.sourceFile ?? "";
|
|
3535
|
+
const startLine = lineNumberOf(attrs.sourceLocation ?? "");
|
|
3536
|
+
if (!isReadableFile(file) || startLine === null) continue;
|
|
3537
|
+
const lines = await readLines(file);
|
|
3538
|
+
if (lines === null) continue;
|
|
3539
|
+
const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);
|
|
3540
|
+
const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);
|
|
3541
|
+
if (covered) continue;
|
|
3542
|
+
const code = lines.slice(start - 1, end).join("\n");
|
|
3543
|
+
const cost = tokensOf(code) + 15;
|
|
3544
|
+
if (tokens + cost > tokenBudget) {
|
|
3545
|
+
overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
tokens += cost;
|
|
3549
|
+
snippets.push({
|
|
3550
|
+
nodeId: node.id,
|
|
3551
|
+
label: attrs.label ?? node.id,
|
|
3552
|
+
file,
|
|
3553
|
+
startLine: start,
|
|
3554
|
+
endLine: end,
|
|
3555
|
+
code,
|
|
3556
|
+
reason: node.reason,
|
|
3557
|
+
score: node.score
|
|
3558
|
+
});
|
|
3559
|
+
const ranges = coveredRanges.get(file) ?? [];
|
|
3560
|
+
ranges.push({ start, end });
|
|
3561
|
+
coveredRanges.set(file, ranges);
|
|
3562
|
+
}
|
|
3563
|
+
return { task, snippets, tokens, tokenBudget, overflow };
|
|
3564
|
+
}
|
|
3565
|
+
function renderContextPack(pack) {
|
|
3566
|
+
const lines = [
|
|
3567
|
+
`# Context pack: ${pack.task}`,
|
|
3568
|
+
"",
|
|
3569
|
+
`_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,
|
|
3570
|
+
""
|
|
3571
|
+
];
|
|
3572
|
+
if (pack.snippets.length === 0) {
|
|
3573
|
+
lines.push("No matching code found \u2014 try different keywords.");
|
|
3574
|
+
return lines.join("\n");
|
|
3575
|
+
}
|
|
3576
|
+
for (const snippet of pack.snippets) {
|
|
3577
|
+
lines.push(`## ${snippet.label} \u2014 \`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\``);
|
|
3578
|
+
lines.push(`_${snippet.reason}_`);
|
|
3579
|
+
lines.push("```");
|
|
3580
|
+
lines.push(snippet.code);
|
|
3581
|
+
lines.push("```");
|
|
3582
|
+
lines.push("");
|
|
3583
|
+
}
|
|
3584
|
+
if (pack.overflow.length > 0) {
|
|
3585
|
+
lines.push(`## Didn't fit the budget (${pack.overflow.length})`);
|
|
3586
|
+
for (const item of pack.overflow.slice(0, 15)) {
|
|
3587
|
+
lines.push(`- ${item.nodeId} (${item.file})`);
|
|
3588
|
+
}
|
|
3589
|
+
lines.push("");
|
|
3590
|
+
}
|
|
3591
|
+
return lines.join("\n");
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/testmap.ts
|
|
3595
|
+
var TEST_FILE_PATTERNS = [
|
|
3596
|
+
/(^|\/)tests?\//,
|
|
3597
|
+
// tests/ or test/ directory
|
|
3598
|
+
/(^|\/)__tests__\//,
|
|
3599
|
+
/(^|\/)spec\//,
|
|
3600
|
+
/\.test\.[cm]?[jt]sx?$/,
|
|
3601
|
+
/\.spec\.[cm]?[jt]sx?$/,
|
|
3602
|
+
/(^|\/)test_[^/]+\.py$/,
|
|
3603
|
+
/_test\.py$/,
|
|
3604
|
+
/_test\.go$/,
|
|
3605
|
+
/Tests?\.(java|cs)$/,
|
|
3606
|
+
/_spec\.rb$/,
|
|
3607
|
+
/_test\.rb$/
|
|
3608
|
+
];
|
|
3609
|
+
function isTestFile(path12) {
|
|
3610
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path12));
|
|
3611
|
+
}
|
|
3612
|
+
var TEST_REACH_DEPTH = 4;
|
|
3613
|
+
var TEST_REACH_LIMIT = 2e3;
|
|
3614
|
+
function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
3615
|
+
const best = /* @__PURE__ */ new Map();
|
|
3616
|
+
let affectedNodeCount = 0;
|
|
3617
|
+
for (const id of targetIds) {
|
|
3618
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
3619
|
+
if (!impact) continue;
|
|
3620
|
+
affectedNodeCount += impact.affected.length;
|
|
3621
|
+
for (const node of impact.affected) {
|
|
3622
|
+
if (!isTestFile(node.sourceFile)) continue;
|
|
3623
|
+
const existing = best.get(node.sourceFile);
|
|
3624
|
+
if (!existing || node.depth < existing.depth) {
|
|
3625
|
+
best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
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));
|
|
3630
|
+
return { target: targetLabel, testFiles, affectedNodeCount };
|
|
3631
|
+
}
|
|
3632
|
+
function testsForNode(graph, nodeQuery) {
|
|
3633
|
+
const match = resolveNode(graph, nodeQuery);
|
|
3634
|
+
if (!match) return null;
|
|
3635
|
+
return selectionFromImpact(graph, [match.id], match.id);
|
|
3636
|
+
}
|
|
3637
|
+
function testsForChangedFiles(graph, changedFiles) {
|
|
3638
|
+
const fileIds = [];
|
|
3639
|
+
const selfSelected = /* @__PURE__ */ new Map();
|
|
3640
|
+
for (const file of changedFiles) {
|
|
3641
|
+
if (isTestFile(file)) {
|
|
3642
|
+
selfSelected.set(file, { depth: 0, via: "(changed directly)" });
|
|
3643
|
+
continue;
|
|
3644
|
+
}
|
|
3645
|
+
if (graph.hasNode(file)) fileIds.push(file);
|
|
3646
|
+
}
|
|
3647
|
+
const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
|
|
3648
|
+
for (const [file, info] of selfSelected) {
|
|
3649
|
+
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
3650
|
+
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
3654
|
+
return selection;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// src/diff.ts
|
|
3658
|
+
var import_node_child_process = require("child_process");
|
|
3659
|
+
var fs16 = __toESM(require("fs/promises"), 1);
|
|
3660
|
+
var os = __toESM(require("os"), 1);
|
|
3661
|
+
var path11 = __toESM(require("path"), 1);
|
|
3662
|
+
var import_node_util = require("util");
|
|
3663
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
3664
|
+
async function graphForDirectory(root) {
|
|
3665
|
+
const manifest = collectFiles(root);
|
|
3666
|
+
const extractions = [];
|
|
3667
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
3668
|
+
extractions.push(await extract(path11.join(root, relFile), relFile));
|
|
3669
|
+
}
|
|
3670
|
+
resolveCrossFileReferences(extractions, { selfNames: await readSelfNames(root) });
|
|
3671
|
+
return buildGraph(extractions);
|
|
3672
|
+
}
|
|
3673
|
+
async function checkoutRev(repoRoot, rev) {
|
|
3674
|
+
const dest = await fs16.mkdtemp(path11.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
|
|
3675
|
+
const { stdout } = await execFileAsync(
|
|
3676
|
+
"git",
|
|
3677
|
+
["-C", repoRoot, "archive", "--format=tar", rev],
|
|
3678
|
+
{ encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
|
|
3679
|
+
);
|
|
3680
|
+
await new Promise((resolve5, reject) => {
|
|
3681
|
+
const tar = (0, import_node_child_process.execFile)("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve5());
|
|
3682
|
+
tar.stdin?.end(stdout);
|
|
3683
|
+
});
|
|
3684
|
+
return dest;
|
|
3685
|
+
}
|
|
3686
|
+
function edgeSignature(graph, id) {
|
|
3687
|
+
const signature = /* @__PURE__ */ new Set();
|
|
3688
|
+
graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
|
|
3689
|
+
graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
|
|
3690
|
+
return signature;
|
|
3691
|
+
}
|
|
3692
|
+
function describeSymbol(graph, id) {
|
|
3693
|
+
const attrs = graph.getNodeAttributes(id);
|
|
3694
|
+
const impact = affectedBy(graph, id, { maxDepth: 3 });
|
|
3695
|
+
const tests = testsForNode(graph, id);
|
|
3696
|
+
return {
|
|
3697
|
+
id,
|
|
3698
|
+
label: attrs.label ?? id,
|
|
3699
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
3700
|
+
blastRadius: impact?.affected.length ?? 0,
|
|
3701
|
+
tests: tests?.testFiles.map((t) => t.file) ?? []
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
function entityIds(graph) {
|
|
3705
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3706
|
+
graph.forEachNode((id) => {
|
|
3707
|
+
if (id.includes("::")) ids.add(id);
|
|
3708
|
+
});
|
|
3709
|
+
return ids;
|
|
3710
|
+
}
|
|
3711
|
+
function diffGraphs(baseGraph, headGraph, base, head) {
|
|
3712
|
+
const baseIds = entityIds(baseGraph);
|
|
3713
|
+
const headIds = entityIds(headGraph);
|
|
3714
|
+
const added = [];
|
|
3715
|
+
const removed = [];
|
|
3716
|
+
const rewired = [];
|
|
3717
|
+
for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
|
|
3718
|
+
if (!baseIds.has(id)) {
|
|
3719
|
+
added.push(describeSymbol(headGraph, id));
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
const before = edgeSignature(baseGraph, id);
|
|
3723
|
+
const after = edgeSignature(headGraph, id);
|
|
3724
|
+
const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
|
|
3725
|
+
const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
|
|
3726
|
+
if (gained.length > 0 || lost.length > 0) {
|
|
3727
|
+
rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
|
|
3731
|
+
if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
|
|
3732
|
+
}
|
|
3733
|
+
return { base, head, added, removed, rewired };
|
|
3734
|
+
}
|
|
3735
|
+
async function reviewRevisions(repoRoot, base, head = "HEAD") {
|
|
3736
|
+
const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
|
|
3737
|
+
try {
|
|
3738
|
+
const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
|
|
3739
|
+
return diffGraphs(baseGraph, headGraph, base, head);
|
|
3740
|
+
} finally {
|
|
3741
|
+
await Promise.all([
|
|
3742
|
+
fs16.rm(baseDir, { recursive: true, force: true }),
|
|
3743
|
+
fs16.rm(headDir, { recursive: true, force: true })
|
|
3744
|
+
]);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
function renderReview(diff) {
|
|
3748
|
+
const lines = [
|
|
3749
|
+
`# Structural review: ${diff.base}..${diff.head}`,
|
|
3750
|
+
"",
|
|
3751
|
+
`${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
|
|
3752
|
+
""
|
|
3753
|
+
];
|
|
3754
|
+
const section = (title, symbols) => {
|
|
3755
|
+
lines.push(`## ${title} (${symbols.length})`, "");
|
|
3756
|
+
if (symbols.length === 0) {
|
|
3757
|
+
lines.push("(none)", "");
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
for (const s of symbols) {
|
|
3761
|
+
const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
|
|
3762
|
+
lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
|
|
3763
|
+
const r = s;
|
|
3764
|
+
if (r.gainedEdges) {
|
|
3765
|
+
for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
|
|
3766
|
+
for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
lines.push("");
|
|
3770
|
+
};
|
|
3771
|
+
section("Removed \u2014 check every caller", diff.removed);
|
|
3772
|
+
section("Rewired \u2014 dependencies changed", diff.rewired);
|
|
3773
|
+
section("Added", diff.added);
|
|
3774
|
+
const allTests = /* @__PURE__ */ new Set();
|
|
3775
|
+
for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
|
|
3776
|
+
for (const t of s.tests) allTests.add(t);
|
|
3777
|
+
}
|
|
3778
|
+
lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
|
|
3779
|
+
for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
|
|
3780
|
+
lines.push("");
|
|
3781
|
+
return lines.join("\n");
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// src/check.ts
|
|
3785
|
+
var DEPENDENCY_RELATIONS2 = /* @__PURE__ */ new Set([
|
|
3786
|
+
"calls",
|
|
3787
|
+
"imports",
|
|
3788
|
+
"imports_from",
|
|
3789
|
+
"inherits",
|
|
3790
|
+
"implements",
|
|
3791
|
+
"mixes_in",
|
|
3792
|
+
"embeds",
|
|
3793
|
+
"re_exports"
|
|
3794
|
+
]);
|
|
3795
|
+
function globToRegExp(glob) {
|
|
3796
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3797
|
+
const pattern = escaped.replace(
|
|
3798
|
+
/\*\*|\*|\?/g,
|
|
3799
|
+
(token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
|
|
3800
|
+
);
|
|
3801
|
+
return new RegExp(`^${pattern}$`);
|
|
3802
|
+
}
|
|
3803
|
+
function validateRules(value) {
|
|
3804
|
+
const config = value;
|
|
3805
|
+
if (!config || !Array.isArray(config.rules)) {
|
|
3806
|
+
throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
|
|
3807
|
+
}
|
|
3808
|
+
for (const rule of config.rules) {
|
|
3809
|
+
if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
|
|
3810
|
+
throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return config;
|
|
3814
|
+
}
|
|
3815
|
+
function checkRules(graph, config) {
|
|
3816
|
+
const compiled = config.rules.map((rule) => ({
|
|
3817
|
+
rule,
|
|
3818
|
+
from: globToRegExp(rule.from),
|
|
3819
|
+
disallow: rule.disallow.map(globToRegExp)
|
|
3820
|
+
}));
|
|
3821
|
+
const violations = [];
|
|
3822
|
+
graph.forEachEdge((_edge, attrs, source, target) => {
|
|
3823
|
+
const relation = String(attrs.relation);
|
|
3824
|
+
if (!DEPENDENCY_RELATIONS2.has(relation)) return;
|
|
3825
|
+
const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
|
|
3826
|
+
const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
|
|
3827
|
+
if (!fromFile || !toFile || fromFile === toFile) return;
|
|
3828
|
+
for (const { rule, from, disallow } of compiled) {
|
|
3829
|
+
if (!from.test(fromFile)) continue;
|
|
3830
|
+
if (disallow.some((d) => d.test(toFile))) {
|
|
3831
|
+
violations.push({
|
|
3832
|
+
rule: rule.name,
|
|
3833
|
+
reason: rule.reason,
|
|
3834
|
+
fromFile,
|
|
3835
|
+
fromNode: source,
|
|
3836
|
+
toFile,
|
|
3837
|
+
toNode: target,
|
|
3838
|
+
relation
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
});
|
|
3843
|
+
violations.sort(
|
|
3844
|
+
(a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
|
|
3845
|
+
);
|
|
3846
|
+
return violations;
|
|
3847
|
+
}
|
|
2738
3848
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2739
3849
|
0 && (module.exports = {
|
|
2740
3850
|
EXTRACTOR_REGISTRY,
|
|
3851
|
+
ExtractionCache,
|
|
2741
3852
|
ExtractionResultSchema,
|
|
2742
3853
|
ExtractionValidationError,
|
|
3854
|
+
affectedBy,
|
|
2743
3855
|
analyze,
|
|
3856
|
+
buildContextPack,
|
|
2744
3857
|
buildGraph,
|
|
3858
|
+
checkRules,
|
|
2745
3859
|
cluster,
|
|
2746
3860
|
collectFiles,
|
|
3861
|
+
diffGraphs,
|
|
2747
3862
|
explainNode,
|
|
2748
3863
|
exportGraph,
|
|
2749
3864
|
extract,
|
|
3865
|
+
extractMysql,
|
|
3866
|
+
globToRegExp,
|
|
3867
|
+
graphForDirectory,
|
|
3868
|
+
isTestFile,
|
|
2750
3869
|
loadGraph,
|
|
3870
|
+
loadResults,
|
|
3871
|
+
mergeGraphs,
|
|
2751
3872
|
queryGraph,
|
|
3873
|
+
rankForTask,
|
|
3874
|
+
renderContextPack,
|
|
3875
|
+
renderLessons,
|
|
2752
3876
|
renderReport,
|
|
3877
|
+
renderReview,
|
|
3878
|
+
resolveCrossFileReferences,
|
|
2753
3879
|
resolveNode,
|
|
3880
|
+
reviewRevisions,
|
|
2754
3881
|
runPipeline,
|
|
3882
|
+
saveResult,
|
|
2755
3883
|
scoreNodes,
|
|
2756
3884
|
shortestPath,
|
|
2757
|
-
|
|
3885
|
+
testsForChangedFiles,
|
|
3886
|
+
testsForNode,
|
|
3887
|
+
validateExtraction,
|
|
3888
|
+
validateRules
|
|
2758
3889
|
});
|
|
2759
3890
|
//# sourceMappingURL=index.cjs.map
|