@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/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 path11 = fn.childForFieldName("path")?.text;
1606
+ const path12 = fn.childForFieldName("path")?.text;
1607
1607
  const name = fn.childForFieldName("name")?.text;
1608
- if (path11 && name) {
1609
- const viaImport = importIndex.get(path11);
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 (!binding.ref.external) {
1681
- if (binding.kind === "named" && binding.importedName) {
1682
- return entityId(binding.ref.id, binding.importedName);
1683
- }
1684
- if (binding.kind === "namespace" && property) {
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
- if (value.type === "arrow_function" || value.type === "function_expression") {
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(value.id, id);
1745
+ functionNodeMap.set(core.id, id);
1737
1746
  continue;
1738
1747
  }
1739
- if (value.type === "call_expression") {
1740
- const callee = value.childForFieldName("function");
1741
- const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(value) : null;
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(guessed, candidates, realIds) {
2174
+ function uniqueMatch(candidates, real) {
2126
2175
  const matches = /* @__PURE__ */ new Set();
2127
2176
  for (const candidate of candidates) {
2128
- if (realIds.has(candidate)) matches.add(candidate);
2177
+ if (real.has(candidate)) matches.add(candidate);
2129
2178
  }
2130
- if (matches.size !== 1) return null;
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
- const resolveId = (id) => {
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
- const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
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(id, candidatePaths(id), realFiles);
2230
+ resolved = uniqueMatch(candidatePaths(id), realFiles);
2162
2231
  }
2163
2232
  if (resolved !== null) remap.set(id, resolved);
2164
2233
  return resolved;
2165
2234
  };
2166
- let resolvedEndpoints = 0;
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
- const source = resolveId(edge.source);
2170
- if (source !== null) {
2171
- edge.source = source;
2172
- resolvedEndpoints++;
2173
- }
2174
- const target = resolveId(edge.target);
2175
- if (target !== null) {
2176
- edge.target = target;
2177
- resolvedEndpoints++;
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 fs10 = __toESM(require("fs/promises"), 1);
2457
- var path5 = __toESM(require("path"), 1);
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 fs9 = __toESM(require("fs"), 1);
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(path11, base) {
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 = fs9.realpathSync(baseDir);
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(path11) ? path11 : nodePath.join(resolvedBase, path11);
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 = fs9.realpathSync(resolvedCandidate);
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/: ${path11}`);
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 = path5.dirname(packageJsonPath);
2652
+ const root = path6.dirname(packageJsonPath);
2536
2653
  return {
2537
- jsPath: path5.join(root, "standalone", "umd", "vis-network.min.js"),
2538
- cssPath: path5.join(root, "styles", "vis-network.min.css")
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
- fs10.readFile(jsPath, "utf-8"),
2598
- fs10.readFile(cssPath, "utf-8")
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 = path5.resolve(options.outDir);
2639
- await fs10.mkdir(outDir, { recursive: true });
2755
+ const outDir = path6.resolve(options.outDir);
2756
+ await fs11.mkdir(outDir, { recursive: true });
2640
2757
  const jsonPath = validateGraphPath("graph.json", outDir);
2641
- await fs10.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
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 fs10.writeFile(htmlPath, await renderHtml(graph), "utf-8");
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 fs10.writeFile(reportPath, report, "utf-8");
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 fs12 = __toESM(require("fs/promises"), 1);
2662
- var path7 = __toESM(require("path"), 1);
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 fs11 = __toESM(require("fs/promises"), 1);
2667
- var path6 = __toESM(require("path"), 1);
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 = path6.join(outDir, "cache");
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 fs11.readFile(path6.join(this.dir, `${key}.json`), "utf-8");
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 fs11.mkdir(this.dir, { recursive: true });
2688
- await fs11.writeFile(path6.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
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 = path7.resolve(root);
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 ?? path7.join(resolvedRoot, "graphify-out");
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 = path7.relative(process.cwd(), path7.join(resolvedRoot, relFile)) || relFile;
2825
+ const filePath = path8.relative(process.cwd(), path8.join(resolvedRoot, relFile)) || relFile;
2709
2826
  try {
2710
- const key = cache.key(relFile, await fs12.readFile(path7.join(resolvedRoot, relFile)));
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 fs13 = __toESM(require("fs/promises"), 1);
2764
- var path8 = __toESM(require("path"), 1);
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 = path8.join(process.cwd(), "graphify-out")) {
2885
+ async function loadGraph(outDir = path9.join(process.cwd(), "graphify-out")) {
2767
2886
  const jsonPath = validateGraphPath("graph.json", outDir);
2768
- const raw = await fs13.readFile(jsonPath, "utf-8");
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
- matches.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
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 path11 = [to.id];
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
- path11.unshift(prev);
3119
+ path12.unshift(prev);
2998
3120
  cursor = prev;
2999
3121
  }
3000
3122
  const edges = [];
3001
- for (let i = 0; i < path11.length - 1; i++) {
3002
- const a = path11[i];
3003
- const b = path11[i + 1];
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: path11, edges };
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 (!DEPENDENCY_RELATIONS.has(relation)) return;
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 fs14 = __toESM(require("fs/promises"), 1);
3306
- var path9 = __toESM(require("path"), 1);
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 fs14.mkdir(memoryDir, { recursive: true });
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 = path9.join(memoryDir, `result-${stamp}-${hash}.json`);
3329
- await fs14.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
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 fs14.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
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 fs14.readFile(path9.join(memoryDir, file), "utf-8"));
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, readFile13, options = {}) {
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 readFile13(file)).split("\n");
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(path11) {
3565
- return TEST_FILE_PATTERNS.some((p) => p.test(path11));
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
- function selectionFromImpact(graph, targetIds, targetLabel) {
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 { target: targetLabel, testFiles, affectedNodeCount };
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 selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
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 fs15 = __toESM(require("fs/promises"), 1);
3759
+ var fs16 = __toESM(require("fs/promises"), 1);
3615
3760
  var os = __toESM(require("os"), 1);
3616
- var path10 = __toESM(require("path"), 1);
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(path10.join(root, relFile), relFile));
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 fs15.mkdtemp(path10.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
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
- fs15.rm(baseDir, { recursive: true, force: true }),
3698
- fs15.rm(headDir, { recursive: true, force: true })
3842
+ fs16.rm(baseDir, { recursive: true, force: true }),
3843
+ fs16.rm(headDir, { recursive: true, force: true })
3699
3844
  ]);
3700
3845
  }
3701
3846
  }