interlinked-mapper 0.3.10__tar.gz → 0.3.11__tar.gz

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.
Files changed (47) hide show
  1. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/PKG-INFO +1 -1
  2. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/graph.py +34 -20
  3. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/parser.py +12 -1
  4. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/PKG-INFO +1 -1
  5. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/pyproject.toml +1 -1
  6. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/tests/test_accuracy.py +85 -0
  7. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/__init__.py +0 -0
  8. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/__init__.py +0 -0
  9. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/dead_code.py +0 -0
  10. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/embeddings.py +0 -0
  11. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/similarity.py +0 -0
  12. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/cli.py +0 -0
  13. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/__init__.py +0 -0
  14. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/llm.py +0 -0
  15. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/query.py +0 -0
  16. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/repl.py +0 -0
  17. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/mcp_server.py +0 -0
  18. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/models.py +0 -0
  19. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/__init__.py +0 -0
  20. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
  21. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
  22. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/index.html +0 -0
  23. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html +0 -0
  24. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
  25. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package-lock.json +0 -0
  26. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package.json +0 -0
  27. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/App.tsx +0 -0
  28. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
  29. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
  30. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/index.css +0 -0
  31. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/main.tsx +0 -0
  32. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
  33. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
  34. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/theme.ts +0 -0
  35. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/types.ts +0 -0
  36. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
  37. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/tsconfig.json +0 -0
  38. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/vite.config.ts +0 -0
  39. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/layouts.py +0 -0
  40. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/server.py +0 -0
  41. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/SOURCES.txt +0 -0
  42. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
  43. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/entry_points.txt +0 -0
  44. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/requires.txt +0 -0
  45. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/top_level.txt +0 -0
  46. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/setup.cfg +0 -0
  47. {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/tests/test_query_completeness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.10
3
+ Version: 0.3.11
4
4
  Summary: A Python program topology explorer — visualize the shape of your codebase
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/austerecryptid/interlinked
@@ -15,6 +15,7 @@ from interlinked.models import (
15
15
  # Method names so common on builtins (dict, list, set, str, etc.) that resolving
16
16
  # them to project symbols by bare-name matching is almost always a false positive.
17
17
  # e.g. `op_dict.items()` should NOT resolve to `ActionCost.items`.
18
+ # Checked against the LAST component of dotted targets (e.g. "effect.get" → "get").
18
19
  _BUILTIN_METHOD_NAMES: frozenset[str] = frozenset({
19
20
  # dict
20
21
  "items", "keys", "values", "get", "pop", "update", "setdefault",
@@ -28,8 +29,9 @@ _BUILTIN_METHOD_NAMES: frozenset[str] = frozenset({
28
29
  # str
29
30
  "strip", "split", "join", "replace", "startswith", "endswith",
30
31
  "lower", "upper", "format", "encode", "decode",
31
- # general
32
+ # general / logging
32
33
  "close", "read", "write", "flush", "seek", "tell",
34
+ "warning", "error", "info", "debug", "exception", "critical",
33
35
  })
34
36
 
35
37
 
@@ -236,36 +238,48 @@ class CodeGraph:
236
238
  source = next(iter(candidates))
237
239
 
238
240
  if target not in node_ids:
239
- # Never resolve bare builtin method names — they match too
240
- # broadly (e.g. "items" matching ActionCost.items when the
241
- # actual call is dict.items()).
242
- if target in _BUILTIN_METHOD_NAMES:
241
+ # Check the last dotted component against builtin method names.
242
+ # Catches both bare "items" and dotted "effect.get", "args.items".
243
+ leaf = target.rsplit(".", 1)[-1]
244
+ if leaf in _BUILTIN_METHOD_NAMES:
243
245
  return edge
244
246
 
247
+ # Dotted targets like "effect.get" or "logger.warning" where the
248
+ # root is NOT a known node are local-variable method calls —
249
+ # resolving them through the name index produces false positives.
250
+ if "." in target:
251
+ root = target.split(".", 1)[0]
252
+ # If the root isn't itself a project node, it's a local var
253
+ if root not in node_ids and root not in name_index:
254
+ return edge
255
+
245
256
  candidates = name_index.get(target, set())
246
257
  if len(candidates) == 1:
247
258
  target = next(iter(candidates))
248
259
  elif len(candidates) > 1:
249
- # For CALLS edges, a bare name like `process()` in Python
250
- # NEVER resolves to the same class — you need `self.process()`
251
- # for that. Exclude the source itself to prevent self-call
252
- # artifacts, and prefer module-level over class-level.
260
+ # Exclude self-calls (bare `process()` != `self.process()`)
253
261
  filtered = candidates - {source}
254
262
  if not filtered:
255
263
  filtered = candidates
256
264
 
257
- # Extract the top-level module from the source
265
+ # Prefer candidates sharing the longest common prefix with
266
+ # the source. This ensures a call from engine.rules.resolver
267
+ # to _resolve_entity_ref picks resolver's own definition
268
+ # over engine.rules.field_paths._resolve_entity_ref.
258
269
  src_parts = source.split(".")
259
- src_module = src_parts[0] if src_parts else source
260
-
261
- # Score candidates: prefer same module, then shorter paths
262
- # (module-level functions are shorter than class methods)
263
- best = None
264
- for c in filtered:
265
- if c.startswith(src_module + "."):
266
- if best is None or c.count(".") < best.count("."):
267
- best = c
268
- target = best or next(iter(filtered))
270
+
271
+ def _common_prefix_len(c: str) -> int:
272
+ c_parts = c.split(".")
273
+ n = 0
274
+ for a, b in zip(src_parts, c_parts):
275
+ if a == b:
276
+ n += 1
277
+ else:
278
+ break
279
+ return n
280
+
281
+ best = max(filtered, key=lambda c: (_common_prefix_len(c), -c.count(".")))
282
+ target = best
269
283
 
270
284
  if source == edge.source and target == edge.target:
271
285
  return edge
@@ -34,6 +34,7 @@ import warnings
34
34
  from pathlib import Path
35
35
  from typing import Any
36
36
 
37
+ from interlinked.analyzer.graph import _BUILTIN_METHOD_NAMES
37
38
  from interlinked.models import NodeData, EdgeData, SymbolType, EdgeType
38
39
 
39
40
  # Python builtins we should never create nodes/edges for
@@ -323,7 +324,17 @@ def parse_project(root: str | Path) -> tuple[list[NodeData], list[EdgeData]]:
323
324
  metadata=e.metadata,
324
325
  ))
325
326
  else:
326
- # Keep the raw call target — external library calls visible to auditors
327
+ # Keep the raw call target — external library calls visible
328
+ # to auditors. BUT filter out localvar.builtin_method()
329
+ # patterns: if the inferencer couldn't resolve the target
330
+ # and the leaf is a common builtin method name, it's almost
331
+ # certainly a dict/list/str/logging method on a local var
332
+ # (e.g. effect.get(), args.items(), logger.warning()).
333
+ # Real project method calls resolve through the type system.
334
+ if "." in raw_target:
335
+ leaf = raw_target.rsplit(".", 1)[-1]
336
+ if leaf in _BUILTIN_METHOD_NAMES:
337
+ continue
327
338
  resolved_edges.append(e)
328
339
  continue
329
340
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: interlinked-mapper
3
- Version: 0.3.10
3
+ Version: 0.3.11
4
4
  Summary: A Python program topology explorer — visualize the shape of your codebase
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/austerecryptid/interlinked
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "interlinked-mapper"
7
- version = "0.3.10"
7
+ version = "0.3.11"
8
8
  description = "A Python program topology explorer — visualize the shape of your codebase"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -917,6 +917,91 @@ class TestParserSkipDirs:
917
917
  # ══════════════════════════════════════════════════════════════════════
918
918
 
919
919
 
920
+ class TestFalsePositiveEdges:
921
+ """Regression tests for false positive edge resolution.
922
+
923
+ These patterns caused incorrect edges in production:
924
+ - effect.get(), args.items() — dotted builtin methods not caught
925
+ - logger.warning() — logging method resolving to project symbols
926
+ - _resolve_entity_ref — same-name function in sibling module
927
+ """
928
+
929
+ @pytest.fixture(autouse=True)
930
+ def setup(self):
931
+ self.graph, self.engine, _, _ = _build(FIXTURES)
932
+
933
+ def _edge_targets_from(self, source_partial: str, edge_type: EdgeType | None = None) -> set[str]:
934
+ """All edge targets from a source matching partial name."""
935
+ src = _find(self.graph, source_partial)
936
+ return {
937
+ e.target for e in self.graph.all_edges()
938
+ if e.source == src
939
+ and (edge_type is None or e.edge_type == edge_type)
940
+ }
941
+
942
+ def test_dict_get_not_resolved(self):
943
+ """effect.get('value') must NOT create a call to any project symbol."""
944
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
945
+ # "effect.get" should not resolve to anything with ".get" in it
946
+ assert not any(t.endswith(".get") for t in targets), \
947
+ f"dict.get() false positive: {[t for t in targets if '.get' in t]}"
948
+
949
+ def test_dict_items_not_resolved(self):
950
+ """args.items() must NOT create a call to any project symbol."""
951
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
952
+ assert not any(t.endswith(".items") for t in targets), \
953
+ f"dict.items() false positive: {[t for t in targets if '.items' in t]}"
954
+
955
+ def test_list_extend_not_resolved(self):
956
+ """ops.extend() must NOT resolve to a project symbol."""
957
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
958
+ assert not any(t.endswith(".extend") for t in targets), \
959
+ f"list.extend() false positive: {[t for t in targets if '.extend' in t]}"
960
+
961
+ def test_list_append_not_resolved(self):
962
+ """collected.append() must NOT resolve to a project symbol."""
963
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
964
+ assert not any(t.endswith(".append") and "." in t.rsplit(".append", 1)[0] for t in targets), \
965
+ f"list.append() false positive: {[t for t in targets if '.append' in t]}"
966
+
967
+ def test_str_startswith_not_resolved(self):
968
+ """ref.startswith() must NOT resolve to a project symbol."""
969
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
970
+ assert not any(t.endswith(".startswith") for t in targets), \
971
+ f"str.startswith() false positive: {[t for t in targets if '.startswith' in t]}"
972
+
973
+ def test_str_split_not_resolved(self):
974
+ """ref.split() must NOT resolve to a project symbol."""
975
+ targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
976
+ assert not any(t.endswith(".split") for t in targets), \
977
+ f"str.split() false positive: {[t for t in targets if '.split' in t]}"
978
+
979
+ def test_logger_warning_not_resolved(self):
980
+ """logger.warning() must NOT create edges to project symbols."""
981
+ targets = self._edge_targets_from("logging_calls", EdgeType.CALLS)
982
+ # Should not resolve to any project node
983
+ node_ids = _node_ids(self.graph)
984
+ resolved_to_project = targets & node_ids
985
+ bad = {t for t in resolved_to_project if "warning" in t or "error" in t or "info" in t or "debug" in t}
986
+ assert not bad, \
987
+ f"logging calls resolved to project symbols: {bad}"
988
+
989
+ def test_same_name_helper_resolves_locally(self):
990
+ """calls_shared_helper() should call THIS module's _shared_helper."""
991
+ caller = _find(self.graph, "calls_shared_helper")
992
+ targets = {
993
+ e.target for e in self.graph.all_edges()
994
+ if e.source == caller and e.edge_type == EdgeType.CALLS
995
+ }
996
+ # Should resolve to false_positive_edges._shared_helper, not another module's
997
+ local_match = [t for t in targets if "false_positive_edges._shared_helper" in t]
998
+ foreign_match = [t for t in targets if "_shared_helper" in t and "false_positive_edges" not in t]
999
+ assert local_match, \
1000
+ f"calls_shared_helper should call local _shared_helper, got: {targets}"
1001
+ assert not foreign_match, \
1002
+ f"calls_shared_helper resolved to foreign _shared_helper: {foreign_match}"
1003
+
1004
+
920
1005
  class TestMCPFidelity:
921
1006
  """Verify MCP dispatch produces the same results as direct engine calls."""
922
1007