victor-codegraph 0.1.1__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.
Files changed (28) hide show
  1. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/PKG-INFO +1 -1
  2. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/pyproject.toml +1 -1
  3. victor_codegraph-0.1.2/tests/test_symbol_oid_contract.py +81 -0
  4. victor_codegraph-0.1.2/tests/test_symbol_oid_parity.py +77 -0
  5. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/__init__.py +5 -1
  6. victor_codegraph-0.1.2/victor_codegraph/adapter.py +182 -0
  7. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/model.py +31 -1
  8. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/treesitter_parser.py +35 -7
  9. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/PKG-INFO +1 -1
  10. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/SOURCES.txt +2 -0
  11. victor_codegraph-0.1.1/victor_codegraph/adapter.py +0 -99
  12. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/README.md +0 -0
  13. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/setup.cfg +0 -0
  14. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_adapter.py +0 -0
  15. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_line_base_contract.py +0 -0
  16. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_python_parser.py +0 -0
  17. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_repo.py +0 -0
  18. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_sizing_and_chunk.py +0 -0
  19. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/tests/test_treesitter_jsts.py +0 -0
  20. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/config.py +0 -0
  21. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/languages.py +0 -0
  22. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/parser.py +0 -0
  23. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/python_parser.py +0 -0
  24. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/repo.py +0 -0
  25. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph/sizing.py +0 -0
  26. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/dependency_links.txt +0 -0
  27. {victor_codegraph-0.1.1 → victor_codegraph-0.1.2}/victor_codegraph.egg-info/requires.txt +0 -0
  28. {victor_codegraph-0.1.1 → 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.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.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"
@@ -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
@@ -24,11 +24,13 @@ from .model import (
24
24
  CodeSymbolType,
25
25
  ParsedCode,
26
26
  SourceLocation,
27
+ deterministic_symbol_id,
28
+ stable_symbol_oid,
27
29
  )
28
30
  from .parser import chunk, parse
29
31
  from .repo import chunk_path, chunk_repo, iter_source_files, parse_path
30
32
 
31
- __version__ = "0.1.1"
33
+ __version__ = "0.1.2"
32
34
 
33
35
  __all__ = [
34
36
  "__version__",
@@ -51,4 +53,6 @@ __all__ = [
51
53
  "CodeRelationType",
52
54
  "ParsedCode",
53
55
  "SourceLocation",
56
+ "stable_symbol_oid",
57
+ "deterministic_symbol_id",
54
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
@@ -159,12 +159,42 @@ class CodeChunk:
159
159
 
160
160
 
161
161
  def deterministic_symbol_id(file_path: str, name: str, line: int, column: int = 0) -> str:
162
- """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
+ """
163
168
 
164
169
  key = f"{file_path}:{name}:{line}:{column}"
165
170
  return hashlib.sha256(key.encode("utf-8")).hexdigest()[:16]
166
171
 
167
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
+
168
198
  def content_hash(content: str) -> str:
169
199
  """SHA-256 of file content, for change detection on the re-index hot path."""
170
200
 
@@ -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 ("identifier", "type_identifier", "field_identifier", "property_identifier"):
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
- _mk(decl, src, file_path, language, name, CodeSymbolType.FUNCTION, [])
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.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
@@ -5,6 +5,8 @@ tests/test_line_base_contract.py
5
5
  tests/test_python_parser.py
6
6
  tests/test_repo.py
7
7
  tests/test_sizing_and_chunk.py
8
+ tests/test_symbol_oid_contract.py
9
+ tests/test_symbol_oid_parity.py
8
10
  tests/test_treesitter_jsts.py
9
11
  victor_codegraph/__init__.py
10
12
  victor_codegraph/adapter.py
@@ -1,99 +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": {
77
- "confidence": relation.confidence,
78
- "context": relation.context,
79
- # call-site line (0 when unknown) — lets the graph carry where the edge fires.
80
- "line": relation.call_site.start_line if relation.call_site is not None else 0,
81
- },
82
- }
83
-
84
-
85
- def to_proxima_records(
86
- parsed: ParsedCode,
87
- repo_graph_id: str,
88
- branch_id: str = "main",
89
- embedder: Embedder | None = None,
90
- ) -> list[dict[str, Any]]:
91
- """Project an entire parsed file to node + edge records (shapes only)."""
92
-
93
- records = [
94
- symbol_to_record(s, repo_graph_id, branch_id, embedder) for s in parsed.symbols
95
- ]
96
- records.extend(
97
- relation_to_record(r, repo_graph_id, branch_id) for r in parsed.relations
98
- )
99
- return records