victor-codegraph 0.9.0__tar.gz → 0.10.1__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 (48) hide show
  1. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/PKG-INFO +58 -4
  2. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/README.md +55 -3
  3. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/pyproject.toml +5 -1
  4. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_adapter.py +9 -0
  5. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_adapter_validation_v2.py +27 -0
  6. victor_codegraph-0.10.1/tests/test_diagnostics_v2.py +46 -0
  7. victor_codegraph-0.10.1/tests/test_embedding_contract.py +145 -0
  8. victor_codegraph-0.10.1/tests/test_repository_delta_v2.py +128 -0
  9. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_treesitter_jsts.py +35 -0
  10. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/__init__.py +17 -1
  11. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/adapter.py +19 -4
  12. victor_codegraph-0.10.1/victor_codegraph/embeddings.py +196 -0
  13. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/parser.py +18 -10
  14. victor_codegraph-0.10.1/victor_codegraph/py.typed +0 -0
  15. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/repository_index.py +59 -8
  16. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/treesitter_parser.py +3 -1
  17. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/PKG-INFO +58 -4
  18. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/SOURCES.txt +3 -0
  19. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/requires.txt +3 -0
  20. victor_codegraph-0.9.0/tests/test_diagnostics_v2.py +0 -26
  21. victor_codegraph-0.9.0/tests/test_repository_delta_v2.py +0 -46
  22. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/setup.cfg +0 -0
  23. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_chunk_contract_v2.py +0 -0
  24. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_identity_v2.py +0 -0
  25. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_line_base_contract.py +0 -0
  26. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_manifest.py +0 -0
  27. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_python_parser.py +0 -0
  28. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_relations_v2.py +0 -0
  29. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_repo.py +0 -0
  30. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_sizing_and_chunk.py +0 -0
  31. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_sizing_overlap.py +0 -0
  32. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_symbol_oid_contract.py +0 -0
  33. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_symbol_oid_parity.py +0 -0
  34. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_treesitter_languages.py +0 -0
  35. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/tests/test_treesitter_relations.py +0 -0
  36. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/config.py +0 -0
  37. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/javascript_repository.py +0 -0
  38. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/languages.py +0 -0
  39. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/manifest.py +0 -0
  40. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/model.py +0 -0
  41. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/python_parser.py +0 -0
  42. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/repo.py +0 -0
  43. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/resolution.py +0 -0
  44. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/rust_repository.py +0 -0
  45. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/sizing.py +0 -0
  46. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph/treesitter_tables.py +0 -0
  47. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/dependency_links.txt +0 -0
  48. {victor_codegraph-0.9.0 → victor_codegraph-0.10.1}/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.9.0
3
+ Version: 0.10.1
4
4
  Summary: Repository-to-Code-Semantic-Graph compiler with stable identity, bounded chunks, incremental deltas, and ProximaRecord projection.
5
5
  Author-email: Vijaykumar Singh <vijay@anvaiops.com>
6
6
  License: Apache-2.0
@@ -16,6 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Topic :: Software Development :: Libraries
17
17
  Requires-Python: >=3.12
18
18
  Description-Content-Type: text/markdown
19
+ Provides-Extra: embeddings
20
+ Requires-Dist: sentence-transformers<6,>=3; extra == "embeddings"
19
21
  Provides-Extra: treesitter
20
22
  Requires-Dist: tree-sitter>=0.26.0; extra == "treesitter"
21
23
  Requires-Dist: tree-sitter-language-pack>=1.21.3; extra == "treesitter"
@@ -54,7 +56,7 @@ gaps:
54
56
  ## Install
55
57
 
56
58
  Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/).
57
- This source tree is version **0.9.0**; consumers should use a compatible `<1.0` pin.
59
+ This source tree is version **0.10.1**; consumers should use a compatible `<1.0` pin.
58
60
 
