graphlens 0.1.1__py3-none-any.whl

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.
graphlens/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """Models, contracts, registry, and utilities for polyglot code analysis."""
2
+
3
+ from graphlens.contracts import (
4
+ DependencyFileParser,
5
+ DiscoveredProject,
6
+ GraphBackend,
7
+ LanguageAdapter,
8
+ ProjectReader,
9
+ normalize_pkg_name,
10
+ )
11
+ from graphlens.exceptions import (
12
+ AdapterError,
13
+ AdapterNotFoundError,
14
+ BackendError,
15
+ DiscoveryError,
16
+ DuplicateNodeError,
17
+ GraphLensError,
18
+ )
19
+ from graphlens.models import (
20
+ GraphLens,
21
+ Node,
22
+ NodeKind,
23
+ Relation,
24
+ RelationKind,
25
+ )
26
+ from graphlens.registry import AdapterRegistry, adapter_registry
27
+
28
+ __all__ = [
29
+ "AdapterError",
30
+ "AdapterNotFoundError",
31
+ # registry
32
+ "AdapterRegistry",
33
+ "BackendError",
34
+ # contracts
35
+ "DependencyFileParser",
36
+ "DiscoveredProject",
37
+ "DiscoveryError",
38
+ "DuplicateNodeError",
39
+ "GraphBackend",
40
+ # models
41
+ "GraphLens",
42
+ # exceptions
43
+ "GraphLensError",
44
+ "LanguageAdapter",
45
+ "Node",
46
+ "NodeKind",
47
+ "ProjectReader",
48
+ "Relation",
49
+ "RelationKind",
50
+ "adapter_registry",
51
+ "normalize_pkg_name",
52
+ ]
@@ -0,0 +1,18 @@
1
+ """Public contracts (ABCs) for graphlens adapters and backends."""
2
+
3
+ from graphlens.contracts.adapter import LanguageAdapter
4
+ from graphlens.contracts.backend import GraphBackend
5
+ from graphlens.contracts.deps import (
6
+ DependencyFileParser,
7
+ normalize_pkg_name,
8
+ )
9
+ from graphlens.contracts.reader import DiscoveredProject, ProjectReader
10
+
11
+ __all__ = [
12
+ "DependencyFileParser",
13
+ "DiscoveredProject",
14
+ "GraphBackend",
15
+ "LanguageAdapter",
16
+ "ProjectReader",
17
+ "normalize_pkg_name",
18
+ ]
@@ -0,0 +1,79 @@
1
+ """LanguageAdapter contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+ from graphlens.models.graph import GraphLens
12
+
13
+ _EXCLUDED_DIRS: frozenset[str] = frozenset(
14
+ {
15
+ ".venv", "venv", "__pycache__", ".git",
16
+ "dist", "build", ".eggs", "node_modules",
17
+ }
18
+ )
19
+
20
+
21
+ class LanguageAdapter(ABC):
22
+ """Contract that every language adapter package must implement."""
23
+
24
+ @abstractmethod
25
+ def language(self) -> str:
26
+ """Return the language identifier, e.g. 'python', 'typescript'."""
27
+ ...
28
+
29
+ @abstractmethod
30
+ def can_handle(self, project_root: Path) -> bool:
31
+ """
32
+ Return True if this adapter can handle the project at the given root.
33
+
34
+ Typically checks for marker files
35
+ (pyproject.toml, package.json, Cargo.toml).
36
+ """
37
+ ...
38
+
39
+ @abstractmethod
40
+ def analyze(
41
+ self, project_root: Path, files: list[Path] | None = None
42
+ ) -> GraphLens:
43
+ """
44
+ Parse the project and return a GraphLens with nodes and relations.
45
+
46
+ If ``files`` is None, the adapter collects source files itself via
47
+ ``collect_files()``. Pass an explicit list to override (e.g. for
48
+ incremental updates or custom filtering in a pipeline).
49
+
50
+ Adapters must not write to any backend — they return data only.
51
+ """
52
+ ...
53
+
54
+ def file_extensions(self) -> set[str]:
55
+ """
56
+ Return file extensions this adapter handles, e.g. {'.py'}.
57
+
58
+ Used by ``collect_files()`` for automatic discovery.
59
+ """
60
+ return set()
61
+
62
+ def collect_files(self, project_root: Path) -> list[Path]:
63
+ """
64
+ Return all source files under project_root for this adapter.
65
+
66
+ Excludes common non-source directories
67
+ (.venv, __pycache__, .git, etc.).
68
+ Override for custom discovery logic.
69
+ """
70
+ extensions = self.file_extensions()
71
+ if not extensions:
72
+ return []
73
+ return sorted(
74
+ p
75
+ for p in project_root.rglob("*")
76
+ if p.is_file()
77
+ and p.suffix in extensions
78
+ and not (_EXCLUDED_DIRS & set(p.relative_to(project_root).parts))
79
+ )
@@ -0,0 +1,27 @@
1
+ """GraphBackend contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from graphlens.models.graph import GraphLens
10
+
11
+
12
+ class GraphBackend(ABC):
13
+ """Contract for graph persistence backends."""
14
+
15
+ @abstractmethod
16
+ def store(self, graph: GraphLens) -> None:
17
+ """
18
+ Persist the given graph.
19
+
20
+ Implementation decides merge/replace semantics.
21
+ """
22
+ ...
23
+
24
+ @abstractmethod
25
+ def clear(self) -> None:
26
+ """Remove all stored data."""
27
+ ...
@@ -0,0 +1,65 @@
1
+ """DependencyFileParser contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+
12
+ class DependencyFileParser(ABC):
13
+ """
14
+ Extracts declared third-party dependency names from a project manifest.
15
+
16
+ Each implementation targets one file format (pyproject.toml, package.json,
17
+ requirements.txt, Cargo.toml, …). Language adapters ship default parsers
18
+ for their ecosystem; users can pass custom parsers to handle non-standard
19
+ package managers (poetry, pnpm workspaces, pip-tools, etc.).
20
+
21
+ ``parse()`` returns *normalized* top-level distribution names so that
22
+ callers can compare them against the first segment of an import path::
23
+
24
+ "requests" # PyPI
25
+ "scikit_learn" # normalized: scikit-learn → scikit_learn
26
+ "@types/node" # npm scoped package (keep as-is)
27
+
28
+ Normalization rule: lowercase, hyphens → underscores, drop extras/version
29
+ specifiers. Scoped npm names (``@scope/pkg``) are kept unchanged.
30
+ """
31
+
32
+ @abstractmethod
33
+ def can_parse(self, project_root: Path) -> bool:
34
+ """Return True if this parser applies to the given project root."""
35
+ ...
36
+
37
+ @abstractmethod
38
+ def parse(self, project_root: Path) -> frozenset[str]:
39
+ """Return normalized top-level package names declared as deps."""
40
+ ...
41
+
42
+
43
+ def normalize_pkg_name(name: str) -> str:
44
+ """
45
+ Normalize a distribution name for import-name comparison.
46
+
47
+ * Strips version specifiers and extras:
48
+ ``requests>=2.0 [security]`` → ``requests``
49
+ * Lowercases
50
+ * Replaces hyphens with underscores
51
+
52
+ Scoped npm names (``@scope/pkg``) are returned as-is (lowercased).
53
+ """
54
+ # Strip inline comments (requirements.txt style)
55
+ name = name.split("#", maxsplit=1)[0].strip()
56
+ # Strip extras and version specifiers: Foo[bar]>=1.0 → Foo
57
+ for sep in ("[", ">", "<", "=", "!", "~", ";", " "):
58
+ name = name.split(sep)[0]
59
+ name = name.strip()
60
+ if not name:
61
+ return ""
62
+ # Scoped npm packages keep their structure
63
+ if name.startswith("@"):
64
+ return name.lower()
65
+ return name.lower().replace("-", "_")
@@ -0,0 +1,33 @@
1
+ """ProjectReader contract and DiscoveredProject model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class DiscoveredProject:
15
+ """Result of project discovery: a root path, language, and source files."""
16
+
17
+ root: Path
18
+ language: str
19
+ files: list[Path] = field(default_factory=list)
20
+
21
+
22
+ class ProjectReader(ABC):
23
+ """Contract for project discovery and source file enumeration."""
24
+
25
+ @abstractmethod
26
+ def discover(self, root: Path) -> list[DiscoveredProject]:
27
+ """
28
+ Scan the root directory and return discovered projects.
29
+
30
+ A monorepo may contain multiple projects (e.g., a Python backend and
31
+ a TypeScript frontend). Each gets its own DiscoveredProject entry.
32
+ """
33
+ ...
@@ -0,0 +1,25 @@
1
+ """Base exceptions for graphlens."""
2
+
3
+
4
+ class GraphLensError(Exception):
5
+ """Base exception for all graphlens errors."""
6
+
7
+
8
+ class AdapterNotFoundError(GraphLensError):
9
+ """No adapter found for the requested language."""
10
+
11
+
12
+ class AdapterError(GraphLensError):
13
+ """Error raised during adapter execution."""
14
+
15
+
16
+ class DuplicateNodeError(GraphLensError):
17
+ """A node with this ID already exists in the graph."""
18
+
19
+
20
+ class DiscoveryError(GraphLensError):
21
+ """Error raised during project discovery."""
22
+
23
+
24
+ class BackendError(GraphLensError):
25
+ """Error raised during graph backend operation."""
@@ -0,0 +1,7 @@
1
+ """Graph model classes: GraphLens, Node, Relation, and their enums."""
2
+
3
+ from graphlens.models.graph import GraphLens
4
+ from graphlens.models.nodes import Node, NodeKind
5
+ from graphlens.models.relations import Relation, RelationKind
6
+
7
+ __all__ = ["GraphLens", "Node", "NodeKind", "Relation", "RelationKind"]
@@ -0,0 +1,41 @@
1
+ """In-memory code graph container."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ from graphlens.exceptions import DuplicateNodeError
9
+
10
+ if TYPE_CHECKING:
11
+ from graphlens.models.nodes import Node
12
+ from graphlens.models.relations import Relation
13
+
14
+
15
+ @dataclass
16
+ class GraphLens:
17
+ """Accumulator for nodes and relations produced by language adapters."""
18
+
19
+ nodes: dict[str, Node] = field(default_factory=dict)
20
+ relations: list[Relation] = field(default_factory=list)
21
+
22
+ def add_node(self, node: Node) -> None:
23
+ """Add a node; raise DuplicateNodeError on ID collision."""
24
+ if node.id in self.nodes:
25
+ msg = f"Node with id '{node.id}' already exists"
26
+ raise DuplicateNodeError(msg)
27
+ self.nodes[node.id] = node
28
+
29
+ def add_relation(self, relation: Relation) -> None:
30
+ """Append a relation to the graph."""
31
+ self.relations.append(relation)
32
+
33
+ def merge(self, other: GraphLens) -> None:
34
+ """
35
+ Merge another graph into this one.
36
+
37
+ Raises DuplicateNodeError on ID collision.
38
+ """
39
+ for node in other.nodes.values():
40
+ self.add_node(node)
41
+ self.relations.extend(other.relations)
@@ -0,0 +1,45 @@
1
+ """Node (entity) model for the code graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from graphlens.utils.span import Span
11
+
12
+
13
+ class NodeKind(enum.Enum):
14
+ """Discriminator for the kind of entity a node represents."""
15
+
16
+ PROJECT = "project"
17
+ MODULE = "module"
18
+ FILE = "file"
19
+ CLASS = "class"
20
+ FUNCTION = "function"
21
+ METHOD = "method"
22
+ PARAMETER = "parameter"
23
+ IMPORT = "import"
24
+ DEPENDENCY = "dependency"
25
+ SYMBOL = "symbol"
26
+ EXTERNAL_SYMBOL = "external_symbol"
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class Node:
31
+ """
32
+ A single entity in the code graph.
33
+
34
+ Uses a kind discriminator instead of a class-per-entity hierarchy to
35
+ keep the model flat, serialization-friendly, and easy to produce in
36
+ adapter tight loops.
37
+ """
38
+
39
+ id: str
40
+ kind: NodeKind
41
+ qualified_name: str
42
+ name: str
43
+ file_path: str | None = None
44
+ span: Span | None = None
45
+ metadata: dict[str, object] = field(default_factory=dict)
@@ -0,0 +1,29 @@
1
+ """Relation (edge) model for the code graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ class RelationKind(enum.Enum):
10
+ """Discriminator for the kind of directed edge between two nodes."""
11
+
12
+ CONTAINS = "contains"
13
+ DECLARES = "declares"
14
+ IMPORTS = "imports"
15
+ CALLS = "calls"
16
+ REFERENCES = "references"
17
+ DEPENDS_ON = "depends_on"
18
+ RESOLVES_TO = "resolves_to"
19
+ INHERITS_FROM = "inherits_from"
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class Relation:
24
+ """A directed edge between two nodes, referenced by ID."""
25
+
26
+ source_id: str
27
+ target_id: str
28
+ kind: RelationKind
29
+ metadata: dict[str, object] = field(default_factory=dict)
graphlens/py.typed ADDED
File without changes
graphlens/registry.py ADDED
@@ -0,0 +1,76 @@
1
+ """Adapter registry — discovers and loads language adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.metadata
6
+ from typing import TYPE_CHECKING
7
+
8
+ from graphlens.exceptions import AdapterNotFoundError
9
+
10
+ if TYPE_CHECKING:
11
+ from graphlens.contracts.adapter import LanguageAdapter
12
+
13
+
14
+ class AdapterRegistry:
15
+ """
16
+ Registry for language adapters.
17
+
18
+ Supports two registration mechanisms (in resolution order):
19
+ 1. In-memory registration via ``register()`` — for manual setup
20
+ and testing.
21
+ 2. Automatic discovery via ``importlib.metadata`` entry points
22
+ under the ``"graphlens.adapters"`` group — for installed
23
+ adapter packages.
24
+
25
+ Adapter packages register themselves in their ``pyproject.toml``::
26
+
27
+ [project.entry-points."graphlens.adapters"]
28
+ python = "graphlens_python:PythonAdapter"
29
+ """
30
+
31
+ ENTRY_POINT_GROUP = "graphlens.adapters"
32
+
33
+ def __init__(self) -> None:
34
+ """Initialise the registry with an empty in-memory store."""
35
+ self._adapters: dict[str, type[LanguageAdapter]] = {}
36
+
37
+ def register(self, name: str, adapter_cls: type[LanguageAdapter]) -> None:
38
+ """Register an adapter class for the given language name."""
39
+ self._adapters[name] = adapter_cls
40
+
41
+ def load(self, name: str) -> type[LanguageAdapter]:
42
+ """
43
+ Return the adapter class for the given language name.
44
+
45
+ Checks in-memory registry first, then entry points.
46
+ Raises :exc:`AdapterNotFoundError` if not found.
47
+ """
48
+ if name in self._adapters:
49
+ return self._adapters[name]
50
+
51
+ for ep in importlib.metadata.entry_points(
52
+ group=self.ENTRY_POINT_GROUP
53
+ ):
54
+ if ep.name == name:
55
+ adapter_cls = ep.load()
56
+ self._adapters[name] = adapter_cls
57
+ return adapter_cls
58
+
59
+ msg = (
60
+ f"No adapter found for language '{name}'. "
61
+ f"Install a graphlens-{name} package or register an adapter"
62
+ " manually."
63
+ )
64
+ raise AdapterNotFoundError(msg)
65
+
66
+ def available(self) -> list[str]:
67
+ """Return names of all available adapters (registered + entry pts)."""
68
+ names: set[str] = set(self._adapters)
69
+ for ep in importlib.metadata.entry_points(
70
+ group=self.ENTRY_POINT_GROUP
71
+ ):
72
+ names.add(ep.name)
73
+ return sorted(names)
74
+
75
+
76
+ adapter_registry = AdapterRegistry()
@@ -0,0 +1,6 @@
1
+ """Shared utility helpers: deterministic IDs and source spans."""
2
+
3
+ from graphlens.utils.ids import make_node_id
4
+ from graphlens.utils.span import Span
5
+
6
+ __all__ = ["Span", "make_node_id"]
graphlens/utils/ids.py ADDED
@@ -0,0 +1,16 @@
1
+ """Deterministic node ID generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+
8
+ def make_node_id(project_name: str, qualified_name: str, kind: str) -> str:
9
+ """
10
+ Return a stable, deterministic node ID.
11
+
12
+ Uses a truncated SHA-256 hex digest so the same inputs always produce
13
+ the same ID across runs, enabling incremental updates and graph diffing.
14
+ """
15
+ key = f"{project_name}::{kind}::{qualified_name}"
16
+ return hashlib.sha256(key.encode()).hexdigest()[:16]
@@ -0,0 +1,15 @@
1
+ """Source location utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Span:
10
+ """A source location range. All values are 1-based."""
11
+
12
+ start_line: int
13
+ start_col: int
14
+ end_line: int
15
+ end_col: int
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.3
2
+ Name: graphlens
3
+ Version: 0.1.1
4
+ Summary: Extensible polyglot code analysis framework with a graph IR
5
+ Keywords: code-analysis,graphlens,ast,tree-sitter,static-analysis,graph,polyglot,dependency-analysis,python,code-intelligence
6
+ Author: Neko1313
7
+ Author-email: Neko1313 <nikita.ribalchencko@yandex.ru>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Neko1313
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.13
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
36
+ Classifier: Topic :: Software Development :: Quality Assurance
37
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
38
+ Classifier: Typing :: Typed
39
+ Classifier: Operating System :: OS Independent
40
+ Requires-Dist: graphlens-python ; extra == 'python'
41
+ Requires-Python: >=3.13
42
+ Project-URL: Repository, https://github.com/Neko1313/graphlens
43
+ Project-URL: Issues, https://github.com/Neko1313/graphlens/issues
44
+ Project-URL: Changelog, https://github.com/Neko1313/graphlens/blob/main/CHANGELOG.md
45
+ Provides-Extra: python
46
+ Description-Content-Type: text/markdown
47
+
48
+ <div align="center">
49
+
50
+ <h1>graphlens</h1>
51
+
52
+ <p>Extensible polyglot code analysis framework that parses source projects, normalizes their structure into a shared graph IR, and exposes it for dependency analysis, navigation, and code intelligence tooling.</p>
53
+
54
+ [![PyPI](https://img.shields.io/pypi/v/graphlens?color=blue)](https://pypi.org/project/graphlens/)
55
+ [![Python](https://img.shields.io/pypi/pyversions/graphlens)](https://pypi.org/project/graphlens/)
56
+ [![License](https://img.shields.io/github/license/Neko1313/graphlens)](LICENSE)
57
+ [![CI](https://img.shields.io/github/actions/workflow/status/Neko1313/graphlens/ci.yml?label=CI)](https://github.com/Neko1313/graphlens/actions)
58
+ [![codecov](https://codecov.io/gh/Neko1313/graphlens/graph/badge.svg)](https://codecov.io/gh/Neko1313/graphlens)
59
+
60
+ [Repository](https://github.com/Neko1313/graphlens) · [Issues](https://github.com/Neko1313/graphlens/issues)
61
+
62
+ </div>
63
+
64
+ ---
65
+
66
+ ## Architecture
67
+
68
+ ```
69
+ Repository → Language Adapter → GraphLens (IR) → Graph Backend
70
+ ```
71
+
72
+ | Layer | Responsibility |
73
+ |---|---|
74
+ | **Language Adapter** | Parses source files, produces `GraphLens` |
75
+ | **GraphLens** | Typed nodes + directed relations (the IR) |
76
+ | **Graph Backend** | Persists or queries the graph (Neo4j, in-memory, …) |
77
+
78
+ Adapters are **pure data producers** — they never write to any backend. The graph is the only output.
79
+
80
+ ## Why graph IR?
81
+
82
+ - **Language-agnostic** — one shared model for Python, TypeScript, Rust, …
83
+ - **Plugin-based adapters** — each language is a separate package, registered via Python entry points
84
+ - **Tree-sitter powered** — all adapters use tree-sitter for error-tolerant CST parsing and exact span positions
85
+ - **Monorepo aware** — `can_handle()` and `find_*_roots()` handle multi-language repos correctly
86
+ - **Deterministic node IDs** — SHA-256 hash of `project::kind::qualified_name` → stable across re-scans
87
+
88
+ ## Installation
89
+
90
+ ```bash
91
+ # Core library only (models, contracts, registry)
92
+ pip install graphlens
93
+
94
+ # Core + Python adapter
95
+ pip install "graphlens[python]"
96
+ ```
97
+
98
+ With uv:
99
+
100
+ ```bash
101
+ uv add graphlens
102
+ uv add "graphlens[python]"
103
+ ```
104
+
105
+ ## Quick start
106
+
107
+ ```python
108
+ from pathlib import Path
109
+ from graphlens import adapter_registry
110
+
111
+ # Load and instantiate the Python adapter
112
+ adapter = adapter_registry.load("python")()
113
+
114
+ # Analyze a project — returns a GraphLens
115
+ graph = adapter.analyze(Path("./my-project"))
116
+
117
+ print(f"Nodes: {len(graph.nodes)}")
118
+ print(f"Relations: {len(graph.relations)}")
119
+
120
+ # Inspect nodes by kind
121
+ from graphlens import NodeKind
122
+
123
+ modules = [n for n in graph.nodes.values() if n.kind == NodeKind.MODULE]
124
+ classes = [n for n in graph.nodes.values() if n.kind == NodeKind.CLASS]
125
+ ```
126
+
127
+ ## Graph model
128
+
129
+ ### Node kinds
130
+
131
+ | Kind | Description |
132
+ |---|---|
133
+ | `PROJECT` | Root project node |
134
+ | `MODULE` | Python/TS/… module (directory or file) |
135
+ | `FILE` | Source file |
136
+ | `CLASS` | Class declaration |
137
+ | `FUNCTION` | Top-level function |
138
+ | `METHOD` | Method inside a class |
139
+ | `PARAMETER` | Function/method parameter |
140
+ | `IMPORT` | Import statement |
141
+ | `DEPENDENCY` | Declared package dependency |
142
+ | `SYMBOL` | Internal symbol reference |
143
+ | `EXTERNAL_SYMBOL` | External symbol (stdlib, third-party, unknown) |
144
+
145
+ ### Relation kinds
146
+
147
+ | Kind | Description |
148
+ |---|---|
149
+ | `CONTAINS` | Structural containment (project → module → file → class) |
150
+ | `DECLARES` | Declaration (file declares function, class declares method) |
151
+ | `IMPORTS` | Import edge (file → import node) |
152
+ | `RESOLVES_TO` | Import resolved to a module or external symbol |
153
+ | `CALLS` | Function/method call |
154
+ | `REFERENCES` | Symbol reference |
155
+ | `INHERITS_FROM` | Class inheritance |
156
+ | `DEPENDS_ON` | Package dependency |
157
+
158
+ ## Adapter plugin system
159
+
160
+ Language adapters register themselves via Python entry points — no changes to the core needed:
161
+
162
+ ```toml
163
+ # packages/graphlens-python/pyproject.toml
164
+ [project.entry-points."graphlens.adapters"]
165
+ python = "graphlens_python:PythonAdapter"
166
+ ```
167
+
168
+ The registry discovers installed adapters automatically at runtime:
169
+
170
+ ```python
171
+ from graphlens import adapter_registry
172
+
173
+ adapter_registry.available() # ["python", ...]
174
+ adapter_cls = adapter_registry.load("python")
175
+ adapter = adapter_cls()
176
+ ```
177
+
178
+ Adapters can also be registered manually (useful for testing):
179
+
180
+ ```python
181
+ adapter_registry.register("python", MyPythonAdapter)
182
+ ```
183
+
184
+ ## Implementing an adapter
185
+
186
+ Subclass `LanguageAdapter` and implement four methods:
187
+
188
+ ```python
189
+ from pathlib import Path
190
+ from graphlens import GraphLens, LanguageAdapter
191
+
192
+ class MyLangAdapter(LanguageAdapter):
193
+ def language(self) -> str:
194
+ return "mylang"
195
+
196
+ def file_extensions(self) -> set[str]:
197
+ return {".ml", ".mli"}
198
+
199
+ def can_handle(self, project_root: Path) -> bool:
200
+ return (project_root / "dune-project").exists()
201
+
202
+ def analyze(
203
+ self, project_root: Path, files: list[Path] | None = None
204
+ ) -> GraphLens:
205
+ graph = GraphLens()
206
+ files = files or self.collect_files(project_root)
207
+ # ... parse and populate graph ...
208
+ return graph
209
+ ```
210
+
211
+ Register in `pyproject.toml` and the core registry finds it automatically.
212
+
213
+ ## Project structure
214
+
215
+ ```
216
+ graphlens/ ← uv workspace root (core library)
217
+ src/graphlens/ ← models, contracts, registry, exceptions, utils
218
+ packages/
219
+ graphlens-python/ ← Python adapter (tree-sitter)
220
+ tests/ ← core tests (100% coverage)
221
+ examples/ ← runnable usage examples
222
+ ```
223
+
224
+ ## Development
225
+
226
+ Requires Python 3.13+, [uv](https://docs.astral.sh/uv/), [task](https://taskfile.dev/).
227
+
228
+ ```bash
229
+ task install # uv sync --all-groups
230
+ task lint # ruff + ty + bandit for all packages
231
+ task tests # all tests with coverage
232
+ ```
233
+
234
+ Individual package tasks:
235
+
236
+ ```bash
237
+ task core:lint task core:test
238
+ task python:lint task python:test
239
+ ```
240
+
241
+ ## License
242
+
243
+ MIT
@@ -0,0 +1,19 @@
1
+ graphlens/__init__.py,sha256=jHL1qyi3WoUmc_cEAzrkXik4wDlWK5jfwUzlczFGwH8,1062
2
+ graphlens/contracts/__init__.py,sha256=qBB2uUiwR_zbJfWgt6YHcg_lvaBJ1-ihioEqlINxWH8,498
3
+ graphlens/contracts/adapter.py,sha256=o8WaJGPSlw4c4VkMpny8NjTwVyVQRbecTmHdxSuxAs4,2279
4
+ graphlens/contracts/backend.py,sha256=ZUzQFDJ9w7jv0fxIPvZxduCSRHvRyIbemh1R8zGX_po,575
5
+ graphlens/contracts/deps.py,sha256=qkBv1G-k9YkYZ8pjck55Ad5921BjV52sPlg4IAhF3nQ,2233
6
+ graphlens/contracts/reader.py,sha256=-EoFjsaYBhnF894ZaqLDZRJlpaJZAGYd6eWEEu_eruk,905
7
+ graphlens/exceptions.py,sha256=_-Dzg1if1T_oqzWYYQ05leD6C9B_0A1i4ABVoDlCQq0,596
8
+ graphlens/models/__init__.py,sha256=--q_kvve3sbcaWaLEGQLNkPFfexSBatM9Xf7ezMn49Q,302
9
+ graphlens/models/graph.py,sha256=BgBLcIWJAcnv9VOOTAcO_sD-Ara7lvpSSBQ8IJ_5j3Y,1258
10
+ graphlens/models/nodes.py,sha256=am9W1IrPdrua6ay4q6vYbkL-ovN-FRMrkyb-V6Ibg6U,1070
11
+ graphlens/models/relations.py,sha256=7rRtI3p8jZA7uxFN8R2TUXsFINu9gTT7h43GdBizVYY,710
12
+ graphlens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ graphlens/registry.py,sha256=mjuGbMbg2yG2TiOzpEhpJOIyaRgNrlVRyO7nmu_Y3lU,2448
14
+ graphlens/utils/__init__.py,sha256=kK7sCTawkc0mBw4WOy3GIe7HkKbCJwXT77eiqSTH3hQ,186
15
+ graphlens/utils/ids.py,sha256=DDxJVJRC3tgt4jQ1UnHNvcC3FGJZxXZ-JjoxV3ZRSKg,493
16
+ graphlens/utils/span.py,sha256=5D-WbcCjb5u_dVgsr0RmJ-LOM0GaM-Vu-3CixdZ9_Ig,288
17
+ graphlens-0.1.1.dist-info/WHEEL,sha256=M4DeIjVCA49okfALADZoWX5JOGwnmHb-JOpQHtI-1c0,80
18
+ graphlens-0.1.1.dist-info/METADATA,sha256=FcskvjnwhEXJuHHfIrAFsIl0kIzvBGTkODTE96L0_w4,8400
19
+ graphlens-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any