victor-codegraph 0.1.0__tar.gz → 0.1.2__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.
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/PKG-INFO +1 -1
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/pyproject.toml +1 -1
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/tests/test_adapter.py +4 -0
- victor_codegraph-0.1.2/tests/test_line_base_contract.py +30 -0
- victor_codegraph-0.1.2/tests/test_repo.py +62 -0
- victor_codegraph-0.1.2/tests/test_symbol_oid_contract.py +81 -0
- victor_codegraph-0.1.2/tests/test_symbol_oid_parity.py +77 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/__init__.py +12 -1
- victor_codegraph-0.1.2/victor_codegraph/adapter.py +182 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/model.py +55 -2
- victor_codegraph-0.1.2/victor_codegraph/repo.py +113 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/treesitter_parser.py +35 -7
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/PKG-INFO +1 -1
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/SOURCES.txt +5 -0
- victor_codegraph-0.1.0/victor_codegraph/adapter.py +0 -94
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/README.md +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/setup.cfg +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/tests/test_python_parser.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/tests/test_sizing_and_chunk.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/tests/test_treesitter_jsts.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/config.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/languages.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/parser.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/python_parser.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph/sizing.py +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/dependency_links.txt +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/requires.txt +0 -0
- {victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: victor-codegraph
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Code->CPG chunker: tree-sitter symbol + relation extraction, size-capped chunks, ProximaRecord projection. Shared by Victor, ProximaDB SDK, and AnvaiOps.
|
|
5
5
|
Author-email: Vijaykumar Singh <singhvjd@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "victor-codegraph"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Code->CPG chunker: tree-sitter symbol + relation extraction, size-capped chunks, ProximaRecord projection. Shared by Victor, ProximaDB SDK, and AnvaiOps."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -37,6 +37,10 @@ def test_edge_records_reference_node_oids():
|
|
|
37
37
|
assert e["edge"]["from_oid"].startswith("graph/repo1/node/")
|
|
38
38
|
assert e["edge"]["to_oid"].startswith("graph/repo1/node/")
|
|
39
39
|
assert e["edge"]["edge_type"] == "CALLS"
|
|
40
|
+
# edge props always carry a call-site line (0 when unknown; >0 once the parser
|
|
41
|
+
# preserves call_site through resolution — see the call-site-fidelity change).
|
|
42
|
+
assert "line" in e["props"]
|
|
43
|
+
assert isinstance(e["props"]["line"], int)
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
def test_embedder_populates_embedding_cell():
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Pins the canonical 1-based line-number contract (see model.LINE_BASE).
|
|
2
|
+
|
|
3
|
+
This is the cross-surface contract — Victor, the ProximaDB SDK, and AnvaiOps all
|
|
4
|
+
consume this package and must agree on a symbol's (file, name, line) coordinate.
|
|
5
|
+
If a refactor ever shifts line numbers, this test fails loudly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from victor_codegraph import LINE_BASE, parse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_line_base_is_one():
|
|
14
|
+
assert LINE_BASE == 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_parse_emits_1_based_lines():
|
|
18
|
+
# The def is on the FIRST line → start_line must be 1 (not 0).
|
|
19
|
+
parsed = parse("def first():\n return second()\n\n\ndef second():\n return 1\n", file_path="m.py")
|
|
20
|
+
by_name = {s.simple_name: s for s in parsed.symbols}
|
|
21
|
+
assert by_name["first"].location.start_line == 1
|
|
22
|
+
assert by_name["second"].location.start_line == 5 # 1-based line of the 2nd def
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_call_site_line_is_1_based():
|
|
26
|
+
parsed = parse("def a():\n return b()\n\n\ndef b():\n return 1\n", file_path="m.py")
|
|
27
|
+
calls = [r for r in parsed.relations if r.relation_type.name == "CALLS"]
|
|
28
|
+
assert calls
|
|
29
|
+
# the b() call is on line 2 (1-based), not 1
|
|
30
|
+
assert any(r.call_site is not None and r.call_site.start_line == 2 for r in calls)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Repo-walk tests — fully offline (tmp tree + stdlib ast for the .py files)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from victor_codegraph import chunk_path, chunk_repo, iter_source_files, parse_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _make_tree(root):
|
|
9
|
+
(root / "pkg").mkdir()
|
|
10
|
+
(root / "pkg" / "a.py").write_text("def a():\n return b()\ndef b():\n return 1\n")
|
|
11
|
+
(root / "pkg" / "notes.txt").write_text("not source") # unknown extension
|
|
12
|
+
(root / "README.md").write_text("# docs") # markdown is not in the language map
|
|
13
|
+
# noise dirs that must be skipped
|
|
14
|
+
(root / "node_modules").mkdir()
|
|
15
|
+
(root / "node_modules" / "dep.py").write_text("def vendored():\n pass\n")
|
|
16
|
+
(root / ".git").mkdir()
|
|
17
|
+
(root / ".git" / "hook.py").write_text("def hook():\n pass\n")
|
|
18
|
+
return root
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_iter_source_files_filters_and_skips_noise(tmp_path):
|
|
22
|
+
_make_tree(tmp_path)
|
|
23
|
+
found = {p.name for p in iter_source_files(tmp_path)}
|
|
24
|
+
assert found == {"a.py"} # .txt/.md skipped; node_modules/.git pruned
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_iter_source_files_language_filter(tmp_path):
|
|
28
|
+
_make_tree(tmp_path)
|
|
29
|
+
assert {p.name for p in iter_source_files(tmp_path, languages=["python"])} == {"a.py"}
|
|
30
|
+
assert list(iter_source_files(tmp_path, languages=["rust"])) == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_iter_single_file(tmp_path):
|
|
34
|
+
f = tmp_path / "solo.py"
|
|
35
|
+
f.write_text("def f():\n return 1\n")
|
|
36
|
+
assert [p.name for p in iter_source_files(f)] == ["solo.py"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_chunk_path(tmp_path):
|
|
40
|
+
f = tmp_path / "m.py"
|
|
41
|
+
f.write_text("def f():\n return 1\n")
|
|
42
|
+
chunks = chunk_path(f)
|
|
43
|
+
assert chunks and any(c.metadata.get("simple_name") == "f" for c in chunks)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_chunk_path_unreadable_returns_empty(tmp_path):
|
|
47
|
+
assert chunk_path(tmp_path / "does_not_exist.py") == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_parse_path(tmp_path):
|
|
51
|
+
f = tmp_path / "m.py"
|
|
52
|
+
f.write_text("def a():\n return b()\ndef b():\n return 1\n")
|
|
53
|
+
parsed = parse_path(f)
|
|
54
|
+
assert parsed is not None
|
|
55
|
+
assert {s.simple_name for s in parsed.symbols} == {"a", "b"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_chunk_repo_streams_all_files(tmp_path):
|
|
59
|
+
_make_tree(tmp_path)
|
|
60
|
+
chunks = list(chunk_repo(tmp_path))
|
|
61
|
+
names = {c.metadata.get("simple_name") for c in chunks}
|
|
62
|
+
assert {"a", "b"} <= names # symbols from pkg/a.py, vendored dirs excluded
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Pins the ADR-044 stable, line-independent symbol-oid contract.
|
|
2
|
+
|
|
3
|
+
The correlated-CPG join key must survive edits that don't change *what a symbol is*:
|
|
4
|
+
a line move keeps the oid; a rename / signature change mints a new one; a body edit
|
|
5
|
+
bumps ``content_version`` but not the oid. Every cross-surface consumer (Victor, the
|
|
6
|
+
ProximaDB SDK, the AnvaiOps connector) derives identity from this one function, so if it
|
|
7
|
+
ever drifts this test fails loudly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from victor_codegraph import parse, stable_symbol_oid, to_proxima_records
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _node(records, name):
|
|
16
|
+
for r in records:
|
|
17
|
+
if "graph_node" in r.get("labels", []) and r["props"].get("name") == name:
|
|
18
|
+
return r
|
|
19
|
+
raise AssertionError(f"no node record for {name!r}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _records(content, *, repo="repo1", stable=True):
|
|
23
|
+
return to_proxima_records(
|
|
24
|
+
parse(content, file_path="m.py"), repo_graph_id=repo, stable_oid=stable
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _oid_of(content, name):
|
|
29
|
+
return _node(_records(content), name)["oid"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_stable_oid_unchanged_when_symbol_moves_lines():
|
|
33
|
+
base = "def first():\n return second()\n\n\ndef second():\n return 1\n"
|
|
34
|
+
moved = "\n\n\n" + base # both symbols pushed down; identity must not churn
|
|
35
|
+
assert _oid_of(base, "second") == _oid_of(moved, "second"), "line move must not churn the oid"
|
|
36
|
+
assert _oid_of(base, "first") == _oid_of(moved, "first")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_stable_oid_changes_on_rename():
|
|
40
|
+
a = "def alpha():\n return 1\n"
|
|
41
|
+
b = "def beta():\n return 1\n"
|
|
42
|
+
assert _oid_of(a, "alpha") != _oid_of(b, "beta"), "rename must mint a new identity"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_stable_oid_changes_on_signature_change():
|
|
46
|
+
one = "def f(x):\n return x\n"
|
|
47
|
+
two = "def f(x, y):\n return x\n"
|
|
48
|
+
assert _oid_of(one, "f") != _oid_of(two, "f"), "signature change must mint a new identity"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_body_edit_bumps_content_version_not_oid():
|
|
52
|
+
a = _node(_records("def f(x):\n return x\n"), "f")
|
|
53
|
+
b = _node(_records("def f(x):\n return x + 1 # changed body\n"), "f")
|
|
54
|
+
assert a["oid"] == b["oid"], "body edit must not churn the oid"
|
|
55
|
+
assert (
|
|
56
|
+
a["props"]["content_version"] != b["props"]["content_version"]
|
|
57
|
+
), "body edit must bump content_version"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_dual_emit_and_default_on_gate(monkeypatch):
|
|
61
|
+
monkeypatch.delenv("VICTOR_CODEGRAPH_STABLE_OID", raising=False)
|
|
62
|
+
parsed = parse("def f(x):\n return x\n", file_path="m.py")
|
|
63
|
+
# P2 cutover — default ON: the record oid is the canonical stable id; BOTH ids are
|
|
64
|
+
# always emitted for mixed-read.
|
|
65
|
+
on = _node(to_proxima_records(parsed, repo_graph_id="r"), "f")
|
|
66
|
+
assert on["oid"] == on["props"]["stable_oid"]
|
|
67
|
+
assert on["props"]["stable_oid"] != on["props"]["legacy_oid"]
|
|
68
|
+
# Explicit opt-out → legacy line-coupled id (existing collections stay byte-identical).
|
|
69
|
+
off = _node(to_proxima_records(parsed, repo_graph_id="r", stable_oid=False), "f")
|
|
70
|
+
assert off["oid"] == off["props"]["legacy_oid"]
|
|
71
|
+
assert off["props"]["stable_oid"] == on["props"]["stable_oid"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_stable_symbol_oid_is_pure_and_structural():
|
|
75
|
+
# The pure derivation depends only on structural coordinates — never on line/col.
|
|
76
|
+
k = stable_symbol_oid("repo1", "python", "m.py::f", "(x)")
|
|
77
|
+
assert k == stable_symbol_oid("repo1", "python", "m.py::f", "(x)") # deterministic
|
|
78
|
+
assert k != stable_symbol_oid("repo2", "python", "m.py::f", "(x)") # repo
|
|
79
|
+
assert k != stable_symbol_oid("repo1", "rust", "m.py::f", "(x)") # language
|
|
80
|
+
assert k != stable_symbol_oid("repo1", "python", "m.py::g", "(x)") # name (FQN)
|
|
81
|
+
assert k != stable_symbol_oid("repo1", "python", "m.py::f", "(x, y)") # overload
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""ADR-044 P2 cutover **parity ratchet**.
|
|
2
|
+
|
|
3
|
+
The canonical oid becomes the default only if it is a SAFE, non-lossy replacement for the
|
|
4
|
+
legacy one. This ratchet pins the invariants that gate the cutover, over a representative
|
|
5
|
+
multi-symbol corpus:
|
|
6
|
+
|
|
7
|
+
1. **No collisions** — distinct symbols get distinct canonical oids (else the cutover
|
|
8
|
+
would merge two symbols into one record and lose data).
|
|
9
|
+
2. **Completeness** — every symbol gets a non-empty canonical oid + a legacy alias +
|
|
10
|
+
a content_version.
|
|
11
|
+
3. **Line-shift stability** — no symbol's canonical oid changes when lines move
|
|
12
|
+
(the incremental-re-index property the whole ADR exists for).
|
|
13
|
+
4. **Determinism** — re-parsing yields identical oids.
|
|
14
|
+
|
|
15
|
+
If any of these regresses, the cutover is unsafe and CI fails.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from victor_codegraph import parse, to_proxima_records
|
|
21
|
+
|
|
22
|
+
_CORPUS = """\
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Auth:
|
|
27
|
+
def login(self, user):
|
|
28
|
+
return self._check(user)
|
|
29
|
+
|
|
30
|
+
def _check(self, user):
|
|
31
|
+
return bool(user)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def helper(x):
|
|
35
|
+
return x + 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def helper2(x, y):
|
|
39
|
+
return x + y
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _nodes(records):
|
|
44
|
+
return [r for r in records if "graph_node" in r["labels"]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _canonical_nodes(src):
|
|
48
|
+
return _nodes(
|
|
49
|
+
to_proxima_records(parse(src, file_path="auth.py"), repo_graph_id="r", stable_oid=True)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_no_collisions_distinct_symbols_distinct_oids():
|
|
54
|
+
recs = _canonical_nodes(_CORPUS)
|
|
55
|
+
oids = [r["oid"] for r in recs]
|
|
56
|
+
assert len(recs) >= 5, "Auth, login, _check, helper, helper2 expected"
|
|
57
|
+
assert len(oids) == len(set(oids)), "canonical oids must be unique per symbol (no merge)"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_completeness_every_symbol_has_canonical_legacy_and_version():
|
|
61
|
+
for r in _canonical_nodes(_CORPUS):
|
|
62
|
+
assert r["props"]["stable_oid"], "missing canonical oid"
|
|
63
|
+
assert r["props"]["legacy_oid"], "missing legacy alias"
|
|
64
|
+
assert r["props"]["content_version"], "missing content_version"
|
|
65
|
+
assert r["oid"] == r["props"]["stable_oid"], "gated record oid must be the canonical one"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_line_shift_stable_for_all_symbols():
|
|
69
|
+
base = {r["props"]["name"]: r["oid"] for r in _canonical_nodes(_CORPUS)}
|
|
70
|
+
moved = {r["props"]["name"]: r["oid"] for r in _canonical_nodes("\n\n\n" + _CORPUS)}
|
|
71
|
+
assert base == moved, "no symbol's canonical oid may change on a line shift"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_deterministic_reparse():
|
|
75
|
+
a = {r["props"]["name"]: r["oid"] for r in _canonical_nodes(_CORPUS)}
|
|
76
|
+
b = {r["props"]["name"]: r["oid"] for r in _canonical_nodes(_CORPUS)}
|
|
77
|
+
assert a == b
|
|
@@ -16,6 +16,7 @@ from .adapter import relation_to_record, symbol_to_record, to_proxima_records
|
|
|
16
16
|
from .config import ChunkConfig
|
|
17
17
|
from .languages import detect_language
|
|
18
18
|
from .model import (
|
|
19
|
+
LINE_BASE,
|
|
19
20
|
CodeChunk,
|
|
20
21
|
CodeRelation,
|
|
21
22
|
CodeRelationType,
|
|
@@ -23,16 +24,24 @@ from .model import (
|
|
|
23
24
|
CodeSymbolType,
|
|
24
25
|
ParsedCode,
|
|
25
26
|
SourceLocation,
|
|
27
|
+
deterministic_symbol_id,
|
|
28
|
+
stable_symbol_oid,
|
|
26
29
|
)
|
|
27
30
|
from .parser import chunk, parse
|
|
31
|
+
from .repo import chunk_path, chunk_repo, iter_source_files, parse_path
|
|
28
32
|
|
|
29
|
-
__version__ = "0.1.
|
|
33
|
+
__version__ = "0.1.2"
|
|
30
34
|
|
|
31
35
|
__all__ = [
|
|
32
36
|
"__version__",
|
|
33
37
|
"chunk",
|
|
34
38
|
"parse",
|
|
39
|
+
"chunk_repo",
|
|
40
|
+
"chunk_path",
|
|
41
|
+
"parse_path",
|
|
42
|
+
"iter_source_files",
|
|
35
43
|
"ChunkConfig",
|
|
44
|
+
"LINE_BASE",
|
|
36
45
|
"detect_language",
|
|
37
46
|
"to_proxima_records",
|
|
38
47
|
"symbol_to_record",
|
|
@@ -44,4 +53,6 @@ __all__ = [
|
|
|
44
53
|
"CodeRelationType",
|
|
45
54
|
"ParsedCode",
|
|
46
55
|
"SourceLocation",
|
|
56
|
+
"stable_symbol_oid",
|
|
57
|
+
"deterministic_symbol_id",
|
|
47
58
|
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Projection to the ProximaDB substrate-keystone ``ProximaRecord`` shape.
|
|
2
|
+
|
|
3
|
+
Per ProximaDB ``CODE_GRAPH_CORRELATED_SUBSTRATE_2026_06_22.adoc`` a code symbol is *one*
|
|
4
|
+
record addressable as a relational row, a graph node, and a vector at once. This adapter
|
|
5
|
+
emits the **shape** as plain dicts — it does not import proximadb, embed, or write. The
|
|
6
|
+
consumer (Victor embedded, AnvaiOps service) supplies the embedder and the DB write.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from .model import CodeRelation, CodeSymbol, ParsedCode, stable_symbol_oid
|
|
16
|
+
|
|
17
|
+
Embedder = Callable[[str], list[float]]
|
|
18
|
+
|
|
19
|
+
# ADR-044 mixed-read gate. **P2 cutover (2026-06-28): default ON** — the record `oid` is
|
|
20
|
+
# the line-independent canonical form, gated behind the parity ratchet
|
|
21
|
+
# (tests/test_symbol_oid_parity.py: no collisions / completeness / line-shift stability).
|
|
22
|
+
# BOTH ids are always emitted in props so readers dual-read and legacy collections still
|
|
23
|
+
# resolve. Opt out per-call (`stable_oid=False`) or per-process (`VICTOR_CODEGRAPH_STABLE_OID=0`).
|
|
24
|
+
_STABLE_OID_ENV = "VICTOR_CODEGRAPH_STABLE_OID"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _stable_oid_enabled(override: bool | None = None) -> bool:
|
|
28
|
+
if override is not None:
|
|
29
|
+
return override
|
|
30
|
+
val = os.getenv(_STABLE_OID_ENV)
|
|
31
|
+
if val is None or val.strip() == "":
|
|
32
|
+
return True # ADR-044 P2: canonical is the default (parity-ratchet-gated)
|
|
33
|
+
return val.strip().lower() in ("1", "true", "yes", "on")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _legacy_symbol_oid(repo_graph_id: str, symbol: CodeSymbol) -> str:
|
|
37
|
+
"""Line-coupled alias (today's id) — retained for the mixed-read bake."""
|
|
38
|
+
return f"graph/{repo_graph_id}/node/{symbol.id}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _canonical_symbol_oid(repo_graph_id: str, symbol: CodeSymbol) -> str:
|
|
42
|
+
"""Line-independent canonical oid (ADR-044) — the correlation join key."""
|
|
43
|
+
key = stable_symbol_oid(
|
|
44
|
+
repo_graph_id, symbol.language, symbol.fully_qualified_name, symbol.signature
|
|
45
|
+
)
|
|
46
|
+
return f"graph/{repo_graph_id}/node/{key}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _content_version(symbol: CodeSymbol) -> str:
|
|
50
|
+
"""Body fingerprint for staleness/dedup — NOT identity (a body edit bumps this,
|
|
51
|
+
not the oid)."""
|
|
52
|
+
return hashlib.blake2b(symbol.source_code.encode("utf-8"), digest_size=8).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def symbol_to_record(
|
|
56
|
+
symbol: CodeSymbol,
|
|
57
|
+
repo_graph_id: str,
|
|
58
|
+
branch_id: str = "main",
|
|
59
|
+
embedder: Embedder | None = None,
|
|
60
|
+
model_id: str = "bge-small-en-v1.5",
|
|
61
|
+
dim: int = 384,
|
|
62
|
+
*,
|
|
63
|
+
stable_oid: bool | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Project one symbol to a node record (row + graph node + optional vector).
|
|
66
|
+
|
|
67
|
+
Always emits BOTH the canonical line-independent oid and the legacy line-coupled
|
|
68
|
+
one (ADR-044 dual-emit); the primary record ``oid`` is the canonical one only when
|
|
69
|
+
the stable-oid gate is on (``stable_oid`` arg or ``VICTOR_CODEGRAPH_STABLE_OID``),
|
|
70
|
+
else legacy — so existing collections are byte-identical until cutover. Consumers
|
|
71
|
+
read ``name`` / ``line`` / ``fully_qualified_name`` from props (never by parsing the
|
|
72
|
+
oid), so the oid can be opaque.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
legacy = _legacy_symbol_oid(repo_graph_id, symbol)
|
|
76
|
+
canonical = _canonical_symbol_oid(repo_graph_id, symbol)
|
|
77
|
+
oid = canonical if _stable_oid_enabled(stable_oid) else legacy
|
|
78
|
+
record: dict[str, Any] = {
|
|
79
|
+
"oid": oid,
|
|
80
|
+
"labels": ["graph_node", "code_symbol"],
|
|
81
|
+
"branch_id": branch_id,
|
|
82
|
+
"props": {
|
|
83
|
+
"name": symbol.simple_name,
|
|
84
|
+
"fully_qualified_name": symbol.fully_qualified_name,
|
|
85
|
+
"file": symbol.location.file_path,
|
|
86
|
+
"line": symbol.location.start_line,
|
|
87
|
+
"end_line": symbol.location.end_line,
|
|
88
|
+
"lang": symbol.language,
|
|
89
|
+
"ast_kind": symbol.symbol_type.name,
|
|
90
|
+
"signature": symbol.signature,
|
|
91
|
+
"visibility": "private" if "private" in symbol.modifiers else "public",
|
|
92
|
+
"module_path": "::".join(symbol.scope_chain),
|
|
93
|
+
"snippet": symbol.source_code,
|
|
94
|
+
"documentation": symbol.documentation,
|
|
95
|
+
# ADR-044 dual-emit: both ids always present for mixed-read resolution,
|
|
96
|
+
# plus the body fingerprint (staleness/dedup, not identity).
|
|
97
|
+
"stable_oid": canonical,
|
|
98
|
+
"legacy_oid": legacy,
|
|
99
|
+
"content_version": _content_version(symbol),
|
|
100
|
+
},
|
|
101
|
+
"embeddings": [],
|
|
102
|
+
}
|
|
103
|
+
if embedder is not None:
|
|
104
|
+
record["embeddings"].append(
|
|
105
|
+
{
|
|
106
|
+
"model_id": model_id,
|
|
107
|
+
"modality": "code",
|
|
108
|
+
"dim": dim,
|
|
109
|
+
"values": embedder(symbol.source_code),
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return record
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def relation_to_record(
|
|
116
|
+
relation: CodeRelation,
|
|
117
|
+
repo_graph_id: str,
|
|
118
|
+
branch_id: str = "main",
|
|
119
|
+
*,
|
|
120
|
+
id_map: dict[str, str] | None = None,
|
|
121
|
+
stable_oid: bool | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
"""Project one relation to an edge record.
|
|
124
|
+
|
|
125
|
+
Edge IDENTITY is ``(from_oid, to_oid, edge_type)`` — the call-site line is a prop,
|
|
126
|
+
not identity, so the edge is line-independent once its endpoints are. ``id_map``
|
|
127
|
+
(built by :func:`to_proxima_records`) maps a symbol's legacy id to its canonical
|
|
128
|
+
oid; when the gate is on, endpoints resolve through it so edges and nodes agree.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def _endpoint(symbol_id: str) -> str:
|
|
132
|
+
if id_map is not None and _stable_oid_enabled(stable_oid):
|
|
133
|
+
mapped = id_map.get(symbol_id)
|
|
134
|
+
if mapped is not None:
|
|
135
|
+
return mapped
|
|
136
|
+
return f"graph/{repo_graph_id}/node/{symbol_id}"
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"labels": ["graph_edge"],
|
|
140
|
+
"branch_id": branch_id,
|
|
141
|
+
"edge": {
|
|
142
|
+
"from_oid": _endpoint(relation.from_symbol_id),
|
|
143
|
+
"to_oid": _endpoint(relation.to_symbol_id),
|
|
144
|
+
"edge_type": relation.relation_type.name,
|
|
145
|
+
},
|
|
146
|
+
"props": {
|
|
147
|
+
"confidence": relation.confidence,
|
|
148
|
+
"context": relation.context,
|
|
149
|
+
# call-site line (0 when unknown) — a prop, NOT part of edge identity.
|
|
150
|
+
"line": relation.call_site.start_line if relation.call_site is not None else 0,
|
|
151
|
+
# legacy endpoints for mixed-read resolution.
|
|
152
|
+
"legacy_from_oid": f"graph/{repo_graph_id}/node/{relation.from_symbol_id}",
|
|
153
|
+
"legacy_to_oid": f"graph/{repo_graph_id}/node/{relation.to_symbol_id}",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def to_proxima_records(
|
|
159
|
+
parsed: ParsedCode,
|
|
160
|
+
repo_graph_id: str,
|
|
161
|
+
branch_id: str = "main",
|
|
162
|
+
embedder: Embedder | None = None,
|
|
163
|
+
*,
|
|
164
|
+
stable_oid: bool | None = None,
|
|
165
|
+
) -> list[dict[str, Any]]:
|
|
166
|
+
"""Project an entire parsed file to node + edge records (shapes only).
|
|
167
|
+
|
|
168
|
+
Builds the legacy-id → canonical-oid map once so edges resolve to the same key the
|
|
169
|
+
nodes use under the gate (ADR-044). Pass ``stable_oid=True`` (or set
|
|
170
|
+
``VICTOR_CODEGRAPH_STABLE_OID``) to emit canonical oids as primary.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
id_map = {s.id: _canonical_symbol_oid(repo_graph_id, s) for s in parsed.symbols}
|
|
174
|
+
records = [
|
|
175
|
+
symbol_to_record(s, repo_graph_id, branch_id, embedder, stable_oid=stable_oid)
|
|
176
|
+
for s in parsed.symbols
|
|
177
|
+
]
|
|
178
|
+
records.extend(
|
|
179
|
+
relation_to_record(r, repo_graph_id, branch_id, id_map=id_map, stable_oid=stable_oid)
|
|
180
|
+
for r in parsed.relations
|
|
181
|
+
)
|
|
182
|
+
return records
|
|
@@ -13,6 +13,23 @@ from dataclasses import dataclass, field
|
|
|
13
13
|
from enum import IntEnum
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
# CANONICAL LINE-NUMBER CONTRACT
|
|
18
|
+
#
|
|
19
|
+
# Every line number this package emits (``SourceLocation.start_line`` / ``end_line``,
|
|
20
|
+
# ``CodeChunk.metadata['start_line']``/``['end_line']``, the ``call_site`` line on a
|
|
21
|
+
# ``CodeRelation``) is **1-based** — line 1 is the first line of the file. This matches
|
|
22
|
+
# editors (Monaco/VS Code), pgwire, and how humans reference code.
|
|
23
|
+
#
|
|
24
|
+
# victor-codegraph is the SHARED parser across Victor, the ProximaDB SDK, and AnvaiOps,
|
|
25
|
+
# so this convention is the cross-surface contract: a symbol's ``(file, name, line)``
|
|
26
|
+
# coordinate must be identical in every consumer for the correlated code-graph to
|
|
27
|
+
# reconcile. Consumers MUST pass these line numbers through unchanged — do NOT convert
|
|
28
|
+
# to 0-based (a `- 1` shift) for an internal/tree-sitter-compat convention; that breaks
|
|
29
|
+
# cross-surface reconciliation. Byte offsets (``byte_offset``/``start_pos``) are 0-based.
|
|
30
|
+
LINE_BASE = 1
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
16
33
|
|
|
17
34
|
class CodeSymbolType(IntEnum):
|
|
18
35
|
"""Kinds of code symbol that can be extracted (superset across languages)."""
|
|
@@ -62,7 +79,13 @@ class CodeRelationType(IntEnum):
|
|
|
62
79
|
|
|
63
80
|
@dataclass
|
|
64
81
|
class SourceLocation:
|
|
65
|
-
"""Where a symbol lives in source.
|
|
82
|
+
"""Where a symbol lives in source.
|
|
83
|
+
|
|
84
|
+
Lines (``start_line``/``end_line``) are **1-based** (see ``LINE_BASE`` and the
|
|
85
|
+
canonical line-number contract at the top of this module); byte offsets
|
|
86
|
+
(``byte_offset``/``byte_length``) are 0-based. Pass line numbers through
|
|
87
|
+
unchanged — do not convert to 0-based.
|
|
88
|
+
"""
|
|
66
89
|
|
|
67
90
|
file_path: str
|
|
68
91
|
start_line: int = 0
|
|
@@ -136,12 +159,42 @@ class CodeChunk:
|
|
|
136
159
|
|
|
137
160
|
|
|
138
161
|
def deterministic_symbol_id(file_path: str, name: str, line: int, column: int = 0) -> str:
|
|
139
|
-
"""Stable 16-hex id keyed on (file, name, line, col) — same input, same id.
|
|
162
|
+
"""Stable 16-hex id keyed on (file, name, line, col) — same input, same id.
|
|
163
|
+
|
|
164
|
+
LINE-COUPLED: editing code above a symbol moves its line and changes this id.
|
|
165
|
+
Retained as the *legacy* alias during the ADR-044 mixed-read bake; new identity
|
|
166
|
+
is :func:`stable_symbol_oid`.
|
|
167
|
+
"""
|
|
140
168
|
|
|
141
169
|
key = f"{file_path}:{name}:{line}:{column}"
|
|
142
170
|
return hashlib.sha256(key.encode("utf-8")).hexdigest()[:16]
|
|
143
171
|
|
|
144
172
|
|
|
173
|
+
def stable_symbol_oid(
|
|
174
|
+
repo_id: str,
|
|
175
|
+
language: str,
|
|
176
|
+
fully_qualified_name: str,
|
|
177
|
+
signature: str | None,
|
|
178
|
+
) -> str:
|
|
179
|
+
"""Line-independent stable symbol id (ADR-044) — the correlated-CPG join key.
|
|
180
|
+
|
|
181
|
+
Identity = the structural coordinates that survive edits which don't change *what
|
|
182
|
+
the symbol is*: ``repo ‖ language ‖ fully_qualified_name ‖ overload_discriminator``.
|
|
183
|
+
The FQN already encodes ``path::scope::name`` (no line), and ``signature`` (the
|
|
184
|
+
parameter list / arity) disambiguates same-name overloads. **Line, column, byte
|
|
185
|
+
offset and body text are NOT in identity** — they are metadata / a separate
|
|
186
|
+
``content_version``. So a symbol that merely moves lines (or whose body is edited)
|
|
187
|
+
keeps this id; it changes only on rename / move / signature change.
|
|
188
|
+
|
|
189
|
+
Uses ``blake2b`` (stdlib, dependency-free) and the unit-separator ``\\x1f`` as the
|
|
190
|
+
field delimiter so a coordinate containing the delimiter can't forge a collision.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
disc = signature or ""
|
|
194
|
+
key = "\x1f".join((repo_id, language, fully_qualified_name, disc))
|
|
195
|
+
return hashlib.blake2b(key.encode("utf-8"), digest_size=8).hexdigest()
|
|
196
|
+
|
|
197
|
+
|
|
145
198
|
def content_hash(content: str) -> str:
|
|
146
199
|
"""SHA-256 of file content, for change detection on the re-index hot path."""
|
|
147
200
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Repo-walking convenience — iterate source files and chunk/parse a whole tree.
|
|
2
|
+
|
|
3
|
+
Every consumer (Victor codebase indexing, AnvaiOps code-graph-sync) needs the same
|
|
4
|
+
loop: walk a repository, skip noise directories, and chunk/parse each source file
|
|
5
|
+
whose extension maps to a known language. This puts that loop in one place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .config import ChunkConfig
|
|
15
|
+
from .languages import detect_language
|
|
16
|
+
from .model import CodeChunk, ParsedCode
|
|
17
|
+
from .parser import chunk as _chunk
|
|
18
|
+
from .parser import parse as _parse
|
|
19
|
+
|
|
20
|
+
# Directories never worth indexing (VCS, caches, vendored deps, build output).
|
|
21
|
+
DEFAULT_EXCLUDE_DIRS: frozenset[str] = frozenset(
|
|
22
|
+
{
|
|
23
|
+
".git",
|
|
24
|
+
".hg",
|
|
25
|
+
".svn",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
".venv",
|
|
29
|
+
"venv",
|
|
30
|
+
".mypy_cache",
|
|
31
|
+
".pytest_cache",
|
|
32
|
+
".ruff_cache",
|
|
33
|
+
".tox",
|
|
34
|
+
"dist",
|
|
35
|
+
"build",
|
|
36
|
+
"target",
|
|
37
|
+
".idea",
|
|
38
|
+
".gradle",
|
|
39
|
+
".next",
|
|
40
|
+
"site-packages",
|
|
41
|
+
"vendor",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def iter_source_files(
|
|
47
|
+
root: str | os.PathLike[str],
|
|
48
|
+
*,
|
|
49
|
+
languages: list[str] | None = None,
|
|
50
|
+
exclude_dirs: frozenset[str] | set[str] = DEFAULT_EXCLUDE_DIRS,
|
|
51
|
+
follow_symlinks: bool = False,
|
|
52
|
+
) -> Iterator[Path]:
|
|
53
|
+
"""Yield files under ``root`` whose extension maps to a known language.
|
|
54
|
+
|
|
55
|
+
Skips ``exclude_dirs`` and dot-directories. If ``root`` is itself a file, yields it
|
|
56
|
+
(when its extension is recognized). ``languages`` restricts to those language names.
|
|
57
|
+
"""
|
|
58
|
+
root_path = Path(root)
|
|
59
|
+
if root_path.is_file():
|
|
60
|
+
if detect_language(str(root_path)) is not None:
|
|
61
|
+
yield root_path
|
|
62
|
+
return
|
|
63
|
+
allowed = set(languages) if languages else None
|
|
64
|
+
excl = set(exclude_dirs)
|
|
65
|
+
for dirpath, dirnames, filenames in os.walk(root_path, followlinks=follow_symlinks):
|
|
66
|
+
# Prune in place so os.walk does not descend into excluded/hidden dirs.
|
|
67
|
+
dirnames[:] = [d for d in dirnames if d not in excl and not d.startswith(".")]
|
|
68
|
+
for fn in sorted(filenames):
|
|
69
|
+
p = Path(dirpath) / fn
|
|
70
|
+
lang = detect_language(str(p))
|
|
71
|
+
if lang is None:
|
|
72
|
+
continue
|
|
73
|
+
if allowed is not None and lang not in allowed:
|
|
74
|
+
continue
|
|
75
|
+
yield p
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse_path(
|
|
79
|
+
path: str | os.PathLike[str], *, encoding: str = "utf-8"
|
|
80
|
+
) -> ParsedCode | None:
|
|
81
|
+
"""Parse a single file into symbols + relations. Returns ``None`` if unreadable."""
|
|
82
|
+
p = Path(path)
|
|
83
|
+
try:
|
|
84
|
+
content = p.read_text(encoding=encoding)
|
|
85
|
+
except (OSError, UnicodeDecodeError):
|
|
86
|
+
return None
|
|
87
|
+
return _parse(content, file_path=str(p))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def chunk_path(
|
|
91
|
+
path: str | os.PathLike[str],
|
|
92
|
+
config: ChunkConfig | None = None,
|
|
93
|
+
*,
|
|
94
|
+
encoding: str = "utf-8",
|
|
95
|
+
) -> list[CodeChunk]:
|
|
96
|
+
"""Read + chunk a single file. Returns an empty list if it can't be read."""
|
|
97
|
+
p = Path(path)
|
|
98
|
+
try:
|
|
99
|
+
content = p.read_text(encoding=encoding)
|
|
100
|
+
except (OSError, UnicodeDecodeError):
|
|
101
|
+
return []
|
|
102
|
+
return _chunk(content, file_path=str(p), config=config)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def chunk_repo(
|
|
106
|
+
root: str | os.PathLike[str],
|
|
107
|
+
config: ChunkConfig | None = None,
|
|
108
|
+
*,
|
|
109
|
+
languages: list[str] | None = None,
|
|
110
|
+
) -> Iterator[CodeChunk]:
|
|
111
|
+
"""Walk ``root`` and yield chunks for every source file (streaming, low-memory)."""
|
|
112
|
+
for p in iter_source_files(root, languages=languages):
|
|
113
|
+
yield from chunk_path(p, config)
|
|
@@ -116,7 +116,12 @@ def _name_of(node, src: bytes) -> str | None:
|
|
|
116
116
|
if field is not None:
|
|
117
117
|
return _text(field, src)
|
|
118
118
|
for child in _children(node):
|
|
119
|
-
if child.type in (
|
|
119
|
+
if child.type in (
|
|
120
|
+
"identifier",
|
|
121
|
+
"type_identifier",
|
|
122
|
+
"field_identifier",
|
|
123
|
+
"property_identifier",
|
|
124
|
+
):
|
|
120
125
|
return _text(child, src)
|
|
121
126
|
return None
|
|
122
127
|
|
|
@@ -129,9 +134,7 @@ def _walk_collect(node, src, file_path, language, scope, symbols, relations):
|
|
|
129
134
|
stype = (
|
|
130
135
|
CodeSymbolType.STRUCT
|
|
131
136
|
if t.startswith("struct")
|
|
132
|
-
else CodeSymbolType.INTERFACE
|
|
133
|
-
if "interface" in t
|
|
134
|
-
else CodeSymbolType.CLASS
|
|
137
|
+
else CodeSymbolType.INTERFACE if "interface" in t else CodeSymbolType.CLASS
|
|
135
138
|
)
|
|
136
139
|
sym = _mk(child, src, file_path, language, name, stype, scope)
|
|
137
140
|
symbols.append(sym)
|
|
@@ -170,9 +173,33 @@ def _handle_const_arrow(node, src, file_path, language, symbols):
|
|
|
170
173
|
value = decl.child_by_field_name("value")
|
|
171
174
|
if name_node is not None and value is not None and value.type == "arrow_function":
|
|
172
175
|
name = _text(name_node, src)
|
|
173
|
-
symbols.append(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
symbols.append(_mk(decl, src, file_path, language, name, CodeSymbolType.FUNCTION, []))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Parameter-list node types across tree-sitter grammars (Python/JS-TS/Java/Go/Rust/C…).
|
|
180
|
+
_PARAM_NODE_TYPES = frozenset(
|
|
181
|
+
{
|
|
182
|
+
"parameters",
|
|
183
|
+
"formal_parameters",
|
|
184
|
+
"parameter_list",
|
|
185
|
+
"argument_list",
|
|
186
|
+
"function_value_parameters", # Kotlin/Rust-style
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _param_signature(node, src) -> str | None:
|
|
192
|
+
"""The symbol's parameter-list text — the ADR-044 overload discriminator.
|
|
193
|
+
|
|
194
|
+
Stable under line moves (it's the params, not the position). ``None`` for symbols
|
|
195
|
+
with no parameter node (classes/structs), giving an empty discriminator there.
|
|
196
|
+
Python's AST parser supplies a richer ``signature``; this is the tree-sitter path.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
for ch in _children(node):
|
|
200
|
+
if _attr(ch, "type") in _PARAM_NODE_TYPES:
|
|
201
|
+
return _text(ch, src)
|
|
202
|
+
return None
|
|
176
203
|
|
|
177
204
|
|
|
178
205
|
def _mk(node, src, file_path, language, name, stype, scope) -> CodeSymbol:
|
|
@@ -184,6 +211,7 @@ def _mk(node, src, file_path, language, name, stype, scope) -> CodeSymbol:
|
|
|
184
211
|
symbol_type=stype,
|
|
185
212
|
fully_qualified_name=fqn,
|
|
186
213
|
simple_name=name,
|
|
214
|
+
signature=_param_signature(node, src),
|
|
187
215
|
location=SourceLocation(
|
|
188
216
|
file_path=file_path,
|
|
189
217
|
start_line=start_line,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: victor-codegraph
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Code->CPG chunker: tree-sitter symbol + relation extraction, size-capped chunks, ProximaRecord projection. Shared by Victor, ProximaDB SDK, and AnvaiOps.
|
|
5
5
|
Author-email: Vijaykumar Singh <singhvjd@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
tests/test_adapter.py
|
|
4
|
+
tests/test_line_base_contract.py
|
|
4
5
|
tests/test_python_parser.py
|
|
6
|
+
tests/test_repo.py
|
|
5
7
|
tests/test_sizing_and_chunk.py
|
|
8
|
+
tests/test_symbol_oid_contract.py
|
|
9
|
+
tests/test_symbol_oid_parity.py
|
|
6
10
|
tests/test_treesitter_jsts.py
|
|
7
11
|
victor_codegraph/__init__.py
|
|
8
12
|
victor_codegraph/adapter.py
|
|
@@ -11,6 +15,7 @@ victor_codegraph/languages.py
|
|
|
11
15
|
victor_codegraph/model.py
|
|
12
16
|
victor_codegraph/parser.py
|
|
13
17
|
victor_codegraph/python_parser.py
|
|
18
|
+
victor_codegraph/repo.py
|
|
14
19
|
victor_codegraph/sizing.py
|
|
15
20
|
victor_codegraph/treesitter_parser.py
|
|
16
21
|
victor_codegraph.egg-info/PKG-INFO
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""Projection to the ProximaDB substrate-keystone ``ProximaRecord`` shape.
|
|
2
|
-
|
|
3
|
-
Per ProximaDB ``CODE_GRAPH_CORRELATED_SUBSTRATE_2026_06_22.adoc`` a code symbol is *one*
|
|
4
|
-
record addressable as a relational row, a graph node, and a vector at once. This adapter
|
|
5
|
-
emits the **shape** as plain dicts — it does not import proximadb, embed, or write. The
|
|
6
|
-
consumer (Victor embedded, AnvaiOps service) supplies the embedder and the DB write.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from typing import Any, Callable
|
|
12
|
-
|
|
13
|
-
from .model import CodeRelation, CodeSymbol, ParsedCode
|
|
14
|
-
|
|
15
|
-
Embedder = Callable[[str], list[float]]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _symbol_oid(repo_graph_id: str, symbol: CodeSymbol) -> str:
|
|
19
|
-
return f"graph/{repo_graph_id}/node/{symbol.id}"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def symbol_to_record(
|
|
23
|
-
symbol: CodeSymbol,
|
|
24
|
-
repo_graph_id: str,
|
|
25
|
-
branch_id: str = "main",
|
|
26
|
-
embedder: Embedder | None = None,
|
|
27
|
-
model_id: str = "bge-small-en-v1.5",
|
|
28
|
-
dim: int = 384,
|
|
29
|
-
) -> dict[str, Any]:
|
|
30
|
-
"""Project one symbol to a node record (row + graph node + optional vector)."""
|
|
31
|
-
|
|
32
|
-
oid = _symbol_oid(repo_graph_id, symbol)
|
|
33
|
-
record: dict[str, Any] = {
|
|
34
|
-
"oid": oid,
|
|
35
|
-
"labels": ["graph_node", "code_symbol"],
|
|
36
|
-
"branch_id": branch_id,
|
|
37
|
-
"props": {
|
|
38
|
-
"name": symbol.simple_name,
|
|
39
|
-
"fully_qualified_name": symbol.fully_qualified_name,
|
|
40
|
-
"file": symbol.location.file_path,
|
|
41
|
-
"line": symbol.location.start_line,
|
|
42
|
-
"end_line": symbol.location.end_line,
|
|
43
|
-
"lang": symbol.language,
|
|
44
|
-
"ast_kind": symbol.symbol_type.name,
|
|
45
|
-
"signature": symbol.signature,
|
|
46
|
-
"visibility": "private" if "private" in symbol.modifiers else "public",
|
|
47
|
-
"module_path": "::".join(symbol.scope_chain),
|
|
48
|
-
"snippet": symbol.source_code,
|
|
49
|
-
"documentation": symbol.documentation,
|
|
50
|
-
},
|
|
51
|
-
"embeddings": [],
|
|
52
|
-
}
|
|
53
|
-
if embedder is not None:
|
|
54
|
-
record["embeddings"].append(
|
|
55
|
-
{
|
|
56
|
-
"model_id": model_id,
|
|
57
|
-
"modality": "code",
|
|
58
|
-
"dim": dim,
|
|
59
|
-
"values": embedder(symbol.source_code),
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
return record
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def relation_to_record(relation: CodeRelation, repo_graph_id: str, branch_id: str = "main") -> dict[str, Any]:
|
|
66
|
-
"""Project one relation to an edge record."""
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
"labels": ["graph_edge"],
|
|
70
|
-
"branch_id": branch_id,
|
|
71
|
-
"edge": {
|
|
72
|
-
"from_oid": f"graph/{repo_graph_id}/node/{relation.from_symbol_id}",
|
|
73
|
-
"to_oid": f"graph/{repo_graph_id}/node/{relation.to_symbol_id}",
|
|
74
|
-
"edge_type": relation.relation_type.name,
|
|
75
|
-
},
|
|
76
|
-
"props": {"confidence": relation.confidence, "context": relation.context},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def to_proxima_records(
|
|
81
|
-
parsed: ParsedCode,
|
|
82
|
-
repo_graph_id: str,
|
|
83
|
-
branch_id: str = "main",
|
|
84
|
-
embedder: Embedder | None = None,
|
|
85
|
-
) -> list[dict[str, Any]]:
|
|
86
|
-
"""Project an entire parsed file to node + edge records (shapes only)."""
|
|
87
|
-
|
|
88
|
-
records = [
|
|
89
|
-
symbol_to_record(s, repo_graph_id, branch_id, embedder) for s in parsed.symbols
|
|
90
|
-
]
|
|
91
|
-
records.extend(
|
|
92
|
-
relation_to_record(r, repo_graph_id, branch_id) for r in parsed.relations
|
|
93
|
-
)
|
|
94
|
-
return records
|
|
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
|
{victor_codegraph-0.1.0 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|