59
61
  ```bash
60
62
  # from PyPI
@@ -126,10 +128,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
126
128
 
127
129
  ## Status
128
130
 
129
- `0.9.0` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
131
+ `0.10.1` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
130
132
  Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
131
133
  symbols-tier or fallback status rather than implying full fidelity.
132
134
 
135
+ ### Ambiguous declarations
136
+
137
+ Duplicate structural identities retain raw declarations in `ParsedRepository.files`
138
+ and report each conflicting source location. Repository projection emits only the
139
+ canonical FILE marker for that file, with typed diagnostics, declaration spans and
140
+ omitted symbol/relation counts in metadata. Calls into its declarations remain
141
+ unresolved; healthy imports can still point to its diagnostic marker. Repairing the
142
+ source restores normal projection through deletion-complete repository deltas.
143
+ Direct record projection rejects duplicate identities before invoking an embedder,
144
+ including when legacy OIDs are selected.
145
+
146
+ `identity_collisions` groups use the existing `symbol_id` with `resolution: ambiguous`.
147
+ Each declaration retains `name`, `qualified_name`, `symbol_type` and its full
148
+ `source_span`; these are inspectable evidence, not separately resolved graph nodes.
149
+ The record adapter forwards these groups, projection status and omitted counts only
150
+ when present, without copying arbitrary symbol metadata into exported properties.
151
+
152
+ ### TypeScript grammar limitation
153
+
154
+ The current tree-sitter grammar reports a partial parse for inline import types
155
+ in generic calls, for example `load<typeof import('child_process')>()`, even though
156
+ TypeScript accepts that syntax. A named type alias (`type ChildProcessModule =
157
+ typeof import('child_process')`, then `load<ChildProcessModule>()`) is supported.
158
+ Partial status and syntax diagnostics remain visible; the parser does not rewrite
159
+ source or infer missing relationships.
160
+
133
161
  ### Rust graph fidelity
134
162
 
135
163
  Rust indexing retains inline module scopes and discriminates same-named trait
@@ -143,4 +171,30 @@ workspace-inherited dependencies, re-export chains, macros and unknown receiver
143
171
  types retain unresolved references. Glob imports retain a module dependency but
144
172
  do not guess call targets. This is a conservative static graph, not rustc name
145
173
  resolution or evidence that a call executes. Publish this package before a Victor
146
- release that requires its `0.9.0` repository graph contract.
174
+ release that requires its exact repository graph contract.
175
+
176
+
177
+ ### Embedding conformance (0.10.0)
178
+
179
+ The accepted FEP-0040 supplies store-neutral
180
+ `EmbeddingProfile`, `SymbolEmbedding`, `embed_symbols` and an optional
181
+ `SentenceTransformerEncoder`. The base compiler stays dependency-free. The
182
+ `embeddings` extra loads sentence-transformers only when explicitly selected;
183
+ private source is not sent to a remote model by default.
184
+
185
+ The contract uses canonical compiler IDs and `symbol-summary-v1` (name,
186
+ signature and documentation), with an immutable model revision, dimensions,
187
+ token limit, normalization, query prefix and separate input/source hashes.
188
+ Inference errors propagate; hash placeholders, zero vectors and malformed batches
189
+ are not semantic evidence. File/container symbols marked `embedding_excluded`
190
+ remain graph evidence only.
191
+
192
+ These APIs are introduced in 0.10.0 and are unavailable in 0.9.0. Install the
193
+ exact published `victor-codegraph[treesitter,embeddings]==0.10.0` when selecting
194
+ local model execution; the base package has no mandatory model dependency.
195
+ Current evidence covers
196
+ synthetic document-vector persistence parity and local LanceDB collection/query
197
+ profile enforcement. Labeled relevance and hosted lifecycle remain separate
198
+ acceptance gates. Victor binds a new LanceDB table to the complete profile in its
199
+ Arrow schema and rejects incompatible query encoders and mixed/orphan row markers.
200
+ Legacy tables require a fresh destination; they are never silently relabeled.
@@ -25,7 +25,7 @@ gaps:
25
25
  ## Install
26
26
 
27
27
  Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/).
28
- This source tree is version **0.9.0**; consumers should use a compatible `<1.0` pin.
28
+ This source tree is version **0.10.1**; consumers should use a compatible `<1.0` pin.
29
29
 
30
30
  ```bash
31
31
  # from PyPI
@@ -97,10 +97,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
97
97
 
98
98
  ## Status
99
99
 
100
- `0.9.0` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
100
+ `0.10.1` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
101
101
  Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
102
102
  symbols-tier or fallback status rather than implying full fidelity.
103
103
 
