@dreamtree-org/graphify 1.1.1 → 1.1.3
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 +6 -2
- package/dist/{chunk-EW23BLJC.js → chunk-NS5HB65S.js} +194 -75
- package/dist/chunk-NS5HB65S.js.map +1 -0
- package/dist/{chunk-YT7B6DOD.js → chunk-ZK3TA6FW.js} +33 -7
- package/dist/chunk-ZK3TA6FW.js.map +1 -0
- package/dist/cli/index.cjs +382 -228
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +18 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +245 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +2 -2
- package/dist/mcp/server.cjs +26 -5
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-EW23BLJC.js.map +0 -1
- package/dist/chunk-YT7B6DOD.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1603,10 +1603,10 @@ async function extractRust(filePath, displayPath) {
|
|
|
1603
1603
|
}
|
|
1604
1604
|
}
|
|
1605
1605
|
} else if (fn.type === "scoped_identifier") {
|
|
1606
|
-
const
|
|
1606
|
+
const path12 = fn.childForFieldName("path")?.text;
|
|
1607
1607
|
const name = fn.childForFieldName("name")?.text;
|
|
1608
|
-
if (
|
|
1609
|
-
const viaImport = importIndex.get(
|
|
1608
|
+
if (path12 && name) {
|
|
1609
|
+
const viaImport = importIndex.get(path12);
|
|
1610
1610
|
if (viaImport) {
|
|
1611
1611
|
targetId = viaImport.id;
|
|
1612
1612
|
confidence = "INFERRED";
|
|
@@ -1677,13 +1677,11 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
1677
1677
|
});
|
|
1678
1678
|
}
|
|
1679
1679
|
function importTargetId(binding, property) {
|
|
1680
|
-
if (
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
return entityId(binding.ref.id, property);
|
|
1686
|
-
}
|
|
1680
|
+
if (binding.kind === "named" && binding.importedName) {
|
|
1681
|
+
return entityId(binding.ref.id, binding.importedName);
|
|
1682
|
+
}
|
|
1683
|
+
if (binding.kind === "namespace" && property) {
|
|
1684
|
+
return entityId(binding.ref.id, property);
|
|
1687
1685
|
}
|
|
1688
1686
|
return binding.ref.id;
|
|
1689
1687
|
}
|
|
@@ -1721,24 +1719,35 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
1721
1719
|
}
|
|
1722
1720
|
}
|
|
1723
1721
|
}
|
|
1722
|
+
function unwrapExpression(node) {
|
|
1723
|
+
let current = node;
|
|
1724
|
+
while (current.type === "as_expression" || current.type === "satisfies_expression" || current.type === "non_null_expression" || current.type === "parenthesized_expression") {
|
|
1725
|
+
const inner = namedChildren(current)[0];
|
|
1726
|
+
if (!inner) break;
|
|
1727
|
+
current = inner;
|
|
1728
|
+
}
|
|
1729
|
+
return current;
|
|
1730
|
+
}
|
|
1724
1731
|
function handleTopLevelVariableFunctions(node) {
|
|
1732
|
+
const isExported = node.parent?.type === "export_statement";
|
|
1725
1733
|
for (const declarator of namedChildren(node)) {
|
|
1726
1734
|
if (declarator.type !== "variable_declarator") continue;
|
|
1727
1735
|
const value = declarator.childForFieldName("value");
|
|
1728
1736
|
if (!value) continue;
|
|
1729
|
-
|
|
1737
|
+
const core = unwrapExpression(value);
|
|
1738
|
+
if (core.type === "arrow_function" || core.type === "function_expression") {
|
|
1730
1739
|
const name = declarator.childForFieldName("name")?.text;
|
|
1731
1740
|
if (!name) continue;
|
|
1732
1741
|
const id = entityId(sourceFile, name);
|
|
1733
1742
|
builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
|
|
1734
1743
|
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1735
1744
|
nameIndex.set(name, id);
|
|
1736
|
-
functionNodeMap.set(
|
|
1745
|
+
functionNodeMap.set(core.id, id);
|
|
1737
1746
|
continue;
|
|
1738
1747
|
}
|
|
1739
|
-
if (
|
|
1740
|
-
const callee =
|
|
1741
|
-
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(
|
|
1748
|
+
if (core.type === "call_expression") {
|
|
1749
|
+
const callee = core.childForFieldName("function");
|
|
1750
|
+
const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(core) : null;
|
|
1742
1751
|
if (specifier) {
|
|
1743
1752
|
const nameNode = declarator.childForFieldName("name");
|
|
1744
1753
|
if (nameNode) {
|
|
@@ -1746,6 +1755,21 @@ async function extractTypeScript(filePath, displayPath) {
|
|
|
1746
1755
|
addModuleNode(ref);
|
|
1747
1756
|
registerRequireBinding(nameNode, ref);
|
|
1748
1757
|
}
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (isExported) {
|
|
1762
|
+
const name = declarator.childForFieldName("name")?.text;
|
|
1763
|
+
if (!name) continue;
|
|
1764
|
+
const id = entityId(sourceFile, name);
|
|
1765
|
+
builder.addNode({ id, label: `const ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
|
|
1766
|
+
builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
1767
|
+
nameIndex.set(name, id);
|
|
1768
|
+
if (core.type === "identifier") {
|
|
1769
|
+
const aliased = nameIndex.get(core.text);
|
|
1770
|
+
if (aliased !== void 0 && aliased !== id) {
|
|
1771
|
+
builder.addEdge({ source: id, target: aliased, relation: "references", confidence: "EXTRACTED" });
|
|
1772
|
+
}
|
|
1749
1773
|
}
|
|
1750
1774
|
}
|
|
1751
1775
|
}
|
|
@@ -2087,6 +2111,8 @@ function buildGraph(extractions) {
|
|
|
2087
2111
|
}
|
|
2088
2112
|
|
|
2089
2113
|
// src/resolve.ts
|
|
2114
|
+
var fs9 = __toESM(require("fs/promises"), 1);
|
|
2115
|
+
var path5 = __toESM(require("path"), 1);
|
|
2090
2116
|
var CODE_EXTENSIONS2 = [
|
|
2091
2117
|
".ts",
|
|
2092
2118
|
".tsx",
|
|
@@ -2106,6 +2132,29 @@ var CODE_EXTENSIONS2 = [
|
|
|
2106
2132
|
function isOpaque(id) {
|
|
2107
2133
|
return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
|
|
2108
2134
|
}
|
|
2135
|
+
var SELF_IMPORT_STEMS = (subpath) => subpath === "" ? ["index", "src/index", "lib/index", "source/index", "src/main"] : [
|
|
2136
|
+
subpath,
|
|
2137
|
+
`src/${subpath}`,
|
|
2138
|
+
`lib/${subpath}`,
|
|
2139
|
+
`source/${subpath}`,
|
|
2140
|
+
`${subpath}/index`,
|
|
2141
|
+
`src/${subpath}/index`,
|
|
2142
|
+
`lib/${subpath}/index`
|
|
2143
|
+
];
|
|
2144
|
+
function selfImportCandidates(spec, selfNames) {
|
|
2145
|
+
const selfName = selfNames.find((n) => spec === n || spec.startsWith(`${n}/`));
|
|
2146
|
+
if (selfName === void 0) return null;
|
|
2147
|
+
const subpath = spec === selfName ? "" : spec.slice(selfName.length + 1);
|
|
2148
|
+
return SELF_IMPORT_STEMS(subpath).flatMap((stem) => CODE_EXTENSIONS2.map((ext) => `${stem}${ext}`));
|
|
2149
|
+
}
|
|
2150
|
+
async function readSelfNames(root) {
|
|
2151
|
+
try {
|
|
2152
|
+
const pkg = JSON.parse(await fs9.readFile(path5.join(root, "package.json"), "utf-8"));
|
|
2153
|
+
return typeof pkg.name === "string" && pkg.name.length > 0 ? [pkg.name] : [];
|
|
2154
|
+
} catch {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2109
2158
|
function stripKnownExtension(p) {
|
|
2110
2159
|
for (const ext of CODE_EXTENSIONS2) {
|
|
2111
2160
|
if (p.endsWith(ext)) return p.slice(0, -ext.length);
|
|
@@ -2122,19 +2171,23 @@ function candidatePaths(guessed) {
|
|
|
2122
2171
|
}
|
|
2123
2172
|
return candidates.filter((c) => c !== guessed);
|
|
2124
2173
|
}
|
|
2125
|
-
function uniqueMatch(
|
|
2174
|
+
function uniqueMatch(candidates, real) {
|
|
2126
2175
|
const matches = /* @__PURE__ */ new Set();
|
|
2127
2176
|
for (const candidate of candidates) {
|
|
2128
|
-
if (
|
|
2177
|
+
if (real.has(candidate)) matches.add(candidate);
|
|
2129
2178
|
}
|
|
2130
|
-
|
|
2131
|
-
return [...matches][0];
|
|
2179
|
+
return matches.size === 1 ? [...matches][0] : null;
|
|
2132
2180
|
}
|
|
2133
|
-
function resolveCrossFileReferences(extractions) {
|
|
2181
|
+
function resolveCrossFileReferences(extractions, options = {}) {
|
|
2182
|
+
const selfNames = options.selfNames ?? [];
|
|
2134
2183
|
const realIds = /* @__PURE__ */ new Set();
|
|
2135
2184
|
const realFiles = /* @__PURE__ */ new Set();
|
|
2136
2185
|
for (const extraction of extractions) {
|
|
2137
2186
|
for (const node of extraction.nodes) realIds.add(node.id);
|
|
2187
|
+
const first = extraction.nodes[0];
|
|
2188
|
+
if (first && first.id === first.sourceFile && !isOpaque(first.id)) {
|
|
2189
|
+
realFiles.add(first.id);
|
|
2190
|
+
}
|
|
2138
2191
|
for (const node of extraction.nodes) {
|
|
2139
2192
|
const sep3 = node.id.indexOf("::");
|
|
2140
2193
|
if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
|
|
@@ -2144,7 +2197,24 @@ function resolveCrossFileReferences(extractions) {
|
|
|
2144
2197
|
}
|
|
2145
2198
|
}
|
|
2146
2199
|
const remap = /* @__PURE__ */ new Map();
|
|
2147
|
-
|
|
2200
|
+
let resolvedEndpoints = 0;
|
|
2201
|
+
const applyRemap = (resolveId) => {
|
|
2202
|
+
for (const extraction of extractions) {
|
|
2203
|
+
for (const edge of extraction.edges) {
|
|
2204
|
+
const source = resolveId(edge.source);
|
|
2205
|
+
if (source !== null) {
|
|
2206
|
+
edge.source = source;
|
|
2207
|
+
resolvedEndpoints++;
|
|
2208
|
+
}
|
|
2209
|
+
const target = resolveId(edge.target);
|
|
2210
|
+
if (target !== null) {
|
|
2211
|
+
edge.target = target;
|
|
2212
|
+
resolvedEndpoints++;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
const resolvePathId = (id) => {
|
|
2148
2218
|
if (isOpaque(id)) return null;
|
|
2149
2219
|
const cached = remap.get(id);
|
|
2150
2220
|
if (cached !== void 0) return cached;
|
|
@@ -2154,30 +2224,77 @@ function resolveCrossFileReferences(extractions) {
|
|
|
2154
2224
|
if (realIds.has(id)) return null;
|
|
2155
2225
|
const guessedPath = id.slice(0, sep3);
|
|
2156
2226
|
const name = id.slice(sep3);
|
|
2157
|
-
|
|
2158
|
-
resolved = uniqueMatch(id, candidates, realIds);
|
|
2227
|
+
resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
|
|
2159
2228
|
} else {
|
|
2160
2229
|
if (realFiles.has(id)) return null;
|
|
2161
|
-
resolved = uniqueMatch(
|
|
2230
|
+
resolved = uniqueMatch(candidatePaths(id), realFiles);
|
|
2162
2231
|
}
|
|
2163
2232
|
if (resolved !== null) remap.set(id, resolved);
|
|
2164
2233
|
return resolved;
|
|
2165
2234
|
};
|
|
2166
|
-
|
|
2235
|
+
applyRemap(resolvePathId);
|
|
2236
|
+
const namedAlias = /* @__PURE__ */ new Map();
|
|
2237
|
+
const starTargets = /* @__PURE__ */ new Map();
|
|
2167
2238
|
for (const extraction of extractions) {
|
|
2168
2239
|
for (const edge of extraction.edges) {
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
edge.
|
|
2177
|
-
|
|
2240
|
+
if (edge.relation !== "re_exports" || !realFiles.has(edge.source)) continue;
|
|
2241
|
+
const sep3 = edge.target.lastIndexOf("::");
|
|
2242
|
+
if (sep3 > 0 && realIds.has(edge.target)) {
|
|
2243
|
+
namedAlias.set(`${edge.source}|${edge.target.slice(sep3 + 2)}`, edge.target);
|
|
2244
|
+
} else if (sep3 < 0 && realFiles.has(edge.target)) {
|
|
2245
|
+
const targets = starTargets.get(edge.source) ?? [];
|
|
2246
|
+
targets.push(edge.target);
|
|
2247
|
+
starTargets.set(edge.source, targets);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
const throughBarrel = (file, name) => {
|
|
2252
|
+
const direct = `${file}::${name}`;
|
|
2253
|
+
if (realIds.has(direct)) return direct;
|
|
2254
|
+
const aliased = namedAlias.get(`${file}|${name}`);
|
|
2255
|
+
if (aliased !== void 0) return aliased;
|
|
2256
|
+
const viaStar = (starTargets.get(file) ?? []).map((sf) => `${sf}::${name}`).filter((id) => realIds.has(id));
|
|
2257
|
+
return viaStar.length === 1 ? viaStar[0] : null;
|
|
2258
|
+
};
|
|
2259
|
+
const resolveBarrelId = (id) => {
|
|
2260
|
+
const cached = remap.get(id);
|
|
2261
|
+
if (cached !== void 0) return cached;
|
|
2262
|
+
if (id.startsWith("module:")) {
|
|
2263
|
+
const spec = id.slice("module:".length);
|
|
2264
|
+
const sep4 = spec.indexOf("::");
|
|
2265
|
+
const moduleSpec = sep4 > 0 ? spec.slice(0, sep4) : spec;
|
|
2266
|
+
const name = sep4 > 0 ? spec.slice(sep4 + 2) : null;
|
|
2267
|
+
const candidates = selfImportCandidates(moduleSpec, selfNames);
|
|
2268
|
+
if (candidates === null) {
|
|
2269
|
+
if (name === null) return null;
|
|
2270
|
+
remap.set(id, `module:${moduleSpec}`);
|
|
2271
|
+
return `module:${moduleSpec}`;
|
|
2272
|
+
}
|
|
2273
|
+
const file = uniqueMatch(candidates, realFiles);
|
|
2274
|
+
let resolved = null;
|
|
2275
|
+
if (file !== null) {
|
|
2276
|
+
resolved = name !== null ? throughBarrel(file, name) ?? file : file;
|
|
2277
|
+
} else if (name !== null) {
|
|
2278
|
+
resolved = `module:${moduleSpec}`;
|
|
2279
|
+
}
|
|
2280
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
2281
|
+
return resolved;
|
|
2282
|
+
}
|
|
2283
|
+
const sep3 = id.indexOf("::");
|
|
2284
|
+
if (sep3 > 0 && !realIds.has(id)) {
|
|
2285
|
+
const guessedPath = id.slice(0, sep3);
|
|
2286
|
+
const name = id.slice(sep3 + 2);
|
|
2287
|
+
const files = [guessedPath, ...candidatePaths(guessedPath)].filter((f) => realFiles.has(f));
|
|
2288
|
+
const resolvedSet = new Set(files.map((f) => throughBarrel(f, name)).filter((r) => r !== null));
|
|
2289
|
+
if (resolvedSet.size === 1) {
|
|
2290
|
+
const resolved = [...resolvedSet][0];
|
|
2291
|
+
remap.set(id, resolved);
|
|
2292
|
+
return resolved;
|
|
2178
2293
|
}
|
|
2179
2294
|
}
|
|
2180
|
-
|
|
2295
|
+
return null;
|
|
2296
|
+
};
|
|
2297
|
+
applyRemap(resolveBarrelId);
|
|
2181
2298
|
let droppedPlaceholders = 0;
|
|
2182
2299
|
for (const extraction of extractions) {
|
|
2183
2300
|
const before = extraction.nodes.length;
|
|
@@ -2453,15 +2570,15 @@ function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
|
|
|
2453
2570
|
|
|
2454
2571
|
// src/export.ts
|
|
2455
2572
|
var import_node_module2 = require("module");
|
|
2456
|
-
var
|
|
2457
|
-
var
|
|
2573
|
+
var fs11 = __toESM(require("fs/promises"), 1);
|
|
2574
|
+
var path6 = __toESM(require("path"), 1);
|
|
2458
2575
|
|
|
2459
2576
|
// src/security.ts
|
|
2460
2577
|
var import_node_crypto2 = require("crypto");
|
|
2461
2578
|
var dns = __toESM(require("dns"), 1);
|
|
2462
2579
|
var http = __toESM(require("http"), 1);
|
|
2463
2580
|
var https = __toESM(require("https"), 1);
|
|
2464
|
-
var
|
|
2581
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
2465
2582
|
var net = __toESM(require("net"), 1);
|
|
2466
2583
|
var nodePath = __toESM(require("path"), 1);
|
|
2467
2584
|
var MAX_FETCH_BYTES = 50 * 1024 * 1024;
|
|
@@ -2497,25 +2614,25 @@ function validateDsn(dsn) {
|
|
|
2497
2614
|
}
|
|
2498
2615
|
};
|
|
2499
2616
|
}
|
|
2500
|
-
function validateGraphPath(
|
|
2617
|
+
function validateGraphPath(path12, base) {
|
|
2501
2618
|
const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
|
|
2502
2619
|
let resolvedBase;
|
|
2503
2620
|
try {
|
|
2504
|
-
resolvedBase =
|
|
2621
|
+
resolvedBase = fs10.realpathSync(baseDir);
|
|
2505
2622
|
} catch {
|
|
2506
2623
|
throw new Error(`Base directory does not exist: ${baseDir}`);
|
|
2507
2624
|
}
|
|
2508
|
-
const candidate = nodePath.isAbsolute(
|
|
2625
|
+
const candidate = nodePath.isAbsolute(path12) ? path12 : nodePath.join(resolvedBase, path12);
|
|
2509
2626
|
const resolvedCandidate = nodePath.resolve(candidate);
|
|
2510
2627
|
let realCandidate = resolvedCandidate;
|
|
2511
2628
|
try {
|
|
2512
|
-
realCandidate =
|
|
2629
|
+
realCandidate = fs10.realpathSync(resolvedCandidate);
|
|
2513
2630
|
} catch {
|
|
2514
2631
|
}
|
|
2515
2632
|
const relative4 = nodePath.relative(resolvedBase, realCandidate);
|
|
2516
2633
|
const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
|
|
2517
2634
|
if (escapes) {
|
|
2518
|
-
throw new Error(`Path escapes graphify-out/: ${
|
|
2635
|
+
throw new Error(`Path escapes graphify-out/: ${path12}`);
|
|
2519
2636
|
}
|
|
2520
2637
|
return realCandidate;
|
|
2521
2638
|
}
|
|
@@ -2532,10 +2649,10 @@ function escapeHtml(text) {
|
|
|
2532
2649
|
var require3 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
2533
2650
|
function resolveVisNetworkAssets() {
|
|
2534
2651
|
const packageJsonPath = require3.resolve("vis-network/package.json");
|
|
2535
|
-
const root =
|
|
2652
|
+
const root = path6.dirname(packageJsonPath);
|
|
2536
2653
|
return {
|
|
2537
|
-
jsPath:
|
|
2538
|
-
cssPath:
|
|
2654
|
+
jsPath: path6.join(root, "standalone", "umd", "vis-network.min.js"),
|
|
2655
|
+
cssPath: path6.join(root, "styles", "vis-network.min.css")
|
|
2539
2656
|
};
|
|
2540
2657
|
}
|
|
2541
2658
|
var COMMUNITY_PALETTE = [
|
|
@@ -2594,8 +2711,8 @@ community: ${communityLabel}` : ""}`,
|
|
|
2594
2711
|
async function renderHtml(graph) {
|
|
2595
2712
|
const { jsPath, cssPath } = resolveVisNetworkAssets();
|
|
2596
2713
|
const [visJs, visCss] = await Promise.all([
|
|
2597
|
-
|
|
2598
|
-
|
|
2714
|
+
fs11.readFile(jsPath, "utf-8"),
|
|
2715
|
+
fs11.readFile(cssPath, "utf-8")
|
|
2599
2716
|
]);
|
|
2600
2717
|
const { nodes, edges } = buildVisNodesAndEdges(graph);
|
|
2601
2718
|
const dataJson = safeInlineJson({ nodes, edges });
|
|
@@ -2635,17 +2752,17 @@ ${visJs}
|
|
|
2635
2752
|
`;
|
|
2636
2753
|
}
|
|
2637
2754
|
async function exportGraph(graph, options, report) {
|
|
2638
|
-
const outDir =
|
|
2639
|
-
await
|
|
2755
|
+
const outDir = path6.resolve(options.outDir);
|
|
2756
|
+
await fs11.mkdir(outDir, { recursive: true });
|
|
2640
2757
|
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2641
|
-
await
|
|
2758
|
+
await fs11.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
|
|
2642
2759
|
if (options.html !== false) {
|
|
2643
2760
|
const htmlPath = validateGraphPath("graph.html", outDir);
|
|
2644
|
-
await
|
|
2761
|
+
await fs11.writeFile(htmlPath, await renderHtml(graph), "utf-8");
|
|
2645
2762
|
}
|
|
2646
2763
|
if (report !== void 0) {
|
|
2647
2764
|
const reportPath = validateGraphPath("GRAPH_REPORT.md", outDir);
|
|
2648
|
-
await
|
|
2765
|
+
await fs11.writeFile(reportPath, report, "utf-8");
|
|
2649
2766
|
}
|
|
2650
2767
|
const notImplemented = [];
|
|
2651
2768
|
if (options.svg) notImplemented.push("--svg");
|
|
@@ -2658,24 +2775,24 @@ async function exportGraph(graph, options, report) {
|
|
|
2658
2775
|
}
|
|
2659
2776
|
|
|
2660
2777
|
// src/pipeline.ts
|
|
2661
|
-
var
|
|
2662
|
-
var
|
|
2778
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
2779
|
+
var path8 = __toESM(require("path"), 1);
|
|
2663
2780
|
|
|
2664
2781
|
// src/extractionCache.ts
|
|
2665
2782
|
var import_node_crypto3 = require("crypto");
|
|
2666
|
-
var
|
|
2667
|
-
var
|
|
2783
|
+
var fs12 = __toESM(require("fs/promises"), 1);
|
|
2784
|
+
var path7 = __toESM(require("path"), 1);
|
|
2668
2785
|
var ExtractionCache = class {
|
|
2669
2786
|
dir;
|
|
2670
2787
|
constructor(outDir) {
|
|
2671
|
-
this.dir =
|
|
2788
|
+
this.dir = path7.join(outDir, "cache");
|
|
2672
2789
|
}
|
|
2673
2790
|
key(relFile, content) {
|
|
2674
2791
|
return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
2675
2792
|
}
|
|
2676
2793
|
async get(key) {
|
|
2677
2794
|
try {
|
|
2678
|
-
const raw = await
|
|
2795
|
+
const raw = await fs12.readFile(path7.join(this.dir, `${key}.json`), "utf-8");
|
|
2679
2796
|
const parsed = JSON.parse(raw);
|
|
2680
2797
|
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
|
|
2681
2798
|
return parsed;
|
|
@@ -2684,8 +2801,8 @@ var ExtractionCache = class {
|
|
|
2684
2801
|
}
|
|
2685
2802
|
}
|
|
2686
2803
|
async put(key, extraction) {
|
|
2687
|
-
await
|
|
2688
|
-
await
|
|
2804
|
+
await fs12.mkdir(this.dir, { recursive: true });
|
|
2805
|
+
await fs12.writeFile(path7.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
|
|
2689
2806
|
}
|
|
2690
2807
|
};
|
|
2691
2808
|
|
|
@@ -2693,21 +2810,21 @@ var ExtractionCache = class {
|
|
|
2693
2810
|
async function runPipeline(root, options = {}) {
|
|
2694
2811
|
const progress = options.onProgress ?? (() => {
|
|
2695
2812
|
});
|
|
2696
|
-
const resolvedRoot =
|
|
2813
|
+
const resolvedRoot = path8.resolve(root);
|
|
2697
2814
|
progress(`Scanning ${resolvedRoot} ...`);
|
|
2698
2815
|
const manifest = collectFiles(resolvedRoot);
|
|
2699
2816
|
progress(
|
|
2700
2817
|
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
2701
2818
|
);
|
|
2702
|
-
const outDir = options.outDir ??
|
|
2819
|
+
const outDir = options.outDir ?? path8.join(resolvedRoot, "graphify-out");
|
|
2703
2820
|
const cache = new ExtractionCache(outDir);
|
|
2704
2821
|
progress("Extracting...");
|
|
2705
2822
|
const extractions = [];
|
|
2706
2823
|
let cacheHits = 0;
|
|
2707
2824
|
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
2708
|
-
const filePath =
|
|
2825
|
+
const filePath = path8.relative(process.cwd(), path8.join(resolvedRoot, relFile)) || relFile;
|
|
2709
2826
|
try {
|
|
2710
|
-
const key = cache.key(relFile, await
|
|
2827
|
+
const key = cache.key(relFile, await fs13.readFile(path8.join(resolvedRoot, relFile)));
|
|
2711
2828
|
let extraction = options.update ? await cache.get(key) : null;
|
|
2712
2829
|
if (extraction) {
|
|
2713
2830
|
cacheHits++;
|
|
@@ -2728,7 +2845,9 @@ async function runPipeline(root, options = {}) {
|
|
|
2728
2845
|
extractions.push(...options.extraExtractions);
|
|
2729
2846
|
}
|
|
2730
2847
|
progress("Resolving cross-file references...");
|
|
2731
|
-
const resolveStats = resolveCrossFileReferences(extractions
|
|
2848
|
+
const resolveStats = resolveCrossFileReferences(extractions, {
|
|
2849
|
+
selfNames: await readSelfNames(resolvedRoot)
|
|
2850
|
+
});
|
|
2732
2851
|
if (resolveStats.resolvedEndpoints > 0) {
|
|
2733
2852
|
progress(
|
|
2734
2853
|
` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
|
|
@@ -2760,12 +2879,12 @@ async function runPipeline(root, options = {}) {
|
|
|
2760
2879
|
}
|
|
2761
2880
|
|
|
2762
2881
|
// src/graphStore.ts
|
|
2763
|
-
var
|
|
2764
|
-
var
|
|
2882
|
+
var fs14 = __toESM(require("fs/promises"), 1);
|
|
2883
|
+
var path9 = __toESM(require("path"), 1);
|
|
2765
2884
|
var import_graphology3 = __toESM(require("graphology"), 1);
|
|
2766
|
-
async function loadGraph(outDir =
|
|
2885
|
+
async function loadGraph(outDir = path9.join(process.cwd(), "graphify-out")) {
|
|
2767
2886
|
const jsonPath = validateGraphPath("graph.json", outDir);
|
|
2768
|
-
const raw = await
|
|
2887
|
+
const raw = await fs14.readFile(jsonPath, "utf-8");
|
|
2769
2888
|
const data = JSON.parse(raw);
|
|
2770
2889
|
return import_graphology3.default.from(data);
|
|
2771
2890
|
}
|
|
@@ -2873,7 +2992,10 @@ function scoreNodes(graph, query) {
|
|
|
2873
2992
|
}
|
|
2874
2993
|
if (score > 0) matches.push({ id, label, score });
|
|
2875
2994
|
});
|
|
2876
|
-
|
|
2995
|
+
const placeholderRank = (id) => id.startsWith("module:") || id.startsWith("external:") ? 1 : 0;
|
|
2996
|
+
matches.sort(
|
|
2997
|
+
(a, b) => b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id)
|
|
2998
|
+
);
|
|
2877
2999
|
return matches;
|
|
2878
3000
|
}
|
|
2879
3001
|
function resolveNode(graph, query) {
|
|
@@ -2989,25 +3111,25 @@ function shortestPath(graph, fromQuery, toQuery) {
|
|
|
2989
3111
|
if (!visited.has(to.id)) {
|
|
2990
3112
|
return { from, to, found: false, path: [], edges: [] };
|
|
2991
3113
|
}
|
|
2992
|
-
const
|
|
3114
|
+
const path12 = [to.id];
|
|
2993
3115
|
let cursor = to.id;
|
|
2994
3116
|
while (cursor !== from.id) {
|
|
2995
3117
|
const prev = predecessor.get(cursor);
|
|
2996
3118
|
if (!prev) break;
|
|
2997
|
-
|
|
3119
|
+
path12.unshift(prev);
|
|
2998
3120
|
cursor = prev;
|
|
2999
3121
|
}
|
|
3000
3122
|
const edges = [];
|
|
3001
|
-
for (let i = 0; i <
|
|
3002
|
-
const a =
|
|
3003
|
-
const b =
|
|
3123
|
+
for (let i = 0; i < path12.length - 1; i++) {
|
|
3124
|
+
const a = path12[i];
|
|
3125
|
+
const b = path12[i + 1];
|
|
3004
3126
|
const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
|
|
3005
3127
|
if (key) {
|
|
3006
3128
|
const attrs = graph.getEdgeAttributes(key);
|
|
3007
3129
|
edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
|
|
3008
3130
|
}
|
|
3009
3131
|
}
|
|
3010
|
-
return { from, to, found: true, path:
|
|
3132
|
+
return { from, to, found: true, path: path12, edges };
|
|
3011
3133
|
}
|
|
3012
3134
|
function explainNode(graph, query) {
|
|
3013
3135
|
const match = resolveNode(graph, query);
|
|
@@ -3056,6 +3178,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
3056
3178
|
if (!target) return null;
|
|
3057
3179
|
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
3058
3180
|
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
3181
|
+
const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
|
|
3059
3182
|
const affected = [];
|
|
3060
3183
|
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
3061
3184
|
let frontier = [target.id];
|
|
@@ -3066,7 +3189,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
|
|
|
3066
3189
|
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
3067
3190
|
if (visited.has(source)) return;
|
|
3068
3191
|
const relation = edgeAttrs.relation;
|
|
3069
|
-
if (!
|
|
3192
|
+
if (!followed.has(relation)) return;
|
|
3070
3193
|
const confidence = edgeAttrs.confidence;
|
|
3071
3194
|
const existing = nextFrontier.get(source);
|
|
3072
3195
|
if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
|
|
@@ -3302,8 +3425,8 @@ async function addViewEdges(query, schema, viewNames, tableNames, edges) {
|
|
|
3302
3425
|
|
|
3303
3426
|
// src/memory.ts
|
|
3304
3427
|
var import_node_crypto4 = require("crypto");
|
|
3305
|
-
var
|
|
3306
|
-
var
|
|
3428
|
+
var fs15 = __toESM(require("fs/promises"), 1);
|
|
3429
|
+
var path10 = __toESM(require("path"), 1);
|
|
3307
3430
|
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
3308
3431
|
var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
|
|
3309
3432
|
function validateResult(value) {
|
|
@@ -3321,25 +3444,25 @@ function validateResult(value) {
|
|
|
3321
3444
|
}
|
|
3322
3445
|
async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
3323
3446
|
validateResult(entry);
|
|
3324
|
-
await
|
|
3447
|
+
await fs15.mkdir(memoryDir, { recursive: true });
|
|
3325
3448
|
const saved = { ...entry, savedAt: now.toISOString() };
|
|
3326
3449
|
const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
3327
3450
|
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
3328
|
-
const filePath =
|
|
3329
|
-
await
|
|
3451
|
+
const filePath = path10.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
3452
|
+
await fs15.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
3330
3453
|
return filePath;
|
|
3331
3454
|
}
|
|
3332
3455
|
async function loadResults(memoryDir) {
|
|
3333
3456
|
let files;
|
|
3334
3457
|
try {
|
|
3335
|
-
files = (await
|
|
3458
|
+
files = (await fs15.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
|
|
3336
3459
|
} catch {
|
|
3337
3460
|
return [];
|
|
3338
3461
|
}
|
|
3339
3462
|
const results = [];
|
|
3340
3463
|
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
3341
3464
|
try {
|
|
3342
|
-
const parsed = JSON.parse(await
|
|
3465
|
+
const parsed = JSON.parse(await fs15.readFile(path10.join(memoryDir, file), "utf-8"));
|
|
3343
3466
|
if (parsed.question && parsed.savedAt) results.push(parsed);
|
|
3344
3467
|
} catch {
|
|
3345
3468
|
}
|
|
@@ -3452,7 +3575,7 @@ function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
|
|
|
3452
3575
|
const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
|
|
3453
3576
|
return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
|
|
3454
3577
|
}
|
|
3455
|
-
async function buildContextPack(graph, task,
|
|
3578
|
+
async function buildContextPack(graph, task, readFile14, options = {}) {
|
|
3456
3579
|
const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
3457
3580
|
const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
|
|
3458
3581
|
const ranked = rankForTask(graph, task, options.maxSeeds);
|
|
@@ -3473,7 +3596,7 @@ async function buildContextPack(graph, task, readFile13, options = {}) {
|
|
|
3473
3596
|
if (cached !== void 0) return cached;
|
|
3474
3597
|
let lines;
|
|
3475
3598
|
try {
|
|
3476
|
-
lines = (await
|
|
3599
|
+
lines = (await readFile14(file)).split("\n");
|
|
3477
3600
|
} catch {
|
|
3478
3601
|
lines = null;
|
|
3479
3602
|
}
|
|
@@ -3561,16 +3684,17 @@ var TEST_FILE_PATTERNS = [
|
|
|
3561
3684
|
/_spec\.rb$/,
|
|
3562
3685
|
/_test\.rb$/
|
|
3563
3686
|
];
|
|
3564
|
-
function isTestFile(
|
|
3565
|
-
return TEST_FILE_PATTERNS.some((p) => p.test(
|
|
3687
|
+
function isTestFile(path12) {
|
|
3688
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path12));
|
|
3566
3689
|
}
|
|
3567
3690
|
var TEST_REACH_DEPTH = 4;
|
|
3568
3691
|
var TEST_REACH_LIMIT = 2e3;
|
|
3569
|
-
|
|
3692
|
+
var USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
|
|
3693
|
+
function selectionFromImpact(graph, targetIds, targetLabel, relations) {
|
|
3570
3694
|
const best = /* @__PURE__ */ new Map();
|
|
3571
3695
|
let affectedNodeCount = 0;
|
|
3572
3696
|
for (const id of targetIds) {
|
|
3573
|
-
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
3697
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });
|
|
3574
3698
|
if (!impact) continue;
|
|
3575
3699
|
affectedNodeCount += impact.affected.length;
|
|
3576
3700
|
for (const node of impact.affected) {
|
|
@@ -3582,11 +3706,27 @@ function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
|
3582
3706
|
}
|
|
3583
3707
|
}
|
|
3584
3708
|
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 {
|
|
3709
|
+
return {
|
|
3710
|
+
target: targetLabel,
|
|
3711
|
+
testFiles,
|
|
3712
|
+
affectedNodeCount,
|
|
3713
|
+
precision: relations ? "usage" : "import-reachability"
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
function entityRootsOf(graph, id) {
|
|
3717
|
+
if (id.includes("::")) return [id];
|
|
3718
|
+
const roots = [];
|
|
3719
|
+
graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {
|
|
3720
|
+
if (String(attrs.relation) === "contains") roots.push(target);
|
|
3721
|
+
});
|
|
3722
|
+
roots.sort((a, b) => a.localeCompare(b));
|
|
3723
|
+
return roots.length > 0 ? roots : [id];
|
|
3586
3724
|
}
|
|
3587
3725
|
function testsForNode(graph, nodeQuery) {
|
|
3588
3726
|
const match = resolveNode(graph, nodeQuery);
|
|
3589
3727
|
if (!match) return null;
|
|
3728
|
+
const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);
|
|
3729
|
+
if (precise.testFiles.length > 0) return precise;
|
|
3590
3730
|
return selectionFromImpact(graph, [match.id], match.id);
|
|
3591
3731
|
}
|
|
3592
3732
|
function testsForChangedFiles(graph, changedFiles) {
|
|
@@ -3599,7 +3739,12 @@ function testsForChangedFiles(graph, changedFiles) {
|
|
|
3599
3739
|
}
|
|
3600
3740
|
if (graph.hasNode(file)) fileIds.push(file);
|
|
3601
3741
|
}
|
|
3602
|
-
const
|
|
3742
|
+
const label = changedFiles.join(", ");
|
|
3743
|
+
const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));
|
|
3744
|
+
let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);
|
|
3745
|
+
if (selection.testFiles.length === 0) {
|
|
3746
|
+
selection = selectionFromImpact(graph, fileIds, label);
|
|
3747
|
+
}
|
|
3603
3748
|
for (const [file, info] of selfSelected) {
|
|
3604
3749
|
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
3605
3750
|
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
@@ -3611,22 +3756,22 @@ function testsForChangedFiles(graph, changedFiles) {
|
|
|
3611
3756
|
|
|
3612
3757
|
// src/diff.ts
|
|
3613
3758
|
var import_node_child_process = require("child_process");
|
|
3614
|
-
var
|
|
3759
|
+
var fs16 = __toESM(require("fs/promises"), 1);
|
|
3615
3760
|
var os = __toESM(require("os"), 1);
|
|
3616
|
-
var
|
|
3761
|
+
var path11 = __toESM(require("path"), 1);
|
|
3617
3762
|
var import_node_util = require("util");
|
|
3618
3763
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
3619
3764
|
async function graphForDirectory(root) {
|
|
3620
3765
|
const manifest = collectFiles(root);
|
|
3621
3766
|
const extractions = [];
|
|
3622
3767
|
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
3623
|
-
extractions.push(await extract(
|
|
3768
|
+
extractions.push(await extract(path11.join(root, relFile), relFile));
|
|
3624
3769
|
}
|
|
3625
|
-
resolveCrossFileReferences(extractions);
|
|
3770
|
+
resolveCrossFileReferences(extractions, { selfNames: await readSelfNames(root) });
|
|
3626
3771
|
return buildGraph(extractions);
|
|
3627
3772
|
}
|
|
3628
3773
|
async function checkoutRev(repoRoot, rev) {
|
|
3629
|
-
const dest = await
|
|
3774
|
+
const dest = await fs16.mkdtemp(path11.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
|
|
3630
3775
|
const { stdout } = await execFileAsync(
|
|
3631
3776
|
"git",
|
|
3632
3777
|
["-C", repoRoot, "archive", "--format=tar", rev],
|
|
@@ -3694,8 +3839,8 @@ async function reviewRevisions(repoRoot, base, head = "HEAD") {
|
|
|
3694
3839
|
return diffGraphs(baseGraph, headGraph, base, head);
|
|
3695
3840
|
} finally {
|
|
3696
3841
|
await Promise.all([
|
|
3697
|
-
|
|
3698
|
-
|
|
3842
|
+
fs16.rm(baseDir, { recursive: true, force: true }),
|
|
3843
|
+
fs16.rm(headDir, { recursive: true, force: true })
|
|
3699
3844
|
]);
|
|
3700
3845
|
}
|
|
3701
3846
|
}
|