victor-codegraph 0.10.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.10.0 → victor_codegraph-0.10.1}/PKG-INFO +29 -3
  2. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/README.md +28 -2
  3. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/pyproject.toml +1 -1
  4. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_adapter.py +9 -0
  5. {victor_codegraph-0.10.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_repository_delta_v2.py +128 -0
  8. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_treesitter_jsts.py +35 -0
  9. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/__init__.py +1 -1
  10. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/adapter.py +19 -4
  11. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/parser.py +18 -10
  12. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/repository_index.py +59 -8
  13. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/treesitter_parser.py +3 -1
  14. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/PKG-INFO +29 -3
  15. victor_codegraph-0.10.0/tests/test_diagnostics_v2.py +0 -26
  16. victor_codegraph-0.10.0/tests/test_repository_delta_v2.py +0 -46
  17. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/setup.cfg +0 -0
  18. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_chunk_contract_v2.py +0 -0
  19. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_embedding_contract.py +0 -0
  20. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_identity_v2.py +0 -0
  21. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_line_base_contract.py +0 -0
  22. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_manifest.py +0 -0
  23. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_python_parser.py +0 -0
  24. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_relations_v2.py +0 -0
  25. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_repo.py +0 -0
  26. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_sizing_and_chunk.py +0 -0
  27. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_sizing_overlap.py +0 -0
  28. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_symbol_oid_contract.py +0 -0
  29. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_symbol_oid_parity.py +0 -0
  30. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_treesitter_languages.py +0 -0
  31. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/tests/test_treesitter_relations.py +0 -0
  32. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/config.py +0 -0
  33. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/embeddings.py +0 -0
  34. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/javascript_repository.py +0 -0
  35. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/languages.py +0 -0
  36. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/manifest.py +0 -0
  37. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/model.py +0 -0
  38. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/py.typed +0 -0
  39. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/python_parser.py +0 -0
  40. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/repo.py +0 -0
  41. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/resolution.py +0 -0
  42. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/rust_repository.py +0 -0
  43. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/sizing.py +0 -0
  44. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph/treesitter_tables.py +0 -0
  45. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/SOURCES.txt +0 -0
  46. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/dependency_links.txt +0 -0
  47. {victor_codegraph-0.10.0 → victor_codegraph-0.10.1}/victor_codegraph.egg-info/requires.txt +0 -0
  48. {victor_codegraph-0.10.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.10.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
@@ -56,7 +56,7 @@ gaps:
56
56
  ## Install
57
57
 
58
58
  Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/).
59
- This source tree is version **0.10.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.
60
60
 
61
61
  ```bash
62
62
  # from PyPI
@@ -128,10 +128,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
128
128
 
129
129
  ## Status
130
130
 
131
- `0.10.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,
132
132
  Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
133
133
  symbols-tier or fallback status rather than implying full fidelity.
134
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
+
135
161
  ### Rust graph fidelity
136
162
 
137
163
  Rust indexing retains inline module scopes and discriminates same-named trait
@@ -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.10.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.10.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "victor-codegraph"
7
- version = "0.10.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"
@@ -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,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
@@ -54,7 +54,7 @@ from .repository_index import (
54
54
  relation_key,
55
55
  )
56
56
 
57
- __version__ = "0.10.0"
57
+ __version__ = "0.10.1"
58
58
 
59
59
  __all__ = [
60
60
  "__version__",
@@ -145,6 +145,15 @@ def symbol_to_record(
145
145
  },
146
146
  "embeddings": [],
147
147
  }
148
+ # Preserve compiler-owned ambiguity evidence without exposing arbitrary metadata.
149
+ for key in (
150
+ "identity_collisions",
151
+ "projection_status",
152
+ "omitted_symbol_count",
153
+ "omitted_relation_count",
154
+ ):
155
+ if key in symbol.metadata:
156
+ record["props"][key] = symbol.metadata[key]
148
157
  if embedder is not None and not symbol.metadata.get("embedding_excluded"):
149
158
  record["embeddings"].append(
150
159
  {
@@ -241,14 +250,24 @@ def to_proxima_records(
241
250
 
242
251
  id_map: dict[str, str] = {}
243
252
  use_stable = _stable_oid_enabled(stable_oid)
253
+ seen_symbols: set[str] = set()
254
+ seen_oids: set[str] = set()
244
255
  for symbol in parsed.symbols:
245
256
  primary = (
246
257
  _canonical_symbol_oid(repo_graph_id, symbol)
247
258
  if use_stable
248
259
  else _legacy_symbol_oid(repo_graph_id, symbol)
249
260
  )
261
+ # Validate before invoking either embedding callback. A legacy OID
262
+ # preference must not hide ambiguous canonical symbol identities.
263
+ if symbol.id in seen_symbols or primary in seen_oids:
264
+ raise ValueError("canonical symbol identity collision in adapter input")
265
+ seen_symbols.add(symbol.id)
266
+ seen_oids.add(primary)
250
267
  for key in (symbol.id, symbol.legacy_id):
251
268
  if key:
269
+ if key in id_map and id_map[key] != primary:
270
+ raise ValueError("symbol alias identity collision in adapter input")
252
271
  id_map[key] = primary
253
272
  records = [
254
273
  symbol_to_record(
@@ -286,10 +305,6 @@ def to_proxima_records(
286
305
  "values": _validate_vector(values, dim),
287
306
  }
288
307
  )
289
- by_oid = [r["oid"] for r in records]
290
- if len(by_oid) != len(set(by_oid)):
291
- raise ValueError("canonical symbol identity collision in adapter input")
292
-
293
308
  external_records: dict[str, dict[str, Any]] = {}
294
309
  for relation in parsed.relations:
295
310
  if relation.target_ref is None:
@@ -16,6 +16,7 @@ from .model import (
16
16
  CapabilityTier,
17
17
  CodeChunk,
18
18
  CodeRelationType,
19
+ CodeSymbol,
19
20
  CodeSymbolType,
20
21
  ParseDiagnostic,
21
22
  ParsedCode,
@@ -52,7 +53,7 @@ def _finalize_identity(parsed: ParsedCode, content: str) -> ParsedCode:
52
53
  """Promote parser-local line IDs to the v2 structural identity contract."""
53
54
 
54
55
  id_map: dict[str, str] = {}
55
- seen: dict[str, str] = {}
56
+ groups: dict[str, list[CodeSymbol]] = {}
56
57
  for symbol in parsed.symbols:
57
58
  legacy = symbol.legacy_id or symbol.id
58
59
  stable = stable_symbol_key(
@@ -62,22 +63,29 @@ def _finalize_identity(parsed: ParsedCode, content: str) -> ParsedCode:
62
63
  symbol.signature,
63
64
  symbol.location.file_path,
64
65
  )
65
- other = seen.get(stable)
66
- if other is not None and other != legacy:
67
- parsed.status = ParseStatus.PARTIAL
66
+ groups.setdefault(stable, []).append(symbol)
67
+ id_map[legacy] = stable
68
+ symbol.legacy_id = legacy
69
+ symbol.id = stable
70
+ symbol.identity_version = "v2"
71
+
72
+ for stable, definitions in groups.items():
73
+ if len(definitions) < 2:
74
+ continue
75
+ parsed.status = ParseStatus.PARTIAL
76
+ for symbol in definitions:
68
77
  parsed.diagnostics.append(
69
78
  ParseDiagnostic(
70
79
  code="identity_collision",
71
- message=f"multiple symbols share structural key {stable}",
80
+ message=(
81
+ f"multiple declarations of {symbol.fully_qualified_name!r} "
82
+ f"share structural key {stable}"
83
+ ),
72
84
  severity="error",
73
85
  line=symbol.location.start_line,
86
+ column=symbol.location.start_column,
74
87
  )
75
88
  )
76
- seen[stable] = legacy
77
- id_map[legacy] = stable
78
- symbol.legacy_id = legacy
79
- symbol.id = stable
80
- symbol.identity_version = "v2"
81
89
 
82
90
  for relation in parsed.relations:
83
91
  relation.from_symbol_id = id_map.get(relation.from_symbol_id, relation.from_symbol_id)
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import ast
6
6
  import hashlib
7
7
  import os
8
- from dataclasses import dataclass, field, replace
8
+ from dataclasses import asdict, dataclass, field, replace
9
9
  from pathlib import Path
10
10
 
11
11
  from .model import (
@@ -245,6 +245,40 @@ def _module_candidates(module: str, importer: str) -> tuple[str, ...]:
245
245
  return f"{stem}.py", f"{stem}/__init__.py"
246
246
 
247
247
 
248
+ def _file_projection_metadata(parsed: ParsedCode) -> dict[str, object]:
249
+ """Keep diagnostic evidence while quarantining ambiguous symbol identities."""
250
+ quarantined = any(item.code == "identity_collision" for item in parsed.diagnostics)
251
+ groups: dict[str, list[CodeSymbol]] = {}
252
+ if quarantined:
253
+ for symbol in parsed.symbols:
254
+ groups.setdefault(symbol.id, []).append(symbol)
255
+ return {
256
+ "parse_status": parsed.status.value,
257
+ "capability_tier": parsed.capability_tier.value,
258
+ "diagnostics": [asdict(item) for item in parsed.diagnostics],
259
+ "projection_status": "file_marker_only" if quarantined else "symbols",
260
+ "omitted_symbol_count": len(parsed.symbols) if quarantined else 0,
261
+ "omitted_relation_count": len(parsed.relations) if quarantined else 0,
262
+ "identity_collisions": [
263
+ {
264
+ "symbol_id": symbol_id,
265
+ "resolution": "ambiguous",
266
+ "declarations": [
267
+ {
268
+ "name": symbol.simple_name,
269
+ "qualified_name": symbol.fully_qualified_name,
270
+ "symbol_type": symbol.symbol_type.name.lower(),
271
+ "source_span": asdict(symbol.location),
272
+ }
273
+ for symbol in declarations
274
+ ],
275
+ }
276
+ for symbol_id, declarations in groups.items()
277
+ if len(declarations) > 1
278
+ ],
279
+ }
280
+
281
+
248
282
  def parse_repo(
249
283
  root: str | os.PathLike[str],
250
284
  *,
@@ -288,18 +322,33 @@ def parse_repo(
288
322
  ),
289
323
  source_code="",
290
324
  language=parsed.language,
291
- metadata={"embedding_excluded": True, "content_version": parsed.content_hash},
325
+ metadata={
326
+ "embedding_excluded": True,
327
+ "content_version": parsed.content_hash,
328
+ **_file_projection_metadata(parsed),
329
+ },
292
330
  )
293
331
  file_symbols[relative] = file_symbol
294
332
 
295
- symbols = [*file_symbols.values(), *(s for parsed in files.values() for s in parsed.symbols)]
296
- relations = [relation for parsed in files.values() for relation in parsed.relations]
333
+ # Retain raw ParsedCode in `files` for diagnostics, but never feed ambiguous
334
+ # declarations into symbol lookup or cross-file resolution. The canonical
335
+ # FILE node stays visible, including to healthy files importing this module.
336
+ resolution_files = {
337
+ path: parsed
338
+ for path, parsed in files.items()
339
+ if file_symbols[path].metadata["projection_status"] != "file_marker_only"
340
+ }
341
+ symbols = [
342
+ *file_symbols.values(),
343
+ *(symbol for parsed in resolution_files.values() for symbol in parsed.symbols),
344
+ ]
345
+ relations = [relation for parsed in resolution_files.values() for relation in parsed.relations]
297
346
  from .rust_repository import RustRepository, use_paths
298
347
 
299
- rust = RustRepository(root_path, files)
300
- javascript = JavaScriptRepository(files)
348
+ rust = RustRepository(root_path, resolution_files)
349
+ javascript = JavaScriptRepository(resolution_files)
301
350
  rust_bindings: dict[str, list] = {}
302
- for relative, parsed in files.items():
351
+ for relative, parsed in resolution_files.items():
303
352
  owner = file_symbols[relative]
304
353
  for symbol in parsed.symbols:
305
354
  if not symbol.scope_chain:
@@ -379,7 +428,9 @@ def parse_repo(
379
428
  )
380
429
  # Re-run the scope resolver over the full stable table only for already textual
381
430
  # targets, then apply conservative unique-name cross-file linking.
382
- relations = _resolve_across_files(symbols, relations, files, rust_bindings, javascript)
431
+ relations = _resolve_across_files(
432
+ symbols, relations, resolution_files, rust_bindings, javascript
433
+ )
383
434
  generation_raw = "\n".join(f"{path}\x1f{digest}" for path, digest in sorted(manifest.items()))
384
435
  generation_id = hashlib.blake2b(generation_raw.encode(), digest_size=16).hexdigest()
385
436
  return ParsedRepository(
@@ -426,7 +426,9 @@ def _go_receiver_type(node, src: bytes) -> str | None:
426
426
  def _walk_collect(node, src, file_path, language, frames, symbols, relations, impl_traits):
427
427
  for child in _children(node):
428
428
  t = child.type
429
- if t == "object" and language in {"javascript", "typescript", "tsx"}:
429
+ if t == "object" and child.is_named and language in {"javascript", "typescript", "tsx"}:
430
+ # TypeScript also uses an unnamed `object` keyword in type positions.
431
+ # Only named object expressions introduce a literal scope.
430
432
  # Anonymous object literals have a structural AST path, not a line ID.
431
433
  # Comments/whitespace do not participate; inserting a sibling object
432
434
  # can change the ordinal and requires a fresh graph generation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: victor-codegraph
3
- Version: 0.10.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
@@ -56,7 +56,7 @@ gaps:
56
56
  ## Install
57
57
 
58
58
  Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/).
59
- This source tree is version **0.10.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.
60
60
 
61
61
  ```bash
62
62
  # from PyPI
@@ -128,10 +128,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
128
128
 
129
129
  ## Status
130
130
 
131
- `0.10.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,
132
132
  Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
133
133
  symbols-tier or fallback status rather than implying full fidelity.
134
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
+
135
161
  ### Rust graph fidelity
136
162
 
137
163
  Rust indexing retains inline module scopes and discriminates same-named trait
@@ -1,26 +0,0 @@
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 == []
@@ -1,46 +0,0 @@
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