104
+ ### Ambiguous declarations
105
+
106
+ Duplicate structural identities retain raw declarations in `ParsedRepository.files`
107
+ and report each conflicting source location. Repository projection emits only the
108
+ canonical FILE marker for that file, with typed diagnostics, declaration spans and
109
+ omitted symbol/relation counts in metadata. Calls into its declarations remain
110
+ unresolved; healthy imports can still point to its diagnostic marker. Repairing the
111
+ source restores normal projection through deletion-complete repository deltas.
112
+ Direct record projection rejects duplicate identities before invoking an embedder,
113
+ including when legacy OIDs are selected.
114
+
115
+ `identity_collisions` groups use the existing `symbol_id` with `resolution: ambiguous`.
116
+ Each declaration retains `name`, `qualified_name`, `symbol_type` and its full
117
+ `source_span`; these are inspectable evidence, not separately resolved graph nodes.
118
+ The record adapter forwards these groups, projection status and omitted counts only
119
+ when present, without copying arbitrary symbol metadata into exported properties.
120
+
121
+ ### TypeScript grammar limitation
122
+
123
+ The current tree-sitter grammar reports a partial parse for inline import types
124
+ in generic calls, for example `load<typeof import('child_process')>()`, even though
125
+ TypeScript accepts that syntax. A named type alias (`type ChildProcessModule =
126
+ typeof import('child_process')`, then `load<ChildProcessModule>()`) is supported.
127
+ Partial status and syntax diagnostics remain visible; the parser does not rewrite
128
+ source or infer missing relationships.
129
+
104
130
  ### Rust graph fidelity
105
131
 
106
132
  Rust indexing retains inline module scopes and discriminates same-named trait
@@ -114,4 +140,30 @@ workspace-inherited dependencies, re-export chains, macros and unknown receiver
114
140
  types retain unresolved references. Glob imports retain a module dependency but
115
141
  do not guess call targets. This is a conservative static graph, not rustc name
116
142
  resolution or evidence that a call executes. Publish this package before a Victor
