From 6c8ffba7e305ddab604b0dac7d751fe174f2cfe7 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:48:55 +0300 Subject: [PATCH 1/4] Fix: read tree-sitter Point coordinates via safe index access tree-sitter 0.26.0 has an upstream bug (already fixed, not yet released) where reading a node's Point via the named .row/.column attributes is unstable for source coordinates beyond the small-integer range. On real files (e.g. axios's index.d.ts, first triggered near line 386) this can segfault the interpreter. Tuple/index access (point[0], point[1]) is stable on both affected and unaffected tree-sitter versions. Replace the three .row accesses in graphora/parser.py with a small _point_row() helper that reads Point.row via index access. No public API, confidence tagging, or supported-language changes. Also exclude tree-sitter==0.26.0 in pyproject.toml as defense-in-depth so other/future code paths can't silently receive the broken release, while keeping >=0.23 and any future corrected release (>0.26.0) installable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- graphora/parser.py | 19 ++++++++++++++++--- pyproject.toml | 9 ++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) 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..cf5b2d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", From 07e7a829b077875b886b9479a433b0ef7c9bb99d Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:49:01 +0300 Subject: [PATCH 2/4] Add regression coverage for the tree-sitter Point coordinate fix - tests/fixtures/large_coordinates.ts: a small, locally-authored TypeScript fixture with two functions past line 256 (rows 261 and 265), so relevant coordinates fall outside tree-sitter 0.26.0's affected small-integer range. - test_core_parser_large_coordinates.py: parses the fixture in an isolated subprocess (a dependency regression - including a segfault, as reproduced against unpatched code under tree-sitter 0.26.0 - becomes an ordinary, isolated test failure) and asserts the extracted function and call line numbers. - test_core_packaging.py: asserts the installed package metadata and pyproject.toml both exclude tree-sitter==0.26.0 while keeping >=0.23 and any future corrected release installable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/core/test_core_packaging.py | 56 ++++ .../test_core_parser_large_coordinates.py | 81 ++++++ tests/fixtures/large_coordinates.ts | 269 ++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 tests/core/test_core_packaging.py create mode 100644 tests/core/test_core_parser_large_coordinates.py create mode 100644 tests/fixtures/large_coordinates.ts diff --git a/tests/core/test_core_packaging.py b/tests/core/test_core_packaging.py new file mode 100644 index 0000000..0342063 --- /dev/null +++ b/tests/core/test_core_packaging.py @@ -0,0 +1,56 @@ +"""Package-metadata assertions for the tree-sitter dependency bound. + +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. +""" + +import importlib.metadata as metadata +import re + +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet + + +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`. + from pathlib import Path + + pyproject = Path(__file__).parent.parent.parent / "pyproject.toml" + 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) 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; +} + From d4f427944aef4289c5f0d81610e2dc0d71edbcf9 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:56:54 +0300 Subject: [PATCH 3/4] Prepare v0.2.1 patch release Bump the active version surfaces from 0.2.0 to 0.2.1 for the tree-sitter Point-coordinate compatibility fix in this PR: - pyproject.toml: [project].version - graphora/__init__.py: __version__ (also surfaced by `graphora --version`) The v0.2.0 git tag and no other historical references were changed -- this only touches active release metadata. Extend tests/core/test_core_packaging.py with test_active_version_surfaces_agree(), which asserts __version__, the pyproject.toml version, and the installed package metadata version all match, so a partial version bump fails a normal test instead of shipping inconsistently. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- graphora/__init__.py | 2 +- pyproject.toml | 2 +- tests/core/test_core_packaging.py | 38 +++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 7 deletions(-) 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/pyproject.toml b/pyproject.toml index cf5b2d8..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" diff --git a/tests/core/test_core_packaging.py b/tests/core/test_core_packaging.py index 0342063..33d32ca 100644 --- a/tests/core/test_core_packaging.py +++ b/tests/core/test_core_packaging.py @@ -1,4 +1,4 @@ -"""Package-metadata assertions for the tree-sitter dependency bound. +"""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 @@ -6,14 +6,28 @@ 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. """ 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 [] @@ -45,12 +59,26 @@ def test_tree_sitter_lower_bound_and_future_releases_allowed(): 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`. - from pathlib import Path - - pyproject = Path(__file__).parent.parent.parent / "pyproject.toml" - text = pyproject.read_text(encoding="utf-8") + 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" + ) From fe6258177d7827239357b194bb89cbf5e6a530c3 Mon Sep 17 00:00:00 2001 From: Naseem Ali <34807727+Naseem77@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:00:02 +0300 Subject: [PATCH 4/4] Fix PyPI-unresolvable README banner image URL README.md's banner used a repo-relative . GitHub resolves that against the repo, but PyPI renders the packaged long description with no repository base, so the image never loads on the PyPI project page. Switch to the absolute raw-asset URL: https://raw.githubusercontent.com/Naseem77/Graphora/main/assets/banner.gif Verified the URL responds 200 with content-type: image/gif, and that it is the only source of this image in the packaged wheel/sdist long description (built graphora_kg-0.2.1 wheel + sdist, inspected dist-info/METADATA and PKG-INFO: absolute URL present, repo-relative path absent; twine check passes on both). Added test_readme_banner_uses_absolute_url_in_packaged_long_description to tests/core/test_core_packaging.py to keep this from regressing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- tests/core/test_core_packaging.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8651656..22c012c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![Tests](https://img.shields.io/badge/tests-80%20passing-brightgreen)](#testing) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -Graphora: source files flow into a live knowledge graph that powers blast radius, diff review with risk memory, and MCP tools for agents. 22,160 repo tokens shrink to 1,237. +Graphora: source files flow into a live knowledge graph that powers blast radius, diff review with risk memory, and MCP tools for agents. 22,160 repo tokens shrink to 1,237. `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/tests/core/test_core_packaging.py b/tests/core/test_core_packaging.py index 33d32ca..34b81a2 100644 --- a/tests/core/test_core_packaging.py +++ b/tests/core/test_core_packaging.py @@ -9,7 +9,10 @@ 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. +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 @@ -82,3 +85,19 @@ def test_active_version_surfaces_agree(): 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" + )