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.
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/PKG-INFO +1 -1
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/graph.py +34 -20
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/parser.py +12 -1
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/PKG-INFO +1 -1
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/pyproject.toml +1 -1
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/tests/test_accuracy.py +85 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/__init__.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/__init__.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/dead_code.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/embeddings.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/analyzer/similarity.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/cli.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/__init__.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/llm.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/query.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/commander/repl.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/mcp_server.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/models.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/__init__.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/index.html +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package-lock.json +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package.json +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/App.tsx +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/index.css +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/main.tsx +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/theme.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/types.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/tsconfig.json +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/vite.config.ts +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/layouts.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/server.py +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/SOURCES.txt +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/entry_points.txt +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/requires.txt +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/top_level.txt +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/setup.cfg +0 -0
- {interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/tests/test_query_completeness.py +0 -0
|
@@ -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
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
if
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "interlinked-mapper"
|
|
7
|
-
version = "0.3.
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package.json
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/App.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/main.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/theme.ts
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/types.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.10 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|