victor-codegraph 0.10.0__tar.gz → 0.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/PKG-INFO +33 -3
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/README.md +32 -2
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/pyproject.toml +1 -1
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_adapter.py +9 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_adapter_validation_v2.py +27 -0
- victor_codegraph-0.10.2/tests/test_diagnostics_v2.py +46 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_relations_v2.py +36 -0
- victor_codegraph-0.10.2/tests/test_repository_delta_v2.py +128 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_treesitter_jsts.py +35 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/__init__.py +1 -1
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/adapter.py +19 -4
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/parser.py +18 -10
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/python_parser.py +39 -17
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/repository_index.py +75 -16
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/resolution.py +9 -1
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/treesitter_parser.py +3 -1
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph.egg-info/PKG-INFO +33 -3
- victor_codegraph-0.10.0/tests/test_diagnostics_v2.py +0 -26
- victor_codegraph-0.10.0/tests/test_repository_delta_v2.py +0 -46
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/setup.cfg +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_chunk_contract_v2.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_embedding_contract.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_identity_v2.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_line_base_contract.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_manifest.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_python_parser.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_repo.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_sizing_and_chunk.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_sizing_overlap.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_symbol_oid_contract.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_symbol_oid_parity.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_treesitter_languages.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/tests/test_treesitter_relations.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/config.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/embeddings.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/javascript_repository.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/languages.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/manifest.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/model.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/py.typed +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/repo.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/rust_repository.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/sizing.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/treesitter_tables.py +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph.egg-info/SOURCES.txt +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph.egg-info/dependency_links.txt +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph.egg-info/requires.txt +0 -0
- {victor_codegraph-0.10.0 → victor_codegraph-0.10.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.10.
|
|
3
|
+
Version: 0.10.2
|
|
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,11 @@ 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.
|
|
59
|
+
This source tree is version **0.10.2**; consumers should use a compatible `<1.0` pin.
|
|
60
|
+
It repairs nested Python definition/import shadowing: proven local definitions
|
|
61
|
+
retain their targets, while unmodeled conditional bindings stay unresolved.
|
|
62
|
+
Rebuild derived graphs when adopting the new parser; unchanged source hashes do
|
|
63
|
+
not invalidate old semantic results automatically. Publication is a separate gate.
|
|
60
64
|
|
|
61
65
|
```bash
|
|
62
66
|
# from PyPI
|
|
@@ -128,10 +132,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
|
|
|
128
132
|
|
|
129
133
|
## Status
|
|
130
134
|
|
|
131
|
-
`0.10.
|
|
135
|
+
`0.10.2` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
|
|
132
136
|
Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
|
|
133
137
|
symbols-tier or fallback status rather than implying full fidelity.
|
|
134
138
|
|
|
139
|
+
### Ambiguous declarations
|
|
140
|
+
|
|
141
|
+
Duplicate structural identities retain raw declarations in `ParsedRepository.files`
|
|
142
|
+
and report each conflicting source location. Repository projection emits only the
|
|
143
|
+
canonical FILE marker for that file, with typed diagnostics, declaration spans and
|
|
144
|
+
omitted symbol/relation counts in metadata. Calls into its declarations remain
|
|
145
|
+
unresolved; healthy imports can still point to its diagnostic marker. Repairing the
|
|
146
|
+
source restores normal projection through deletion-complete repository deltas.
|
|
147
|
+
Direct record projection rejects duplicate identities before invoking an embedder,
|
|
148
|
+
including when legacy OIDs are selected.
|
|
149
|
+
|
|
150
|
+
`identity_collisions` groups use the existing `symbol_id` with `resolution: ambiguous`.
|
|
151
|
+
Each declaration retains `name`, `qualified_name`, `symbol_type` and its full
|
|
152
|
+
`source_span`; these are inspectable evidence, not separately resolved graph nodes.
|
|
153
|
+
The record adapter forwards these groups, projection status and omitted counts only
|
|
154
|
+
when present, without copying arbitrary symbol metadata into exported properties.
|
|
155
|
+
|
|
156
|
+
### TypeScript grammar limitation
|
|
157
|
+
|
|
158
|
+
The current tree-sitter grammar reports a partial parse for inline import types
|
|
159
|
+
in generic calls, for example `load<typeof import('child_process')>()`, even though
|
|
160
|
+
TypeScript accepts that syntax. A named type alias (`type ChildProcessModule =
|
|
161
|
+
typeof import('child_process')`, then `load<ChildProcessModule>()`) is supported.
|
|
162
|
+
Partial status and syntax diagnostics remain visible; the parser does not rewrite
|
|
163
|
+
source or infer missing relationships.
|
|
164
|
+
|
|
135
165
|
### Rust graph fidelity
|
|
136
166
|
|
|
137
167
|
Rust indexing retains inline module scopes and discriminates same-named trait
|
|
@@ -25,7 +25,11 @@ 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.
|
|
28
|
+
This source tree is version **0.10.2**; consumers should use a compatible `<1.0` pin.
|
|
29
|
+
It repairs nested Python definition/import shadowing: proven local definitions
|
|
30
|
+
retain their targets, while unmodeled conditional bindings stay unresolved.
|
|
31
|
+
Rebuild derived graphs when adopting the new parser; unchanged source hashes do
|
|
32
|
+
not invalidate old semantic results automatically. Publication is a separate gate.
|
|
29
33
|
|
|
30
34
|
```bash
|
|
31
35
|
# from PyPI
|
|
@@ -97,10 +101,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
|
|
|
97
101
|
|
|
98
102
|
## Status
|
|
99
103
|
|
|
100
|
-
`0.10.
|
|
104
|
+
`0.10.2` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
|
|
101
105
|
Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
|
|
102
106
|
symbols-tier or fallback status rather than implying full fidelity.
|
|
103
107
|
|
|
108
|
+
### Ambiguous declarations
|
|
109
|
+
|
|
110
|
+
Duplicate structural identities retain raw declarations in `ParsedRepository.files`
|
|
111
|
+
and report each conflicting source location. Repository projection emits only the
|
|
112
|
+
canonical FILE marker for that file, with typed diagnostics, declaration spans and
|
|
113
|
+
omitted symbol/relation counts in metadata. Calls into its declarations remain
|
|
114
|
+
unresolved; healthy imports can still point to its diagnostic marker. Repairing the
|
|
115
|
+
source restores normal projection through deletion-complete repository deltas.
|
|
116
|
+
Direct record projection rejects duplicate identities before invoking an embedder,
|
|
117
|
+
including when legacy OIDs are selected.
|
|
118
|
+
|
|
119
|
+
`identity_collisions` groups use the existing `symbol_id` with `resolution: ambiguous`.
|
|
120
|
+
Each declaration retains `name`, `qualified_name`, `symbol_type` and its full
|
|
121
|
+
`source_span`; these are inspectable evidence, not separately resolved graph nodes.
|
|
122
|
+
The record adapter forwards these groups, projection status and omitted counts only
|
|
123
|
+
when present, without copying arbitrary symbol metadata into exported properties.
|
|
124
|
+
|
|
125
|
+
### TypeScript grammar limitation
|
|
126
|
+
|
|
127
|
+
The current tree-sitter grammar reports a partial parse for inline import types
|
|
128
|
+
in generic calls, for example `load<typeof import('child_process')>()`, even though
|
|
129
|
+
TypeScript accepts that syntax. A named type alias (`type ChildProcessModule =
|
|
130
|
+
typeof import('child_process')`, then `load<ChildProcessModule>()`) is supported.
|
|
131
|
+
Partial status and syntax diagnostics remain visible; the parser does not rewrite
|
|
132
|
+
source or infer missing relationships.
|
|
133
|
+
|
|
104
134
|
### Rust graph fidelity
|
|
105
135
|
|
|
106
136
|
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.
|
|
7
|
+
version = "0.10.2"
|
|
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)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
6
|
|
|
5
7
|
from victor_codegraph import CodeRelationType, parse, parse_repo, to_proxima_records
|
|
@@ -319,6 +321,40 @@ def test_python_enclosing_and_capture_bindings_block_import_guess(tmp_path, body
|
|
|
319
321
|
assert calls and all(r.target_ref for r in calls)
|
|
320
322
|
|
|
321
323
|
|
|
324
|
+
@pytest.mark.parametrize(
|
|
325
|
+
("body", "target_scope"),
|
|
326
|
+
[
|
|
327
|
+
("def caller():\n def target(): return 2\n return target()", ["caller"]),
|
|
328
|
+
("async def caller():\n async def target(): return 2\n return await target()", ["caller"]),
|
|
329
|
+
("def caller():\n class target: pass\n return target()", ["caller"]),
|
|
330
|
+
("def outer():\n def target(): return 2\n def caller(): return target()", ["outer"]),
|
|
331
|
+
("def caller(flag):\n if flag:\n def target(): return 2\n return target()", None),
|
|
332
|
+
(
|
|
333
|
+
"def outer():\n def target(): return 2\n def caller(flag):\n if flag:\n def target(): return 3\n return target()",
|
|
334
|
+
None,
|
|
335
|
+
),
|
|
336
|
+
("def caller(): return target()", []),
|
|
337
|
+
],
|
|
338
|
+
)
|
|
339
|
+
def test_python_nearest_definition_precedes_import(
|
|
340
|
+
tmp_path: Path, body: str, target_scope: list[str] | None
|
|
341
|
+
) -> None:
|
|
342
|
+
(tmp_path / "a.py").write_text("def target(): return 1\n")
|
|
343
|
+
(tmp_path / "b.py").write_text("from a import target\n" + body)
|
|
344
|
+
graph = parse_repo(tmp_path)
|
|
345
|
+
calls = [r for r in graph.relations if r.relation_type == CodeRelationType.CALLS]
|
|
346
|
+
assert len(calls) == 1
|
|
347
|
+
call = calls[0]
|
|
348
|
+
if target_scope is None:
|
|
349
|
+
assert call.target_ref is not None
|
|
350
|
+
assert call.confidence < 1.0
|
|
351
|
+
else:
|
|
352
|
+
target = next(s for s in graph.symbols if s.id == call.to_symbol_id)
|
|
353
|
+
assert call.target_ref is None
|
|
354
|
+
assert target.scope_chain == target_scope
|
|
355
|
+
assert target.location.file_path == ("b.py" if target_scope else "a.py")
|
|
356
|
+
|
|
357
|
+
|
|
322
358
|
def test_python_reassigned_self_is_not_a_proven_receiver():
|
|
323
359
|
parsed = parse(
|
|
324
360
|
"class A:\n def target(self): pass\n def caller(self):\n self = external\n self.target()\n",
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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=
|
|
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)
|
|
@@ -8,6 +8,8 @@ signatures, docstrings, decorators, parameters, cyclomatic complexity, and CALLS
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import ast
|
|
11
|
+
from collections.abc import Iterator, Sequence
|
|
12
|
+
from typing import NotRequired, TypedDict
|
|
11
13
|
|
|
12
14
|
from .model import (
|
|
13
15
|
CodeRelation,
|
|
@@ -47,13 +49,20 @@ def _cyclomatic(node: ast.AST) -> dict[str, int]:
|
|
|
47
49
|
return {"cyclomatic": count, "lines": end - lineno + 1}
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
class _Parameter(TypedDict):
|
|
53
|
+
name: str
|
|
54
|
+
type: NotRequired[str]
|
|
55
|
+
is_variadic: NotRequired[bool]
|
|
56
|
+
is_kwonly: NotRequired[bool]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _params(args: ast.arguments) -> list[_Parameter]:
|
|
60
|
+
out: list[_Parameter] = []
|
|
52
61
|
posonly = getattr(args, "posonlyargs", [])
|
|
53
62
|
for a in [*posonly, *args.args]:
|
|
54
63
|
if a.arg in ("self", "cls"):
|
|
55
64
|
continue
|
|
56
|
-
p:
|
|
65
|
+
p: _Parameter = {"name": a.arg}
|
|
57
66
|
if a.annotation is not None:
|
|
58
67
|
p["type"] = ast.unparse(a.annotation)
|
|
59
68
|
out.append(p)
|
|
@@ -109,7 +118,7 @@ def _callee_ref(call: ast.Call) -> SymbolReference | None:
|
|
|
109
118
|
return None
|
|
110
119
|
|
|
111
120
|
|
|
112
|
-
def _calls_owned_by(node: ast.AST):
|
|
121
|
+
def _calls_owned_by(node: ast.AST) -> Iterator[ast.Call]:
|
|
113
122
|
"""Yield calls owned by ``node`` without stealing calls from nested definitions."""
|
|
114
123
|
|
|
115
124
|
stack = list(ast.iter_child_nodes(node))
|
|
@@ -122,13 +131,15 @@ def _calls_owned_by(node: ast.AST):
|
|
|
122
131
|
stack.extend(ast.iter_child_nodes(child))
|
|
123
132
|
|
|
124
133
|
|
|
125
|
-
def
|
|
126
|
-
"""
|
|
134
|
+
def _lexical_bindings(nodes: Sequence[ast.AST]) -> tuple[set[str], set[str]]:
|
|
135
|
+
"""Collect writes and definition bindings without entering nested namespaces."""
|
|
127
136
|
names: set[str] = set()
|
|
137
|
+
definitions: set[str] = set()
|
|
128
138
|
stack = list(nodes)
|
|
129
139
|
while stack:
|
|
130
140
|
node = stack.pop()
|
|
131
141
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
142
|
+
definitions.add(node.name)
|
|
132
143
|
continue
|
|
133
144
|
if isinstance(node, ast.arg):
|
|
134
145
|
names.add(node.arg)
|
|
@@ -141,7 +152,7 @@ def _bound_names(nodes: list[ast.AST]) -> set[str]:
|
|
|
141
152
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
142
153
|
names.update(a.asname or a.name.split(".")[0] for a in node.names)
|
|
143
154
|
stack.extend(ast.iter_child_nodes(node))
|
|
144
|
-
return names
|
|
155
|
+
return names, definitions
|
|
145
156
|
|
|
146
157
|
|
|
147
158
|
class _Visitor:
|
|
@@ -152,6 +163,7 @@ class _Visitor:
|
|
|
152
163
|
self.relations: list[CodeRelation] = []
|
|
153
164
|
self.imports: list[str] = []
|
|
154
165
|
self._enclosing_bindings: list[set[str]] = []
|
|
166
|
+
self._definition_scopes: list[dict[str, list[str]]] = [{}]
|
|
155
167
|
self._import_names: set[str] = set()
|
|
156
168
|
self._wildcard_import = False
|
|
157
169
|
self._fqn_prefix = file_path.replace("/", ".").replace("\\", ".")
|
|
@@ -190,7 +202,7 @@ class _Visitor:
|
|
|
190
202
|
symbol_type: CodeSymbolType,
|
|
191
203
|
scope: list[str],
|
|
192
204
|
signature: str | None = None,
|
|
193
|
-
params: list[
|
|
205
|
+
params: list[_Parameter] | None = None,
|
|
194
206
|
return_type: str | None = None,
|
|
195
207
|
modifiers: list[str] | None = None,
|
|
196
208
|
) -> CodeSymbol:
|
|
@@ -220,7 +232,7 @@ class _Visitor:
|
|
|
220
232
|
signature=signature,
|
|
221
233
|
modifiers=modifiers or [],
|
|
222
234
|
scope_chain=list(scope),
|
|
223
|
-
parameters=params or [],
|
|
235
|
+
parameters=[dict(parameter) for parameter in params or []],
|
|
224
236
|
return_type=return_type,
|
|
225
237
|
complexity=_cyclomatic(node),
|
|
226
238
|
)
|
|
@@ -254,10 +266,16 @@ class _Visitor:
|
|
|
254
266
|
bound.add(node.args.vararg.arg)
|
|
255
267
|
if node.args.kwarg:
|
|
256
268
|
bound.add(node.args.kwarg.arg)
|
|
257
|
-
writes =
|
|
269
|
+
writes, definitions = _lexical_bindings(node.body)
|
|
270
|
+
inner = [*scope, name]
|
|
271
|
+
definition_scopes = {
|
|
272
|
+
**self._definition_scopes[-1],
|
|
273
|
+
**{definition: inner for definition in definitions},
|
|
274
|
+
}
|
|
258
275
|
enclosing = set().union(*self._enclosing_bindings)
|
|
259
276
|
sym.metadata["python_bound_names"] = sorted(bound | writes | enclosing)
|
|
260
277
|
sym.metadata["python_rebound_names"] = sorted(writes | enclosing)
|
|
278
|
+
sym.metadata["python_definition_scopes"] = definition_scopes
|
|
261
279
|
sym.metadata["python_import_names"] = sorted(self._import_names)
|
|
262
280
|
sym.metadata["python_wildcard_import"] = self._wildcard_import
|
|
263
281
|
sym.metadata["python_receiver"] = (
|
|
@@ -275,10 +293,10 @@ class _Visitor:
|
|
|
275
293
|
# and otherwise keeps it as a bare name (so cross-file/external calls — e.g.
|
|
276
294
|
# a CPG's blast radius — are not silently dropped). ``call_site`` records the
|
|
277
295
|
# call line for consumers that need it.
|
|
278
|
-
for
|
|
279
|
-
ref = _callee_ref(
|
|
296
|
+
for call in _calls_owned_by(node):
|
|
297
|
+
ref = _callee_ref(call)
|
|
280
298
|
if ref:
|
|
281
|
-
line, col, end_line, end_col, offset, length = self._span(
|
|
299
|
+
line, col, end_line, end_col, offset, length = self._span(call)
|
|
282
300
|
self.relations.append(
|
|
283
301
|
CodeRelation(
|
|
284
302
|
from_symbol_id=sym.id,
|
|
@@ -297,8 +315,8 @@ class _Visitor:
|
|
|
297
315
|
),
|
|
298
316
|
)
|
|
299
317
|
)
|
|
300
|
-
inner = [*scope, name]
|
|
301
318
|
self._enclosing_bindings.append(bound | writes)
|
|
319
|
+
self._definition_scopes.append(definition_scopes)
|
|
302
320
|
for child in node.body:
|
|
303
321
|
nested: CodeSymbol | None = None
|
|
304
322
|
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -314,6 +332,7 @@ class _Visitor:
|
|
|
314
332
|
)
|
|
315
333
|
)
|
|
316
334
|
self._enclosing_bindings.pop()
|
|
335
|
+
self._definition_scopes.pop()
|
|
317
336
|
return sym
|
|
318
337
|
|
|
319
338
|
def visit_class(self, node: ast.ClassDef, scope: list[str]) -> CodeSymbol:
|
|
@@ -322,6 +341,7 @@ class _Visitor:
|
|
|
322
341
|
if bases:
|
|
323
342
|
mods.append(f"extends({','.join(bases)})")
|
|
324
343
|
cls = self._make_symbol(node, node.name, CodeSymbolType.CLASS, scope, modifiers=mods)
|
|
344
|
+
cls.metadata["python_definition_scopes"] = self._definition_scopes[-1]
|
|
325
345
|
cls.metadata["python_rebound_names"] = sorted(set().union(*self._enclosing_bindings))
|
|
326
346
|
self.symbols.append(cls)
|
|
327
347
|
for base in bases:
|
|
@@ -356,9 +376,11 @@ class _Visitor:
|
|
|
356
376
|
return cls
|
|
357
377
|
|
|
358
378
|
def run(self, tree: ast.Module) -> None:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
379
|
+
module_writes, _ = _lexical_bindings(
|
|
380
|
+
[n for n in tree.body if not isinstance(n, (ast.Import, ast.ImportFrom))]
|
|
381
|
+
)
|
|
382
|
+
self._enclosing_bindings = [module_writes]
|
|
383
|
+
self._definition_scopes = [{}]
|
|
362
384
|
self._import_names = set()
|
|
363
385
|
for declaration in tree.body:
|
|
364
386
|
if isinstance(declaration, (ast.Import, ast.ImportFrom)):
|
|
@@ -5,8 +5,12 @@ 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
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .rust_repository import RustBinding
|
|
10
14
|
|
|
11
15
|
from .model import (
|
|
12
16
|
CodeRelation,
|
|
@@ -67,7 +71,7 @@ def _resolve_across_files(
|
|
|
67
71
|
symbols: list[CodeSymbol],
|
|
68
72
|
relations: list[CodeRelation],
|
|
69
73
|
files: dict[str, ParsedCode],
|
|
70
|
-
rust_bindings: dict[str, list] | None = None,
|
|
74
|
+
rust_bindings: dict[str, list[RustBinding]] | None = None,
|
|
71
75
|
javascript: JavaScriptRepository | None = None,
|
|
72
76
|
) -> list[CodeRelation]:
|
|
73
77
|
"""Resolve only unambiguous repository targets; ambiguity remains explicit."""
|
|
@@ -147,11 +151,15 @@ def _resolve_across_files(
|
|
|
147
151
|
if source.language == "python":
|
|
148
152
|
text = ref.text or ref.name
|
|
149
153
|
root, *tail = text.split(".")
|
|
150
|
-
if
|
|
151
|
-
"python_wildcard_import"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
if (
|
|
155
|
+
not source.metadata.get("python_wildcard_import")
|
|
156
|
+
and root not in source.metadata.get("python_bound_names", [])
|
|
157
|
+
and root not in source.metadata.get("python_definition_scopes", {})
|
|
158
|
+
):
|
|
159
|
+
for import_name, targets in python_bindings.get(
|
|
160
|
+
source.location.file_path, {}
|
|
161
|
+
).items():
|
|
162
|
+
prefix = import_name.split(".")
|
|
155
163
|
parts = text.split(".")
|
|
156
164
|
if parts[: len(prefix)] != prefix:
|
|
157
165
|
continue
|
|
@@ -198,7 +206,7 @@ def _python_bindings(
|
|
|
198
206
|
if isinstance(node, ast.ImportFrom):
|
|
199
207
|
module = "." * node.level + (node.module or "")
|
|
200
208
|
name = alias.asname or alias.name
|
|
201
|
-
target = (alias.name,)
|
|
209
|
+
target: tuple[str, ...] = (alias.name,)
|
|
202
210
|
options = [
|
|
203
211
|
(module, target),
|
|
204
212
|
(module + ("." if node.module else "") + alias.name, ()),
|
|
@@ -245,6 +253,40 @@ def _module_candidates(module: str, importer: str) -> tuple[str, ...]:
|
|
|
245
253
|
return f"{stem}.py", f"{stem}/__init__.py"
|
|
246
254
|
|
|
247
255
|
|
|
256
|
+
def _file_projection_metadata(parsed: ParsedCode) -> dict[str, object]:
|
|
257
|
+
"""Keep diagnostic evidence while quarantining ambiguous symbol identities."""
|
|
258
|
+
quarantined = any(item.code == "identity_collision" for item in parsed.diagnostics)
|
|
259
|
+
groups: dict[str, list[CodeSymbol]] = {}
|
|
260
|
+
if quarantined:
|
|
261
|
+
for symbol in parsed.symbols:
|
|
262
|
+
groups.setdefault(symbol.id, []).append(symbol)
|
|
263
|
+
return {
|
|
264
|
+
"parse_status": parsed.status.value,
|
|
265
|
+
"capability_tier": parsed.capability_tier.value,
|
|
266
|
+
"diagnostics": [asdict(item) for item in parsed.diagnostics],
|
|
267
|
+
"projection_status": "file_marker_only" if quarantined else "symbols",
|
|
268
|
+
"omitted_symbol_count": len(parsed.symbols) if quarantined else 0,
|
|
269
|
+
"omitted_relation_count": len(parsed.relations) if quarantined else 0,
|
|
270
|
+
"identity_collisions": [
|
|
271
|
+
{
|
|
272
|
+
"symbol_id": symbol_id,
|
|
273
|
+
"resolution": "ambiguous",
|
|
274
|
+
"declarations": [
|
|
275
|
+
{
|
|
276
|
+
"name": symbol.simple_name,
|
|
277
|
+
"qualified_name": symbol.fully_qualified_name,
|
|
278
|
+
"symbol_type": symbol.symbol_type.name.lower(),
|
|
279
|
+
"source_span": asdict(symbol.location),
|
|
280
|
+
}
|
|
281
|
+
for symbol in declarations
|
|
282
|
+
],
|
|
283
|
+
}
|
|
284
|
+
for symbol_id, declarations in groups.items()
|
|
285
|
+
if len(declarations) > 1
|
|
286
|
+
],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
248
290
|
def parse_repo(
|
|
249
291
|
root: str | os.PathLike[str],
|
|
250
292
|
*,
|
|
@@ -288,18 +330,33 @@ def parse_repo(
|
|
|
288
330
|
),
|
|
289
331
|
source_code="",
|
|
290
332
|
language=parsed.language,
|
|
291
|
-
metadata={
|
|
333
|
+
metadata={
|
|
334
|
+
"embedding_excluded": True,
|
|
335
|
+
"content_version": parsed.content_hash,
|
|
336
|
+
**_file_projection_metadata(parsed),
|
|
337
|
+
},
|
|
292
338
|
)
|
|
293
339
|
file_symbols[relative] = file_symbol
|
|
294
340
|
|
|
295
|
-
|
|
296
|
-
|
|
341
|
+
# Retain raw ParsedCode in `files` for diagnostics, but never feed ambiguous
|
|
342
|
+
# declarations into symbol lookup or cross-file resolution. The canonical
|
|
343
|
+
# FILE node stays visible, including to healthy files importing this module.
|
|
344
|
+
resolution_files = {
|
|
345
|
+
path: parsed
|
|
346
|
+
for path, parsed in files.items()
|
|
347
|
+
if file_symbols[path].metadata["projection_status"] != "file_marker_only"
|
|
348
|
+
}
|
|
349
|
+
symbols = [
|
|
350
|
+
*file_symbols.values(),
|
|
351
|
+
*(symbol for parsed in resolution_files.values() for symbol in parsed.symbols),
|
|
352
|
+
]
|
|
353
|
+
relations = [relation for parsed in resolution_files.values() for relation in parsed.relations]
|
|
297
354
|
from .rust_repository import RustRepository, use_paths
|
|
298
355
|
|
|
299
|
-
rust = RustRepository(root_path,
|
|
300
|
-
javascript = JavaScriptRepository(
|
|
301
|
-
rust_bindings: dict[str, list] = {}
|
|
302
|
-
for relative, parsed in
|
|
356
|
+
rust = RustRepository(root_path, resolution_files)
|
|
357
|
+
javascript = JavaScriptRepository(resolution_files)
|
|
358
|
+
rust_bindings: dict[str, list[RustBinding]] = {}
|
|
359
|
+
for relative, parsed in resolution_files.items():
|
|
303
360
|
owner = file_symbols[relative]
|
|
304
361
|
for symbol in parsed.symbols:
|
|
305
362
|
if not symbol.scope_chain:
|
|
@@ -379,7 +436,9 @@ def parse_repo(
|
|
|
379
436
|
)
|
|
380
437
|
# Re-run the scope resolver over the full stable table only for already textual
|
|
381
438
|
# targets, then apply conservative unique-name cross-file linking.
|
|
382
|
-
relations = _resolve_across_files(
|
|
439
|
+
relations = _resolve_across_files(
|
|
440
|
+
symbols, relations, resolution_files, rust_bindings, javascript
|
|
441
|
+
)
|
|
383
442
|
generation_raw = "\n".join(f"{path}\x1f{digest}" for path, digest in sorted(manifest.items()))
|
|
384
443
|
generation_id = hashlib.blake2b(generation_raw.encode(), digest_size=16).hexdigest()
|
|
385
444
|
return ParsedRepository(
|
|
@@ -72,6 +72,7 @@ def resolve_relations(
|
|
|
72
72
|
if source is not None and source.language == "rust":
|
|
73
73
|
candidates = _rust_candidates(source, ref, candidates, symbols)
|
|
74
74
|
elif source is not None and source.language == "python":
|
|
75
|
+
definition_scope = source.metadata.get("python_definition_scopes", {}).get(ref.name)
|
|
75
76
|
if source.metadata.get("python_wildcard_import"):
|
|
76
77
|
candidates = []
|
|
77
78
|
elif ref.qualifier in {"self", "cls"}:
|
|
@@ -87,7 +88,10 @@ def resolve_relations(
|
|
|
87
88
|
elif (
|
|
88
89
|
ref.qualifier
|
|
89
90
|
or ref.name in source.metadata.get("python_bound_names", [])
|
|
90
|
-
or
|
|
91
|
+
or (
|
|
92
|
+
ref.name in source.metadata.get("python_import_names", [])
|
|
93
|
+
and definition_scope is None
|
|
94
|
+
)
|
|
91
95
|
):
|
|
92
96
|
candidates = []
|
|
93
97
|
else:
|
|
@@ -104,6 +108,10 @@ def resolve_relations(
|
|
|
104
108
|
and [*source.scope_chain, source.simple_name][: len(c.scope_chain)]
|
|
105
109
|
== c.scope_chain
|
|
106
110
|
]
|
|
111
|
+
# A lexical definition shadows imports and outer definitions even
|
|
112
|
+
# when its conditional body is not represented by this parser.
|
|
113
|
+
if definition_scope is not None:
|
|
114
|
+
candidates = [c for c in candidates if c.scope_chain == definition_scope]
|
|
107
115
|
nearest = max((len(c.scope_chain) for c in candidates), default=-1)
|
|
108
116
|
candidates = [c for c in candidates if len(c.scope_chain) == nearest]
|
|
109
117
|
elif source is not None and source.language in {"javascript", "typescript", "tsx"}:
|
|
@@ -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.
|
|
3
|
+
Version: 0.10.2
|
|
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,11 @@ 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.
|
|
59
|
+
This source tree is version **0.10.2**; consumers should use a compatible `<1.0` pin.
|
|
60
|
+
It repairs nested Python definition/import shadowing: proven local definitions
|
|
61
|
+
retain their targets, while unmodeled conditional bindings stay unresolved.
|
|
62
|
+
Rebuild derived graphs when adopting the new parser; unchanged source hashes do
|
|
63
|
+
not invalidate old semantic results automatically. Publication is a separate gate.
|
|
60
64
|
|
|
61
65
|
```bash
|
|
62
66
|
# from PyPI
|
|
@@ -128,10 +132,36 @@ records = to_proxima_records(repository, repo_graph_id="myrepo")
|
|
|
128
132
|
|
|
129
133
|
## Status
|
|
130
134
|
|
|
131
|
-
`0.10.
|
|
135
|
+
`0.10.2` source version. Python (`ast`) is the full, offline frontend. JS/TS, Go, Rust,
|
|
132
136
|
Java, C, and C++ have conformance fixtures; other mapped tree-sitter languages report
|
|
133
137
|
symbols-tier or fallback status rather than implying full fidelity.
|
|
134
138
|
|
|
139
|
+
### Ambiguous declarations
|
|
140
|
+
|
|
141
|
+
Duplicate structural identities retain raw declarations in `ParsedRepository.files`
|
|
142
|
+
and report each conflicting source location. Repository projection emits only the
|
|
143
|
+
canonical FILE marker for that file, with typed diagnostics, declaration spans and
|
|
144
|
+
omitted symbol/relation counts in metadata. Calls into its declarations remain
|
|
145
|
+
unresolved; healthy imports can still point to its diagnostic marker. Repairing the
|
|
146
|
+
source restores normal projection through deletion-complete repository deltas.
|
|
147
|
+
Direct record projection rejects duplicate identities before invoking an embedder,
|
|
148
|
+
including when legacy OIDs are selected.
|
|
149
|
+
|
|
150
|
+
`identity_collisions` groups use the existing `symbol_id` with `resolution: ambiguous`.
|
|
151
|
+
Each declaration retains `name`, `qualified_name`, `symbol_type` and its full
|
|
152
|
+
`source_span`; these are inspectable evidence, not separately resolved graph nodes.
|
|
153
|
+
The record adapter forwards these groups, projection status and omitted counts only
|
|
154
|
+
when present, without copying arbitrary symbol metadata into exported properties.
|
|
155
|
+
|
|
156
|
+
### TypeScript grammar limitation
|
|
157
|
+
|
|
158
|
+
The current tree-sitter grammar reports a partial parse for inline import types
|
|
159
|
+
in generic calls, for example `load<typeof import('child_process')>()`, even though
|
|
160
|
+
TypeScript accepts that syntax. A named type alias (`type ChildProcessModule =
|
|
161
|
+
typeof import('child_process')`, then `load<ChildProcessModule>()`) is supported.
|
|
162
|
+
Partial status and syntax diagnostics remain visible; the parser does not rewrite
|
|
163
|
+
source or infer missing relationships.
|
|
164
|
+
|
|
135
165
|
### Rust graph fidelity
|
|
136
166
|
|
|
137
167
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph/javascript_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{victor_codegraph-0.10.0 → victor_codegraph-0.10.2}/victor_codegraph.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|