diff --git a/README.md b/README.md
index 8651656..22c012c 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
[](#testing)
[](LICENSE)
-
+
`pip install` it as a library · run it as a CLI · plug it into any AI agent as an MCP server · or install the skill into 22 agents with one command
diff --git a/graphora/__init__.py b/graphora/__init__.py
index 069699b..913b970 100644
--- a/graphora/__init__.py
+++ b/graphora/__init__.py
@@ -9,7 +9,7 @@
import importlib
-__version__ = "0.2.0"
+__version__ = "0.2.1"
_LAZY = {
"parse_code_file": "graphora.parser",
diff --git a/graphora/parser.py b/graphora/parser.py
index 5fa040d..4c49324 100644
--- a/graphora/parser.py
+++ b/graphora/parser.py
@@ -162,11 +162,11 @@ def _parse_with_tree_sitter(path: str, content: str, language: str) -> ParsedFil
if symbol:
symbols.append(symbol)
if symbol.kind == "Function":
- function_ranges.append((symbol.name, node.start_point.row + 1, node.end_point.row + 1))
+ function_ranges.append((symbol.name, _point_row(node.start_point) + 1, _point_row(node.end_point) + 1))
call_name = _tree_sitter_call(language, node, content_bytes)
if call_name:
- line = node.start_point.row + 1
+ line = _point_row(node.start_point) + 1
caller = _caller_for_line(function_ranges, line)
if caller and caller != call_name:
calls.append(ParsedCall(caller=caller, callee=call_name, line=line, confidence=INFERRED))
@@ -217,6 +217,19 @@ def _tree_sitter_parser(language: str) -> Any | None:
return None
+def _point_row(point: Any) -> int:
+ """Read a tree-sitter ``Point``'s row via tuple/index access.
+
+ tree-sitter 0.26.0 has a known upstream bug (already fixed, not yet
+ released) where the named ``Point.row``/``Point.column`` attributes
+ return unstable values for source coordinates beyond the small-integer
+ range, while tuple/index access (``point[0]``, ``point[1]``) remains
+ stable across affected and unaffected versions alike. Always read the
+ row this way rather than via ``point.row``.
+ """
+ return point[0]
+
+
def _walk_nodes(node: Any) -> list[Any]:
nodes = [node]
for child in node.children:
@@ -299,7 +312,7 @@ def _tree_sitter_symbol(language: str, node: Any, content_bytes: bytes, content:
if not kind or name_node is None:
return None
- line = node.start_point.row + 1
+ line = _point_row(node.start_point) + 1
signature = content.splitlines()[line - 1].strip()
name = _node_text(name_node, content_bytes).split("::")[-1]
if not re.match(r"^[A-Za-z_][\w$]*$", name):
diff --git a/pyproject.toml b/pyproject.toml
index 446ed60..54250d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphora-kg"
-version = "0.2.0"
+version = "0.2.1"
description = "Deterministic code knowledge graph: index with tree-sitter, query blast radius, mine risk memory from git, review diffs grounded in structure. No LLM required to build."
readme = "README.md"
requires-python = ">=3.10"
@@ -24,7 +24,14 @@ classifiers = [
]
dependencies = [
"falkordb>=1.0",
- "tree-sitter>=0.23",
+ # tree-sitter 0.26.0 has a known upstream bug (already fixed upstream, not
+ # yet released as of this writing): Point.row/Point.column can return
+ # unstable values for source coordinates beyond the small-integer range.
+ # Graphora reads points via safe tuple/index access (see graphora/parser.py
+ # _point_row) so it is not affected, but 0.26.0 is excluded here as a
+ # defense-in-depth measure for any other code path. Future corrected
+ # releases (>0.26.0) remain installable.
+ "tree-sitter>=0.23,!=0.26.0",
"tree-sitter-python>=0.23",
"tree-sitter-javascript>=0.23",
"tree-sitter-typescript>=0.23",
diff --git a/tests/core/test_core_packaging.py b/tests/core/test_core_packaging.py
new file mode 100644
index 0000000..34b81a2
--- /dev/null
+++ b/tests/core/test_core_packaging.py
@@ -0,0 +1,103 @@
+"""Package-metadata assertions: the tree-sitter dependency bound and version.
+
+tree-sitter 0.26.0 is excluded (see `pyproject.toml` and
+`graphora/parser.py::_point_row`) because of an upstream Point-coordinate
+bug. These tests assert the exclusion is present in the installed package's
+metadata -- so if the bound is accidentally dropped or widened, a normal
+test failure catches it -- while leaving room for any future corrected
+release above 0.26.0.
+
+This file also asserts the project's active version surfaces
+(`pyproject.toml`'s `[project].version`, `graphora.__version__`, and the
+installed package metadata) agree, so a partial version bump is caught,
+and that README.md's banner image uses an absolute URL (not a
+repo-relative path, which PyPI cannot resolve) in the packaged long
+description.
+"""
+
+import importlib.metadata as metadata
+import re
+from pathlib import Path
+
+from packaging.requirements import Requirement
+from packaging.specifiers import SpecifierSet
+
+_PYPROJECT = Path(__file__).parent.parent.parent / "pyproject.toml"
+
+
+def _pyproject_version() -> str:
+ text = _PYPROJECT.read_text(encoding="utf-8")
+ match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text)
+ assert match, "project version not found in pyproject.toml"
+ return match.group(1)
+
+
+def _tree_sitter_requirement() -> Requirement:
+ requires = metadata.requires("graphora-kg") or []
+ for raw in requires:
+ req = Requirement(raw)
+ if req.name == "tree-sitter":
+ return req
+ raise AssertionError("graphora-kg metadata does not declare a tree-sitter requirement")
+
+
+def test_tree_sitter_026_is_excluded():
+ req = _tree_sitter_requirement()
+ specifier = req.specifier
+ assert "0.26.0" not in specifier, "tree-sitter 0.26.0 must stay excluded (unstable Point coordinates)"
+ assert not specifier.contains("0.26.0", prereleases=True)
+
+
+def test_tree_sitter_lower_bound_and_future_releases_allowed():
+ req = _tree_sitter_requirement()
+ specifier: SpecifierSet = req.specifier
+ # The historical floor stays installable...
+ assert specifier.contains("0.23.0", prereleases=True)
+ assert specifier.contains("0.25.2", prereleases=True)
+ # ...and a hypothetical corrected release after 0.26.0 stays supportable.
+ assert specifier.contains("0.26.1", prereleases=True)
+ assert specifier.contains("0.27.0", prereleases=True)
+
+
+def test_pyproject_declares_matching_exclusion():
+ # Cross-check the source declaration too, so the test doesn't only pass
+ # against a stale installed .dist-info from a previous `pip install -e`.
+ text = _PYPROJECT.read_text(encoding="utf-8")
+ # Matches the dependency entry (e.g. "tree-sitter>=0.23,!=0.26.0") while
+ # skipping the bare "tree-sitter" keyword and "tree-sitter-" extras.
+ match = re.search(r'"tree-sitter(>=[^"]*)"', text)
+ assert match, "tree-sitter dependency line not found in pyproject.toml"
+ assert "!=0.26.0" in match.group(1)
+
+
+def test_active_version_surfaces_agree():
+ import graphora
+
+ pyproject_version = _pyproject_version()
+ installed_version = metadata.version("graphora-kg")
+
+ assert graphora.__version__ == pyproject_version, (
+ f"graphora.__version__ ({graphora.__version__}) must match "
+ f"pyproject.toml's [project].version ({pyproject_version})"
+ )
+ assert installed_version == pyproject_version, (
+ f"installed graphora-kg metadata version ({installed_version}) must match "
+ f"pyproject.toml's [project].version ({pyproject_version}) -- reinstall "
+ "(`pip install -e .`) after bumping the version"
+ )
+
+
+def test_readme_banner_uses_absolute_url_in_packaged_long_description():
+ # README.md's banner previously used a repo-relative
,
+ # which PyPI cannot reliably resolve (PyPI renders the long description outside
+ # the repository, with no relative-path base). It must be an absolute HTTPS URL
+ # to the raw asset so the image renders on the PyPI project page.
+ meta = metadata.metadata("graphora-kg")
+ long_description = meta.get("Description") or meta.get_payload() or ""
+ assert long_description, "graphora-kg metadata has no long description"
+
+ absolute_url = "https://raw.githubusercontent.com/Naseem77/Graphora/main/assets/banner.gif"
+ assert absolute_url in long_description, "packaged long description must reference the absolute banner URL"
+ assert 'src="assets/banner.gif"' not in long_description, (
+ "packaged long description must not contain the repo-relative banner path"
+ )
diff --git a/tests/core/test_core_parser_large_coordinates.py b/tests/core/test_core_parser_large_coordinates.py
new file mode 100644
index 0000000..602c20b
--- /dev/null
+++ b/tests/core/test_core_parser_large_coordinates.py
@@ -0,0 +1,81 @@
+"""Regression test: tree-sitter Point coordinates beyond the small-integer range.
+
+tree-sitter 0.26.0 has a known upstream bug (already fixed, not yet released
+as of writing) where reading a node's ``Point`` via the named ``.row``/
+``.column`` attributes is unstable for source coordinates beyond the small
+integer range (observed on real-world files such as axios's `index.d.ts`,
+first triggered near line 386). Reading the same coordinates via tuple/index
+access (``point[0]``, ``point[1]``) remains stable. Graphora reads points via
+``graphora.parser._point_row``, which uses index access exclusively.
+
+This fixture (`tests/fixtures/large_coordinates.ts`) is a small, locally
+authored TypeScript file with two functions defined past line 256 (rows
+261 and 265, 0-based) so the relevant coordinates fall outside the affected
+small-integer range. The parse runs in a fresh subprocess so that any
+dependency regression -- in tree-sitter itself, or in a future edit to
+`graphora/parser.py` that reintroduces named attribute access -- surfaces as
+an ordinary, isolated test failure rather than corrupting this test process.
+"""
+
+import json
+import subprocess
+import sys
+from pathlib import Path
+
+FIXTURE = Path(__file__).parent.parent / "fixtures" / "large_coordinates.ts"
+
+# Expected (1-based) source lines, computed independently of the parser by
+# construction of the fixture file itself.
+EXPECTED_ALPHA_LINE = 262
+EXPECTED_BETA_LINE = 266
+EXPECTED_CALL_LINE = 267
+
+_SUBPROCESS_SCRIPT = """
+import json
+import sys
+
+from graphora.parser import parse_code_file
+
+path = sys.argv[1]
+with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+parsed = parse_code_file("fixtures/large_coordinates.ts", content)
+functions = {s.name: s.line for s in parsed.symbols if s.kind == "Function"}
+calls = [
+ {"caller": c.caller, "callee": c.callee, "line": c.line, "confidence": c.confidence}
+ for c in parsed.calls
+]
+print(json.dumps({
+ "parser_used": parsed.parser_used,
+ "functions": functions,
+ "calls": calls,
+}))
+"""
+
+
+def test_large_row_coordinates_use_stable_point_access():
+ assert FIXTURE.exists(), f"missing fixture: {FIXTURE}"
+
+ result = subprocess.run(
+ [sys.executable, "-c", _SUBPROCESS_SCRIPT, str(FIXTURE)],
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+ assert result.returncode == 0, result.stderr
+ payload = json.loads(result.stdout)
+
+ assert payload["parser_used"] == "tree-sitter"
+
+ functions = payload["functions"]
+ assert functions.get("computeAlpha") == EXPECTED_ALPHA_LINE
+ assert functions.get("computeBeta") == EXPECTED_BETA_LINE
+
+ calls = payload["calls"]
+ assert any(
+ c["caller"] == "computeBeta"
+ and c["callee"] == "computeAlpha"
+ and c["line"] == EXPECTED_CALL_LINE
+ for c in calls
+ ), calls
diff --git a/tests/fixtures/large_coordinates.ts b/tests/fixtures/large_coordinates.ts
new file mode 100644
index 0000000..a95f2be
--- /dev/null
+++ b/tests/fixtures/large_coordinates.ts
@@ -0,0 +1,269 @@
+export type Padding0 = number;
+export type Padding1 = number;
+export type Padding2 = number;
+export type Padding3 = number;
+export type Padding4 = number;
+export type Padding5 = number;
+export type Padding6 = number;
+export type Padding7 = number;
+export type Padding8 = number;
+export type Padding9 = number;
+export type Padding10 = number;
+export type Padding11 = number;
+export type Padding12 = number;
+export type Padding13 = number;
+export type Padding14 = number;
+export type Padding15 = number;
+export type Padding16 = number;
+export type Padding17 = number;
+export type Padding18 = number;
+export type Padding19 = number;
+export type Padding20 = number;
+export type Padding21 = number;
+export type Padding22 = number;
+export type Padding23 = number;
+export type Padding24 = number;
+export type Padding25 = number;
+export type Padding26 = number;
+export type Padding27 = number;
+export type Padding28 = number;
+export type Padding29 = number;
+export type Padding30 = number;
+export type Padding31 = number;
+export type Padding32 = number;
+export type Padding33 = number;
+export type Padding34 = number;
+export type Padding35 = number;
+export type Padding36 = number;
+export type Padding37 = number;
+export type Padding38 = number;
+export type Padding39 = number;
+export type Padding40 = number;
+export type Padding41 = number;
+export type Padding42 = number;
+export type Padding43 = number;
+export type Padding44 = number;
+export type Padding45 = number;
+export type Padding46 = number;
+export type Padding47 = number;
+export type Padding48 = number;
+export type Padding49 = number;
+export type Padding50 = number;
+export type Padding51 = number;
+export type Padding52 = number;
+export type Padding53 = number;
+export type Padding54 = number;
+export type Padding55 = number;
+export type Padding56 = number;
+export type Padding57 = number;
+export type Padding58 = number;
+export type Padding59 = number;
+export type Padding60 = number;
+export type Padding61 = number;
+export type Padding62 = number;
+export type Padding63 = number;
+export type Padding64 = number;
+export type Padding65 = number;
+export type Padding66 = number;
+export type Padding67 = number;
+export type Padding68 = number;
+export type Padding69 = number;
+export type Padding70 = number;
+export type Padding71 = number;
+export type Padding72 = number;
+export type Padding73 = number;
+export type Padding74 = number;
+export type Padding75 = number;
+export type Padding76 = number;
+export type Padding77 = number;
+export type Padding78 = number;
+export type Padding79 = number;
+export type Padding80 = number;
+export type Padding81 = number;
+export type Padding82 = number;
+export type Padding83 = number;
+export type Padding84 = number;
+export type Padding85 = number;
+export type Padding86 = number;
+export type Padding87 = number;
+export type Padding88 = number;
+export type Padding89 = number;
+export type Padding90 = number;
+export type Padding91 = number;
+export type Padding92 = number;
+export type Padding93 = number;
+export type Padding94 = number;
+export type Padding95 = number;
+export type Padding96 = number;
+export type Padding97 = number;
+export type Padding98 = number;
+export type Padding99 = number;
+export type Padding100 = number;
+export type Padding101 = number;
+export type Padding102 = number;
+export type Padding103 = number;
+export type Padding104 = number;
+export type Padding105 = number;
+export type Padding106 = number;
+export type Padding107 = number;
+export type Padding108 = number;
+export type Padding109 = number;
+export type Padding110 = number;
+export type Padding111 = number;
+export type Padding112 = number;
+export type Padding113 = number;
+export type Padding114 = number;
+export type Padding115 = number;
+export type Padding116 = number;
+export type Padding117 = number;
+export type Padding118 = number;
+export type Padding119 = number;
+export type Padding120 = number;
+export type Padding121 = number;
+export type Padding122 = number;
+export type Padding123 = number;
+export type Padding124 = number;
+export type Padding125 = number;
+export type Padding126 = number;
+export type Padding127 = number;
+export type Padding128 = number;
+export type Padding129 = number;
+export type Padding130 = number;
+export type Padding131 = number;
+export type Padding132 = number;
+export type Padding133 = number;
+export type Padding134 = number;
+export type Padding135 = number;
+export type Padding136 = number;
+export type Padding137 = number;
+export type Padding138 = number;
+export type Padding139 = number;
+export type Padding140 = number;
+export type Padding141 = number;
+export type Padding142 = number;
+export type Padding143 = number;
+export type Padding144 = number;
+export type Padding145 = number;
+export type Padding146 = number;
+export type Padding147 = number;
+export type Padding148 = number;
+export type Padding149 = number;
+export type Padding150 = number;
+export type Padding151 = number;
+export type Padding152 = number;
+export type Padding153 = number;
+export type Padding154 = number;
+export type Padding155 = number;
+export type Padding156 = number;
+export type Padding157 = number;
+export type Padding158 = number;
+export type Padding159 = number;
+export type Padding160 = number;
+export type Padding161 = number;
+export type Padding162 = number;
+export type Padding163 = number;
+export type Padding164 = number;
+export type Padding165 = number;
+export type Padding166 = number;
+export type Padding167 = number;
+export type Padding168 = number;
+export type Padding169 = number;
+export type Padding170 = number;
+export type Padding171 = number;
+export type Padding172 = number;
+export type Padding173 = number;
+export type Padding174 = number;
+export type Padding175 = number;
+export type Padding176 = number;
+export type Padding177 = number;
+export type Padding178 = number;
+export type Padding179 = number;
+export type Padding180 = number;
+export type Padding181 = number;
+export type Padding182 = number;
+export type Padding183 = number;
+export type Padding184 = number;
+export type Padding185 = number;
+export type Padding186 = number;
+export type Padding187 = number;
+export type Padding188 = number;
+export type Padding189 = number;
+export type Padding190 = number;
+export type Padding191 = number;
+export type Padding192 = number;
+export type Padding193 = number;
+export type Padding194 = number;
+export type Padding195 = number;
+export type Padding196 = number;
+export type Padding197 = number;
+export type Padding198 = number;
+export type Padding199 = number;
+export type Padding200 = number;
+export type Padding201 = number;
+export type Padding202 = number;
+export type Padding203 = number;
+export type Padding204 = number;
+export type Padding205 = number;
+export type Padding206 = number;
+export type Padding207 = number;
+export type Padding208 = number;
+export type Padding209 = number;
+export type Padding210 = number;
+export type Padding211 = number;
+export type Padding212 = number;
+export type Padding213 = number;
+export type Padding214 = number;
+export type Padding215 = number;
+export type Padding216 = number;
+export type Padding217 = number;
+export type Padding218 = number;
+export type Padding219 = number;
+export type Padding220 = number;
+export type Padding221 = number;
+export type Padding222 = number;
+export type Padding223 = number;
+export type Padding224 = number;
+export type Padding225 = number;
+export type Padding226 = number;
+export type Padding227 = number;
+export type Padding228 = number;
+export type Padding229 = number;
+export type Padding230 = number;
+export type Padding231 = number;
+export type Padding232 = number;
+export type Padding233 = number;
+export type Padding234 = number;
+export type Padding235 = number;
+export type Padding236 = number;
+export type Padding237 = number;
+export type Padding238 = number;
+export type Padding239 = number;
+export type Padding240 = number;
+export type Padding241 = number;
+export type Padding242 = number;
+export type Padding243 = number;
+export type Padding244 = number;
+export type Padding245 = number;
+export type Padding246 = number;
+export type Padding247 = number;
+export type Padding248 = number;
+export type Padding249 = number;
+export type Padding250 = number;
+export type Padding251 = number;
+export type Padding252 = number;
+export type Padding253 = number;
+export type Padding254 = number;
+export type Padding255 = number;
+export type Padding256 = number;
+export type Padding257 = number;
+export type Padding258 = number;
+export type Padding259 = number;
+
+export function computeAlpha(x: number): number {
+ return x + 1;
+}
+
+export function computeBeta(y: number): number {
+ return computeAlpha(y) + 1;
+}
+