117
- release that requires its `0.9.0` repository graph contract.
143
+ release that requires its exact repository graph contract.
144
+
145
+
146
+ ### Embedding conformance (0.10.0)
147
+
148
+ The accepted FEP-0040 supplies store-neutral
149
+ `EmbeddingProfile`, `SymbolEmbedding`, `embed_symbols` and an optional
150
+ `SentenceTransformerEncoder`. The base compiler stays dependency-free. The
151
+ `embeddings` extra loads sentence-transformers only when explicitly selected;
152
+ private source is not sent to a remote model by default.
153
+
154
+ The contract uses canonical compiler IDs and `symbol-summary-v1` (name,
155
+ signature and documentation), with an immutable model revision, dimensions,
156
+ token limit, normalization, query prefix and separate input/source hashes.
157
+ Inference errors propagate; hash placeholders, zero vectors and malformed batches
158
+ are not semantic evidence. File/container symbols marked `embedding_excluded`
159
+ remain graph evidence only.
160
+
161
+ These APIs are introduced in 0.10.0 and are unavailable in 0.9.0. Install the
162
+ exact published `victor-codegraph[treesitter,embeddings]==0.10.0` when selecting
163
+ local model execution; the base package has no mandatory model dependency.
164
+ Current evidence covers
165
+ synthetic document-vector persistence parity and local LanceDB collection/query
166
+ profile enforcement. Labeled relevance and hosted lifecycle remain separate
167
+ acceptance gates. Victor binds a new LanceDB table to the complete profile in its
168
+ Arrow schema and rejects incompatible query encoders and mixed/orphan row markers.
169
+ Legacy tables require a fresh destination; they are never silently relabeled.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "victor-codegraph"
7
- version = "0.9.0"
7
+ version = "0.10.1"
8
8
  description = "Repository-to-Code-Semantic-Graph compiler with stable identity, bounded chunks, incremental deltas, and ProximaRecord projection."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -40,6 +40,7 @@ classifiers = [
40
40
  dependencies = []
41
41
 
42
42
  [project.optional-dependencies]
43
+ embeddings = ["sentence-transformers>=3,<6"]
43
44
  treesitter = [
44
45
  "tree-sitter>=0.26.0",
45
46
  "tree-sitter-language-pack>=1.21.3",
@@ -77,3 +78,6 @@ select = ["E4", "E7", "E9", "F"]
77
78
  [tool.pytest.ini_options]
78
79
  testpaths = ["tests"]
79
80
  addopts = ""
81
+
82
+ [tool.setuptools.package-data]
83
+ victor_codegraph = ["py.typed"]
@@ -26,6 +26,15 @@ def test_symbol_records_shape():
26
26
  assert n["props"]["lang"] == "python"
27
27
  assert n["props"]["ast_kind"] in ("FUNCTION", "METHOD", "CONSTRUCTOR")
28
28
  assert n["embeddings"] == [] # no embedder supplied
29
+ assert (
30
+ not {
31
+ "identity_collisions",
32
+ "projection_status",
33
+ "omitted_symbol_count",
34
+ "omitted_relation_count",
35
+ }
36
+ & n["props"].keys()
37
+ )
29
38
 
30
39
 
31
40
  def test_edge_records_reference_node_oids():
@@ -40,3 +40,30 @@ def test_legacy_oid_mode_keeps_edge_endpoints_attached():
40
40
  edge = next(r for r in records if "graph_edge" in r["labels"])
41
41
  assert edge["edge"]["from_oid"] in nodes
42
42
  assert edge["edge"]["to_oid"] in nodes
43
+
44
+
45
+ @pytest.mark.parametrize("stable_oid", [True, False])
46
+ @pytest.mark.parametrize("embedding_mode", ["embedder", "batch_embedder"])
47
+ def test_duplicate_symbol_ids_fail_before_embedding(stable_oid, embedding_mode):
48
+ from unittest.mock import Mock
49
+
50
+ parsed = parse("def f(): return 1\ndef f(): return 2\n", file_path="duplicate.py")
51
+ embedder = Mock(
52
+ return_value=[0.0, 0.0] if embedding_mode == "embedder" else [[0.0, 0.0], [0.0, 0.0]]
53
+ )
54
+ with pytest.raises(ValueError, match="identity collision"):
55
+ to_proxima_records(
56
+ parsed, "repo", stable_oid=stable_oid, dim=2, **{embedding_mode: embedder}
57
+ )
58
+ embedder.assert_not_called()
59
+
60
+
61
+ def test_symbol_alias_cannot_rebind_another_canonical_identity():
62
+ from unittest.mock import Mock
63
+
64
+ parsed = parse("def a(): pass\ndef b(): pass\n", file_path="aliases.py")
65
+ parsed.symbols[1].legacy_id = parsed.symbols[0].id
66
+ embedder = Mock(return_value=[0.0, 0.0])
67
+ with pytest.raises(ValueError, match="alias identity collision"):
68
+ to_proxima_records(parsed, "repo", embedder=embedder, dim=2)
69
+ embedder.assert_not_called()
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from victor_codegraph import CapabilityTier, ChunkConfig, ParseStatus, parse
4
+
5
+
6
+ def test_syntax_error_reports_fallback_diagnostic():
7
+ parsed = parse("def broken(:\n", file_path="bad.py")
8
+ assert parsed.status == ParseStatus.FALLBACK
9
+ assert parsed.diagnostics
10
+ assert parsed.diagnostics[0].code == "syntax_error"
11
+
12
+
13
+ def test_unknown_language_reports_fallback_instead_of_silent_success():
14
+ parsed = parse("plain text", language="unknown", file_path="x.unknown")
15
+ assert parsed.status == ParseStatus.FALLBACK
16
+ assert parsed.capability_tier == CapabilityTier.FALLBACK
17
+ assert parsed.diagnostics
18
+
19
+
20
+ def test_extract_relations_config_is_effective():
21
+ parsed = parse(
22
+ "def f():\n return g()\n",
23
+ file_path="m.py",
24
+ config=ChunkConfig(extract_relations=False),
25
+ )
26
+ assert parsed.relations == []
27
+
28
+
29
+ def test_shadowed_python_declarations_remain_explicit_identity_collisions():
30
+ parsed = parse(
31
+ "class C:\n @property\n def value(self): return 1\n"
32
+ " @property\n def value(self): return 2\n",
33
+ file_path="duplicate.py",
34
+ )
35
+ assert parsed.status == ParseStatus.PARTIAL
36
+ assert [(item.code, item.severity) for item in parsed.diagnostics] == [
37
+ ("identity_collision", "error"),
38
+ ("identity_collision", "error"),
39
+ ]
40
+ assert [(item.line, item.column) for item in parsed.diagnostics] == [(2, 4), (4, 4)]
41
+ definitions = [symbol for symbol in parsed.symbols if symbol.simple_name == "value"]
42
+ assert len(definitions) == 2
43
+ assert definitions[0].id == definitions[1].id
44
+ assert definitions[0].legacy_id != definitions[1].legacy_id
45
+ assert all("duplicate.py::C::value" in item.message for item in parsed.diagnostics)
46
+ assert all(definitions[0].id in item.message for item in parsed.diagnostics)
@@ -0,0 +1,145 @@
1
+ """One semantic input/provenance contract for local and hosted consumers."""
2
+
3
+ from dataclasses import replace
4
+ import pytest
5
+ from victor_codegraph import parse
6
+ from victor_codegraph.embeddings import (
7
+ BGE_SMALL_PROFILE,
8
+ embedding_text,
9
+ embed_symbols,
10
+ validate_vectors,
11
+ )
12
+
13
+
14
+ def test_same_symbols_produce_store_independent_embedding_evidence():
15
+ symbols = parse(
16
+ 'def pay(x):\n """Charge a customer."""\n return x\n', file_path="billing.py"
17
+ ).symbols
18
+ seen = []
19
+
20
+ def encode(texts):
21
+ seen.extend(texts)
22
+ return [[1.0] + [0.0] * 383 for _ in texts]
23
+
24
+ documents = embed_symbols(symbols, encode, BGE_SMALL_PROFILE)
25
+ assert seen == [embedding_text(s.simple_name, s.signature, s.documentation) for s in symbols]
26
+ assert [d.symbol_id for d in documents] == [s.id for s in symbols]
27
+ assert all(d.profile_id == BGE_SMALL_PROFILE.identity for d in documents)
28
+ assert all(len(d.input_hash) == 64 and len(d.source_hash) == 64 for d in documents)
29
+ assert all(len(d.values) == 384 for d in documents)
30
+
31
+
32
+ @pytest.mark.parametrize(
33
+ "vectors",
34
+ [[], [[0.0, 0.0]], [[True, 1.0]], [[float("nan"), 1.0]], [[1.0]], [[float("inf"), 1.0]]],
35
+ )
36
+ def test_invalid_or_missing_vectors_fail_without_substitution(vectors):
37
+ with pytest.raises(ValueError):
38
+ validate_vectors(vectors, count=1, dimension=2)
39
+
40
+
41
+ def test_model_preprocessing_changes_invalidate_profile_identity():
42
+ p = BGE_SMALL_PROFILE
43
+ assert (
44
+ len(
45
+ {
46
+ p.identity,
47
+ replace(p, revision="a" * 40).identity,
48
+ replace(p, normalize=True).identity,
49
+ replace(p, query_prefix="query: ").identity,
50
+ replace(p, max_tokens=256).identity,
51
+ }
52
+ )
53
+ == 5
54
+ )
55
+ with pytest.raises(ValueError, match="revision"):
56
+ replace(p, revision="main")
57
+
58
+
59
+ def test_failed_encoder_propagates_and_duplicate_symbol_ids_are_rejected():
60
+ symbols = parse("def f(): pass", file_path="m.py").symbols
61
+
62
+ def fail(texts):
63
+ raise RuntimeError("model unavailable")
64
+
65
+ with pytest.raises(RuntimeError, match="model unavailable"):
66
+ embed_symbols(symbols, fail, BGE_SMALL_PROFILE)
67
+ with pytest.raises(ValueError, match="duplicate"):
68
+ embed_symbols(symbols * 2, fail, BGE_SMALL_PROFILE)
69
+
70
+
71
+ def test_unbounded_encoder_outputs_are_rejected_after_one_overflow_item():
72
+ from itertools import repeat
73
+
74
+ with pytest.raises(ValueError, match="dimension"):
75
+ validate_vectors([repeat(1.0)], count=1, dimension=2)
76
+ with pytest.raises(ValueError, match="exactly one"):
77
+ validate_vectors(repeat([1.0, 2.0]), count=1, dimension=2)
78
+
79
+
80
+ def test_optional_encoder_owns_one_pinned_model_and_query_preprocessing(monkeypatch):
81
+ import asyncio
82
+ import sys
83
+ from types import SimpleNamespace
84
+ from victor_codegraph.embeddings import SentenceTransformerEncoder
85
+
86
+ calls = []
87
+
88
+ class Model:
89
+ max_seq_length = 0
90
+
91
+ def __init__(self, model_id, **kwargs):
92
+ calls.append((model_id, kwargs))
93
+
94
+ def get_sentence_embedding_dimension(self):
95
+ return 3
96
+
97
+ def encode(self, texts, **kwargs):
98
+ calls.append((texts, kwargs, self.max_seq_length))
99
+ return [[1.0, 2.0, 3.0] for _ in texts]
100
+
101
+ monkeypatch.setitem(
102
+ sys.modules, "sentence_transformers", SimpleNamespace(SentenceTransformer=Model)
103
+ )
104
+ profile = replace(BGE_SMALL_PROFILE, dimension=3, query_prefix="query: ", max_tokens=256)
105
+ encoder = SentenceTransformerEncoder(profile)
106
+ assert encoder.encode([]) == [] and calls == []
107
+ assert encoder.encode_query("permissions") == [1.0, 2.0, 3.0]
108
+ assert asyncio.run(encoder.embed_text("symbol")) == [1.0, 2.0, 3.0]
109
+ assert len(calls) == 3 # one model load, two bounded inference calls
110
+ assert calls[0] == (
111
+ profile.model_id,
112
+ {"revision": profile.revision, "device": "cpu", "trust_remote_code": False},
113
+ )
114
+ assert calls[1][0] == ["query: permissions"]
115
+ assert calls[1][1]["normalize_embeddings"] is False
116
+ assert calls[1][2] == 256
117
+ with pytest.raises(ValueError, match="512"):
118
+ encoder.encode(["x"] * 513)
119
+ with pytest.raises(ValueError, match="dimension"):
120
+ SentenceTransformerEncoder(BGE_SMALL_PROFILE).encode(["x"])
121
+
122
+
123
+ def test_optional_encoder_missing_dependency_is_explicit(monkeypatch):
124
+ import sys
125
+ from victor_codegraph.embeddings import SentenceTransformerEncoder
126
+
127
+ monkeypatch.setitem(sys.modules, "sentence_transformers", None)
128
+ with pytest.raises(ImportError):
129
+ SentenceTransformerEncoder().encode(["symbol"])
130
+
131
+
132
+ @pytest.mark.parametrize(
133
+ "changes",
134
+ [
135
+ {"model_id": ""},
136
+ {"revision": "main"},
137
+ {"dimension": True},
138
+ {"max_tokens": 0},
139
+ {"text_contract": "body-v1"},
140
+ {"normalize": 1},
141
+ ],
142
+ )
143
+ def test_invalid_profile_cannot_claim_semantic_compatibility(changes):
144
+ with pytest.raises(ValueError):
145
+ replace(BGE_SMALL_PROFILE, **changes)
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+
5
+ import pytest
6
+
7
+ from victor_codegraph import apply_index_delta, diff_repository, parse_repo
8
+
9
+
10
+ def test_repository_delta_deletes_removed_and_replaced_symbols(tmp_path):
11
+ a = tmp_path / "a.py"
12
+ b = tmp_path / "b.py"
13
+ a.write_text("def old_name():\n return 1\n")
14
+ b.write_text("def removed():\n return 2\n")
15
+ previous = parse_repo(tmp_path, repo_id="repo")
16
+
17
+ a.write_text("def new_name():\n return 1\n")
18
+ b.unlink()
19
+ current = parse_repo(tmp_path, repo_id="repo")
20
+ delta = diff_repository(current, previous)
21
+
22
+ old_function_ids = {s.id for s in previous.symbols if s.symbol_type.name == "FUNCTION"}
23
+ assert old_function_ids <= set(delta.delete_symbol_ids)
24
+ assert "new_name" in {s.simple_name for s in delta.upsert_symbols}
25
+
26
+ rebuilt = apply_index_delta(previous, delta)
27
+ assert {s.id for s in rebuilt.symbols} == {s.id for s in current.symbols}
28
+ assert rebuilt.manifest == current.manifest
29
+ assert delta.base_generation_id == previous.generation_id
30
+ assert delta.target_generation_id == current.generation_id
31
+
32
+ with pytest.raises(ValueError, match="base generation"):
33
+ apply_index_delta(replace(previous, generation_id="stale"), delta)
34
+
35
+
36
+ def test_repository_delta_refreshes_relation_evidence_on_line_shift(tmp_path):
37
+ path = tmp_path / "m.py"
38
+ path.write_text("def a():\n return b()\n\ndef b(): return 1\n")
39
+ previous = parse_repo(tmp_path, repo_id="repo")
40
+ path.write_text("\ndef a():\n return b()\n\ndef b(): return 1\n")
41
+ current = parse_repo(tmp_path, repo_id="repo")
42
+
43
+ delta = diff_repository(current, previous)
44
+ assert delta.upsert_relations
45
+ rebuilt = apply_index_delta(previous, delta)
46
+ assert rebuilt.relations[0].call_site.start_line == current.relations[0].call_site.start_line
47
+
48
+
49
+ def test_repository_quarantines_identity_collisions_and_delta_recovers(tmp_path):
50
+ from victor_codegraph import CodeRelationType, CodeSymbolType, ParseStatus
51
+
52
+ ambiguous = tmp_path / "duplicate.py"
53
+ ambiguous.write_text("def helper(): return 1\n")
54
+ (tmp_path / "caller.py").write_text(
55
+ "from duplicate import helper\ndef caller(): return helper()\n"
56
+ )
57
+ previous = parse_repo(tmp_path, repo_id="repo")
58
+ ambiguous.write_text(
59
+ "def f(): return helper()\ndef f(): return helper()\ndef helper(): return 1\n"
60
+ )
61
+ current = parse_repo(tmp_path, repo_id="repo")
62
+ raw = current.files["duplicate.py"]
63
+ assert raw.status == ParseStatus.PARTIAL
64
+ assert len(raw.symbols) == 3 # Keep original evidence, including both definitions.
65
+ projected = [s for s in current.symbols if s.location.file_path == "duplicate.py"]
66
+ assert len(projected) == 1
67
+ marker = projected[0]
68
+ assert marker.symbol_type == CodeSymbolType.FILE
69
+ assert marker.metadata["parse_status"] == "partial"
70
+ assert marker.metadata["projection_status"] == "file_marker_only"
71
+ assert marker.metadata["omitted_symbol_count"] == 3
72
+ assert marker.metadata["omitted_relation_count"] == len(raw.relations)
73
+ assert [d["line"] for d in marker.metadata["diagnostics"]] == [1, 2]
74
+ collisions = marker.metadata["identity_collisions"]
75
+ assert len(collisions) == 1 and collisions[0]["symbol_id"] == raw.symbols[0].id
76
+ assert collisions[0]["resolution"] == "ambiguous"
77
+ assert [d["qualified_name"] for d in collisions[0]["declarations"]] == [
78
+ "duplicate.py::f",
79
+ "duplicate.py::f",
80
+ ]
81
+ assert [d["symbol_type"] for d in collisions[0]["declarations"]] == ["function", "function"]
82
+ assert [d["source_span"]["start_line"] for d in collisions[0]["declarations"]] == [1, 2]
83
+ from victor_codegraph import to_proxima_records
84
+
85
+ marker.metadata["private_source_metadata"] = "must not be exported"
86
+ records = to_proxima_records(current, repo_graph_id="repo")
87
+ record = next(
88
+ r
89
+ for r in records
90
+ if r.get("props", {}).get("file") == "duplicate.py"
91
+ and r.get("props", {}).get("ast_kind") == "FILE"
92
+ )
93
+ for key in (
94
+ "identity_collisions",
95
+ "projection_status",
96
+ "omitted_symbol_count",
97
+ "omitted_relation_count",
98
+ ):
99
+ assert record["props"][key] == marker.metadata[key]
100
+ assert "private_source_metadata" not in record["props"]
101
+ assert not any(
102
+ "/external/" not in r["edge"]["to_oid"]
103
+ for r in records
104
+ if "graph_edge" in r["labels"] and r["edge"]["edge_type"] == "CALLS"
105
+ )
106
+ assert len({s.id for s in current.symbols}) == len(current.symbols)
107
+ assert not any(r.from_symbol_id == marker.id for r in current.relations)
108
+ calls = [r for r in current.relations if r.relation_type == CodeRelationType.CALLS]
109
+ assert len(calls) == 1 and calls[0].target_ref.name == "helper"
110
+ assert any(
111
+ r.relation_type == CodeRelationType.IMPORTS and r.to_symbol_id == marker.id
112
+ for r in current.relations
113
+ )
114
+ delta = diff_repository(current, previous)
115
+ assert delta.delete_symbol_ids and delta.delete_relation_keys
116
+ rebuilt = apply_index_delta(previous, delta)
117
+ assert {s.id for s in rebuilt.symbols} == {s.id for s in current.symbols}
118
+
119
+ ambiguous.write_text("def helper(): return 2\n")
120
+ repaired = parse_repo(tmp_path, repo_id="repo")
121
+ rebuilt = apply_index_delta(current, diff_repository(repaired, current))
122
+ assert {s.id for s in rebuilt.symbols} == {s.id for s in repaired.symbols}
123
+ calls = [r for r in rebuilt.relations if r.relation_type == CodeRelationType.CALLS]
124
+ assert len(calls) == 1 and calls[0].target_ref is None
125
+ assert (
126
+ next(s for s in repaired.symbols if s.id == marker.id).metadata["projection_status"]
127
+ == "symbols"
128
+ )
@@ -75,3 +75,38 @@ def test_chunk_jsts_is_size_capped():
75
75
  if not parsed.symbols:
76
76
  pytest.skip("javascript grammar not installed")
77
77
  assert parsed.symbols # routed through tree-sitter, not the empty stub
78
+
79
+
80
+ @pytest.mark.parametrize(
81
+ "annotation",
82
+ [
83
+ "export type X<T> = T extends object ? T : never;",
84
+ "type Options = Record<string, object>;",
85
+ "function accept(value: object): object { return value; }",
86
+ ],
87
+ )
88
+ def test_typescript_object_keyword_is_not_an_object_literal_scope(annotation):
89
+ source = annotation + "\nconst handlers = { run() { return 1; } };"
90
+ parsed = parse(source, file_path="case.ts")
91
+ shifted = parse("// heading\n\n" + source, file_path="case.ts")
92
+ assert parsed.status.value == "success"
93
+ assert not parsed.diagnostics
94
+ methods = [symbol for symbol in parsed.symbols if symbol.simple_name == "run"]
95
+ assert len(methods) == 1
96
+ assert methods[0].scope_chain
97
+ assert {symbol.id for symbol in parsed.symbols} == {symbol.id for symbol in shifted.symbols}
98
+
99
+
100
+ def test_inline_import_generic_grammar_limitation_stays_explicit():
101
+ # The pinned grammar rejects this valid TypeScript import type in a generic
102
+ # call. Do not hide the partial parse; an equivalent named alias is supported.
103
+ inline = parse("const value = load<typeof import('child_process')>();", file_path="case.ts")
104
+ assert inline.status.value == "partial"
105
+ assert "syntax_error" in {diagnostic.code for diagnostic in inline.diagnostics}
106
+ aliased = parse(
107
+ "type ChildProcessModule = typeof import('child_process');"
108
+ "const value = load<ChildProcessModule>();",
109
+ file_path="case.ts",
110
+ )
111
+ assert aliased.status.value == "success"
112
+ assert not aliased.diagnostics
@@ -14,6 +14,15 @@ from __future__ import annotations
14
14
 
15
15
  from .adapter import relation_to_record, symbol_to_record, to_proxima_records
16
16
  from .config import ChunkConfig
17
+ from .embeddings import (
18
+ BGE_SMALL_PROFILE,
19
+ EmbeddingProfile,
20
+ SymbolEmbedding,
21
+ SentenceTransformerEncoder,
22
+ embedding_text,
23
+ embed_symbols,
24
+ validate_vectors,
25
+ )
17
26
  from .languages import detect_language
18
27
  from .model import (
19
28
  CapabilityTier,
@@ -45,10 +54,17 @@ from .repository_index import (
45
54
  relation_key,
46
55
  )
47
56
 
48
- __version__ = "0.9.0"
57
+ __version__ = "0.10.1"
49
58
 
50
59
  __all__ = [
51
60
  "__version__",
61
+ "BGE_SMALL_PROFILE",
62
+ "EmbeddingProfile",
63
+ "SymbolEmbedding",
64
+ "SentenceTransformerEncoder",
65
+ "embedding_text",
66
+ "embed_symbols",
67
+ "validate_vectors",
52
68
  "chunk",
53
69
  "parse",
54
70
  "chunk_repo",