@dreamtree-org/graphify 1.1.2 → 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
@@ -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
  }
@@ -2147,13 +2171,12 @@ function candidatePaths(guessed) {
2147
2171
  }
2148
2172
  return candidates.filter((c) => c !== guessed);
2149
2173
  }
2150
- function uniqueMatch(guessed, candidates, realIds) {
2174
+ function uniqueMatch(candidates, real) {
2151
2175
  const matches = /* @__PURE__ */ new Set();
2152
2176
  for (const candidate of candidates) {
2153
- if (realIds.has(candidate)) matches.add(candidate);
2177
+ if (real.has(candidate)) matches.add(candidate);
2154
2178
  }
2155
- if (matches.size !== 1) return null;
2156
- return [...matches][0];
2179
+ return matches.size === 1 ? [...matches][0] : null;
2157
2180
  }
2158
2181
  function resolveCrossFileReferences(extractions, options = {}) {
2159
2182
  const selfNames = options.selfNames ?? [];
@@ -2174,20 +2197,24 @@ function resolveCrossFileReferences(extractions, options = {}) {
2174
2197
  }
2175
2198
  }
2176
2199
  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;
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++;
2187
2213
  }
2188
2214
  }
2189
- return null;
2190
2215
  }
2216
+ };
2217
+ const resolvePathId = (id) => {
2191
2218
  if (isOpaque(id)) return null;
2192
2219
  const cached = remap.get(id);
2193
2220
  if (cached !== void 0) return cached;
@@ -2197,30 +2224,77 @@ function resolveCrossFileReferences(extractions, options = {}) {
2197
2224
  if (realIds.has(id)) return null;
2198
2225
  const guessedPath = id.slice(0, sep3);
2199
2226
  const name = id.slice(sep3);
2200
- const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
2201
- resolved = uniqueMatch(id, candidates, realIds);
2227
+ resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
2202
2228
  } else {
2203
2229
  if (realFiles.has(id)) return null;
2204
- resolved = uniqueMatch(id, candidatePaths(id), realFiles);
2230
+ resolved = uniqueMatch(candidatePaths(id), realFiles);
2205
2231
  }
2206
2232
  if (resolved !== null) remap.set(id, resolved);
2207
2233
  return resolved;
2208
2234
  };
2209
- let resolvedEndpoints = 0;
2235
+ applyRemap(resolvePathId);
2236
+ const namedAlias = /* @__PURE__ */ new Map();
2237
+ const starTargets = /* @__PURE__ */ new Map();
2210
2238
  for (const extraction of extractions) {
2211
2239
  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++;
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;
2221
2293
  }
2222
2294
  }
2223
- }
2295
+ return null;
2296
+ };
2297
+ applyRemap(resolveBarrelId);
2224
2298
  let droppedPlaceholders = 0;
2225
2299
  for (const extraction of extractions) {
2226
2300
  const before = extraction.nodes.length;
@@ -2918,7 +2992,10 @@ function scoreNodes(graph, query) {
2918
2992
  }
2919
2993
  if (score > 0) matches.push({ id, label, score });
2920
2994
  });
2921
- 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
+ );
2922
2999
  return matches;
2923
3000
  }
2924
3001
  function resolveNode(graph, query) {
@@ -3101,6 +3178,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
3101
3178
  if (!target) return null;
3102
3179
  const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
3103
3180
  const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
3181
+ const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
3104
3182
  const affected = [];
3105
3183
  const visited = /* @__PURE__ */ new Set([target.id]);
3106
3184
  let frontier = [target.id];
@@ -3111,7 +3189,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
3111
3189
  graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
3112
3190
  if (visited.has(source)) return;
3113
3191
  const relation = edgeAttrs.relation;
3114
- if (!DEPENDENCY_RELATIONS.has(relation)) return;
3192
+ if (!followed.has(relation)) return;
3115
3193
  const confidence = edgeAttrs.confidence;
3116
3194
  const existing = nextFrontier.get(source);
3117
3195
  if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
@@ -3611,11 +3689,12 @@ function isTestFile(path12) {
3611
3689
  }
3612
3690
  var TEST_REACH_DEPTH = 4;
3613
3691
  var TEST_REACH_LIMIT = 2e3;
3614
- function selectionFromImpact(graph, targetIds, targetLabel) {
3692
+ var USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
3693
+ function selectionFromImpact(graph, targetIds, targetLabel, relations) {
3615
3694
  const best = /* @__PURE__ */ new Map();
3616
3695
  let affectedNodeCount = 0;
3617
3696
  for (const id of targetIds) {
3618
- 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 });
3619
3698
  if (!impact) continue;
3620
3699
  affectedNodeCount += impact.affected.length;
3621
3700
  for (const node of impact.affected) {
@@ -3627,11 +3706,27 @@ function selectionFromImpact(graph, targetIds, targetLabel) {
3627
3706
  }
3628
3707
  }
3629
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));
3630
- 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];
3631
3724
  }
3632
3725
  function testsForNode(graph, nodeQuery) {
3633
3726
  const match = resolveNode(graph, nodeQuery);
3634
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;
3635
3730
  return selectionFromImpact(graph, [match.id], match.id);
3636
3731
  }
3637
3732
  function testsForChangedFiles(graph, changedFiles) {
@@ -3644,7 +3739,12 @@ function testsForChangedFiles(graph, changedFiles) {
3644
3739
  }
3645
3740
  if (graph.hasNode(file)) fileIds.push(file);
3646
3741
  }
3647
- 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
+ }
3648
3748
  for (const [file, info] of selfSelected) {
3649
3749
  if (!selection.testFiles.some((t) => t.file === file)) {
3650
3750
  selection.testFiles.push({ file, depth: info.depth, via: info.via });