victor-codegraph 0.1.2__tar.gz → 0.8.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 (35) hide show
  1. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/PKG-INFO +13 -15
  2. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/README.md +10 -12
  3. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/pyproject.toml +8 -3
  4. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_adapter.py +39 -5
  5. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_line_base_contract.py +4 -1
  6. victor_codegraph-0.8.1/tests/test_manifest.py +127 -0
  7. victor_codegraph-0.8.1/tests/test_sizing_overlap.py +132 -0
  8. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_treesitter_jsts.py +4 -1
  9. victor_codegraph-0.8.1/tests/test_treesitter_languages.py +326 -0
  10. victor_codegraph-0.8.1/tests/test_treesitter_relations.py +182 -0
  11. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/__init__.py +10 -2
  12. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/adapter.py +33 -2
  13. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/config.py +5 -1
  14. victor_codegraph-0.8.1/victor_codegraph/manifest.py +109 -0
  15. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/parser.py +3 -1
  16. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/python_parser.py +5 -22
  17. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/repo.py +44 -7
  18. victor_codegraph-0.8.1/victor_codegraph/resolution.py +68 -0
  19. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/sizing.py +24 -7
  20. victor_codegraph-0.8.1/victor_codegraph/treesitter_parser.py +666 -0
  21. victor_codegraph-0.8.1/victor_codegraph/treesitter_tables.py +70 -0
  22. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph.egg-info/PKG-INFO +13 -15
  23. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph.egg-info/SOURCES.txt +7 -0
  24. victor_codegraph-0.1.2/victor_codegraph/treesitter_parser.py +0 -296
  25. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/setup.cfg +0 -0
  26. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_python_parser.py +0 -0
  27. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_repo.py +0 -0
  28. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_sizing_and_chunk.py +0 -0
  29. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_symbol_oid_contract.py +0 -0
  30. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/tests/test_symbol_oid_parity.py +0 -0
  31. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/languages.py +0 -0
  32. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph/model.py +0 -0
  33. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph.egg-info/dependency_links.txt +0 -0
  34. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph.egg-info/requires.txt +0 -0
  35. {victor_codegraph-0.1.2 → victor_codegraph-0.8.1}/victor_codegraph.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: victor-codegraph
3
- Version: 0.1.2
3
+ Version: 0.8.1
4
4
  Summary: Code->CPG chunker: tree-sitter symbol + relation extraction, size-capped chunks, ProximaRecord projection. Shared by Victor, ProximaDB SDK, and AnvaiOps.
5
5
  Author-email: Vijaykumar Singh <singhvjd@gmail.com>
6
6
  License: Apache-2.0
7
- Project-URL: Homepage, https://github.com/vjsingh1984/victor
8
- Project-URL: Repository, https://github.com/vjsingh1984/victor
7
+ Project-URL: Homepage, https://github.com/anvai-labs/victor
8
+ Project-URL: Repository, https://github.com/anvai-labs/victor
9
9
  Keywords: code-graph,cpg,chunking,tree-sitter,ast,embeddings,rag
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
@@ -53,20 +53,18 @@ gaps:
53
53
 
54
54
  ## Install
55
55
 
56
- Not yet published to PyPI — use an **editable install** from the monorepo for now. Consumers
57
- (Victor, the ProximaDB SDK, AnvaiOps) reference it editable until the first `victor-codegraph-v*`
58
- release is cut.
56
+ Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/)
57
+ (current release **0.1.2**). Consumers pin the PyPI release — the ProximaDB SDK's
58
+ `proximadb[codegraph]` extra and AnvaiOps both require `victor-codegraph>=0.1.2`.
59
59
 
60
60
  ```bash
61
- # dev: editable, with tree-sitter grammars + test tooling
62
- make -C victor-codegraph dev # = pip install -e ../victor-contracts && pip install -e ".[dev]"
63
-
64
- # minimal: Python-only (stdlib ast) path, zero native deps
65
- pip install -e ./victor-codegraph
61
+ # from PyPI
62
+ pip install victor-codegraph # Python-only (stdlib ast) path, zero native deps
63
+ pip install "victor-codegraph[treesitter]" # + multi-language tree-sitter grammars
66
64
 
67
- # once published:
68
- # pip install victor-codegraph # Python path
69
- # pip install "victor-codegraph[treesitter]" # + multi-language grammars
65
+ # dev (monorepo): editable, with tree-sitter grammars + test tooling
66
+ make -C victor-codegraph dev # = pip install -e ../victor-contracts && pip install -e ".[dev]"
67
+ pip install -e ./victor-codegraph # minimal editable install
70
68
  ```
71
69
 
72
70
  ### Releasing
@@ -104,6 +102,6 @@ records = to_proxima_records(parsed, repo_graph_id="myrepo", branch_id="main",
104
102
 
105
103
  ## Status
106
104
 
107
- `0.1.0` — TD-CG1 scaffold. Python (stdlib `ast`) is the primary, fully-offline path.
105
+ `0.1.2` (PyPI). Python (stdlib `ast`) is the primary, fully-offline path.
108
106
  Multi-language extraction is best-effort via tree-sitter; deeper per-language relation
109
107
  extraction (the donor parsers' Rust/Go/Java specifics) lands incrementally.
@@ -23,20 +23,18 @@ gaps:
23
23
 
24
24
  ## Install
25
25
 
26
- Not yet published to PyPI — use an **editable install** from the monorepo for now. Consumers
27
- (Victor, the ProximaDB SDK, AnvaiOps) reference it editable until the first `victor-codegraph-v*`
28
- release is cut.
26
+ Published on PyPI as [`victor-codegraph`](https://pypi.org/project/victor-codegraph/)
27
+ (current release **0.1.2**). Consumers pin the PyPI release — the ProximaDB SDK's
28
+ `proximadb[codegraph]` extra and AnvaiOps both require `victor-codegraph>=0.1.2`.
29
29
 
30
30
  ```bash
31
- # dev: editable, with tree-sitter grammars + test tooling
32
- make -C victor-codegraph dev # = pip install -e ../victor-contracts && pip install -e ".[dev]"
33
-
34
- # minimal: Python-only (stdlib ast) path, zero native deps
35
- pip install -e ./victor-codegraph
31
+ # from PyPI
32
+ pip install victor-codegraph # Python-only (stdlib ast) path, zero native deps
33
+ pip install "victor-codegraph[treesitter]" # + multi-language tree-sitter grammars
36
34
 
37
- # once published:
38
- # pip install victor-codegraph # Python path
39
- # pip install "victor-codegraph[treesitter]" # + multi-language grammars
35
+ # dev (monorepo): editable, with tree-sitter grammars + test tooling
36
+ make -C victor-codegraph dev # = pip install -e ../victor-contracts && pip install -e ".[dev]"
37
+ pip install -e ./victor-codegraph # minimal editable install
40
38
  ```
41
39
 
42
40
  ### Releasing
@@ -74,6 +72,6 @@ records = to_proxima_records(parsed, repo_graph_id="myrepo", branch_id="main",
74
72
 
75
73
  ## Status
76
74
 
77
- `0.1.0` — TD-CG1 scaffold. Python (stdlib `ast`) is the primary, fully-offline path.
75
+ `0.1.2` (PyPI). Python (stdlib `ast`) is the primary, fully-offline path.
78
76
  Multi-language extraction is best-effort via tree-sitter; deeper per-language relation
79
77
  extraction (the donor parsers' Rust/Go/Java specifics) lands incrementally.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "victor-codegraph"
7
- version = "0.1.2"
7
+ version = "0.8.1"
8
8
  description = "Code->CPG chunker: tree-sitter symbol + relation extraction, size-capped chunks, ProximaRecord projection. Shared by Victor, ProximaDB SDK, and AnvaiOps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -55,8 +55,8 @@ dev = [
55
55
  ]
56
56
 
57
57
  [project.urls]
58
- Homepage = "https://github.com/vjsingh1984/victor"
59
- Repository = "https://github.com/vjsingh1984/victor"
58
+ Homepage = "https://github.com/anvai-labs/victor"
59
+ Repository = "https://github.com/anvai-labs/victor"
60
60
 
61
61
  [tool.setuptools.packages.find]
62
62
  where = ["."]
@@ -66,6 +66,11 @@ include = ["victor_codegraph*"]
66
66
  line-length = 100
67
67
  target-version = "py310"
68
68
 
69
+ # Pin the rule set explicitly: the CI workflow installs the latest ruff, whose
70
+ # evolving defaults must not be able to red a path-filtered pipeline overnight.
71
+ [tool.ruff.lint]
72
+ select = ["E4", "E7", "E9", "F"]
73
+
69
74
  # Self-contained pytest config so running `pytest` from this package directory uses
70
75
  # THIS file as the rootdir/inifile and does NOT inherit the victor monorepo root's
71
76
  # `addopts = --cov=victor` (pytest-cov isn't a dep here -> "unrecognized arguments").
@@ -4,14 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from victor_codegraph import parse, to_proxima_records
6
6
 
7
- SAMPLE = '''\
7
+ SAMPLE = """\
8
8
  def a():
9
9
  return b()
10
10
 
11
11
 
12
12
  def b():
13
13
  return 1
14
- '''
14
+ """
15
15
 
16
16
 
17
17
  def test_symbol_records_shape():
@@ -45,12 +45,46 @@ def test_edge_records_reference_node_oids():
45
45
 
46
46
  def test_embedder_populates_embedding_cell():
47
47
  parsed = parse(SAMPLE, file_path="m.py")
48
- records = to_proxima_records(
49
- parsed, repo_graph_id="repo1", embedder=lambda text: [0.0] * 384
50
- )
48
+ records = to_proxima_records(parsed, repo_graph_id="repo1", embedder=lambda text: [0.0] * 384)
51
49
  node = next(r for r in records if "graph_node" in r["labels"])
52
50
  assert len(node["embeddings"]) == 1
53
51
  cell = node["embeddings"][0]
54
52
  assert cell["modality"] == "code"
55
53
  assert cell["dim"] == 384
56
54
  assert len(cell["values"]) == 384
55
+
56
+
57
+ def test_batch_embedder_single_call_populates_all_nodes():
58
+ parsed = parse(SAMPLE, file_path="m.py")
59
+ calls: list[list[str]] = []
60
+
61
+ def batch(texts: list[str]) -> list[list[float]]:
62
+ calls.append(texts)
63
+ return [[0.5] * 8 for _ in texts]
64
+
65
+ records = to_proxima_records(parsed, repo_graph_id="repo1", batch_embedder=batch, dim=8)
66
+ nodes = [r for r in records if "graph_node" in r["labels"]]
67
+
68
+ assert len(calls) == 1 # ONE call for the whole file
69
+ assert len(calls[0]) == len(nodes)
70
+ for n in nodes:
71
+ assert len(n["embeddings"]) == 1
72
+ assert n["embeddings"][0]["dim"] == 8
73
+ assert n["embeddings"][0]["values"] == [0.5] * 8
74
+
75
+
76
+ def test_batch_embedder_wins_over_per_symbol_embedder():
77
+ parsed = parse(SAMPLE, file_path="m.py")
78
+ per_symbol_calls: list[str] = []
79
+
80
+ records = to_proxima_records(
81
+ parsed,
82
+ repo_graph_id="repo1",
83
+ embedder=lambda text: per_symbol_calls.append(text) or [0.0] * 8,
84
+ batch_embedder=lambda texts: [[1.0] * 8 for _ in texts],
85
+ dim=8,
86
+ )
87
+ nodes = [r for r in records if "graph_node" in r["labels"]]
88
+
89
+ assert per_symbol_calls == [] # per-symbol path not used
90
+ assert all(n["embeddings"][0]["values"] == [1.0] * 8 for n in nodes)
@@ -16,7 +16,10 @@ def test_line_base_is_one():
16
16
 
17
17
  def test_parse_emits_1_based_lines():
18
18
  # The def is on the FIRST line → start_line must be 1 (not 0).
19
- parsed = parse("def first():\n return second()\n\n\ndef second():\n return 1\n", file_path="m.py")
19
+ parsed = parse(
20
+ "def first():\n return second()\n\n\ndef second():\n return 1\n",
21
+ file_path="m.py",
22
+ )
20
23
  by_name = {s.simple_name: s for s in parsed.symbols}
21
24
  assert by_name["first"].location.start_line == 1
22
25
  assert by_name["second"].location.start_line == 5 # 1-based line of the 2nd def
@@ -0,0 +1,127 @@
1
+ """Incremental manifest contract + encoding fallback (offline, tmp_path only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from victor_codegraph import (
6
+ build_manifest,
7
+ diff_manifest,
8
+ iter_changed_files,
9
+ parse_path,
10
+ read_source_text,
11
+ )
12
+
13
+
14
+ def _seed(tmp_path):
15
+ (tmp_path / "pkg").mkdir()
16
+ (tmp_path / "pkg" / "a.py").write_text("def a():\n return 1\n")
17
+ (tmp_path / "pkg" / "b.py").write_text("def b():\n return 2\n")
18
+ (tmp_path / "README.md").write_text("# not source\n") # unrecognized: excluded
19
+ return tmp_path
20
+
21
+
22
+ def test_build_manifest_relative_posix_keys(tmp_path):
23
+ manifest = build_manifest(_seed(tmp_path))
24
+ assert set(manifest) == {"pkg/a.py", "pkg/b.py"}
25
+ assert all("\\" not in k for k in manifest)
26
+ assert all(len(v) == 64 for v in manifest.values()) # sha-256 hex
27
+
28
+
29
+ def test_manifest_hash_matches_parse_path(tmp_path):
30
+ root = _seed(tmp_path)
31
+ manifest = build_manifest(root)
32
+ parsed = parse_path(root / "pkg" / "a.py")
33
+ assert parsed is not None
34
+ assert manifest["pkg/a.py"] == parsed.content_hash
35
+
36
+
37
+ def test_diff_manifest_classifies_all_states(tmp_path):
38
+ root = _seed(tmp_path)
39
+ previous = build_manifest(root)
40
+
41
+ (root / "pkg" / "a.py").write_text("def a():\n return 42\n") # changed
42
+ (root / "pkg" / "b.py").unlink() # removed
43
+ (root / "pkg" / "c.py").write_text("def c():\n return 3\n") # added
44
+
45
+ current = build_manifest(root)
46
+ diff = diff_manifest(current, previous)
47
+ assert diff.added == ["pkg/c.py"]
48
+ assert diff.changed == ["pkg/a.py"]
49
+ assert diff.removed == ["pkg/b.py"]
50
+ assert diff.unchanged == []
51
+
52
+
53
+ def test_diff_manifest_unchanged(tmp_path):
54
+ root = _seed(tmp_path)
55
+ manifest = build_manifest(root)
56
+ diff = diff_manifest(manifest, dict(manifest))
57
+ assert diff.added == diff.changed == diff.removed == []
58
+ assert set(diff.unchanged) == {"pkg/a.py", "pkg/b.py"}
59
+
60
+
61
+ def test_iter_changed_files_none_previous_yields_all(tmp_path):
62
+ root = _seed(tmp_path)
63
+ changed = list(iter_changed_files(root, None))
64
+ assert {p.name for p, _ in changed} == {"a.py", "b.py"}
65
+ # Yielded hashes ARE the next manifest's values.
66
+ assert {h for _, h in changed} == set(build_manifest(root).values())
67
+
68
+
69
+ def test_iter_changed_files_skips_unchanged(tmp_path):
70
+ root = _seed(tmp_path)
71
+ previous = build_manifest(root)
72
+ assert list(iter_changed_files(root, previous)) == []
73
+
74
+ (root / "pkg" / "a.py").write_text("def a():\n return 99\n")
75
+ changed = list(iter_changed_files(root, previous))
76
+ assert [p.name for p, _ in changed] == ["a.py"]
77
+ # The yielded hash reflects the NEW content.
78
+ assert changed[0][1] == build_manifest(root)["pkg/a.py"]
79
+
80
+
81
+ def test_single_file_root(tmp_path):
82
+ f = tmp_path / "solo.py"
83
+ f.write_text("def solo():\n return 0\n")
84
+ manifest = build_manifest(f)
85
+ assert set(manifest) == {"solo.py"}
86
+
87
+
88
+ # ── encoding fallback ────────────────────────────────────────────────────────
89
+
90
+
91
+ def test_latin1_file_no_longer_skipped(tmp_path):
92
+ f = tmp_path / "legacy.py"
93
+ f.write_bytes("# caf\xe9\ndef caf():\n return 1\n".encode("latin-1"))
94
+ text = read_source_text(f)
95
+ assert text is not None
96
+ assert "caf\xe9" in text
97
+
98
+ parsed = parse_path(f)
99
+ assert parsed is not None
100
+ assert [s.simple_name for s in parsed.symbols] == ["caf"]
101
+
102
+ # And it participates in manifests deterministically.
103
+ manifest = build_manifest(tmp_path)
104
+ assert manifest["legacy.py"] == parsed.content_hash
105
+
106
+
107
+ def test_utf8_bom_stripped(tmp_path):
108
+ f = tmp_path / "bom.py"
109
+ f.write_bytes(b"\xef\xbb\xbf" + b"def bom():\n return 1\n")
110
+ text = read_source_text(f)
111
+ assert text is not None
112
+ assert not text.startswith("")
113
+ parsed = parse_path(f)
114
+ assert parsed is not None
115
+ assert [s.simple_name for s in parsed.symbols] == ["bom"]
116
+
117
+
118
+ def test_fallback_disabled_restores_skip_behavior(tmp_path):
119
+ f = tmp_path / "legacy.py"
120
+ f.write_bytes("# caf\xe9\n".encode("latin-1"))
121
+ assert read_source_text(f, encoding_fallback=False) is None
122
+ assert parse_path(f, encoding_fallback=False) is None
123
+
124
+
125
+ def test_unreadable_file_returns_none(tmp_path):
126
+ assert read_source_text(tmp_path / "missing.py") is None
127
+ assert parse_path(tmp_path / "missing.py") is None
@@ -0,0 +1,132 @@
1
+ """Overlap correctness and no-data-loss guarantees for body-splitting.
2
+
3
+ Pins the fix for the minified-line hard-cut: a single line over the char budget
4
+ used to be truncated (``ln[:max_chars]``), silently dropping the remainder.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from victor_codegraph.config import ChunkConfig
10
+ from victor_codegraph.model import CodeSymbol, CodeSymbolType, SourceLocation
11
+ from victor_codegraph.sizing import chunks_for_symbol
12
+
13
+
14
+ def _cfg(max_tokens: int = 40, overlap_tokens: int = 8) -> ChunkConfig:
15
+ # Small budgets so tests exercise splitting with tiny fixtures.
16
+ return ChunkConfig(max_chunk_tokens=max_tokens, chunk_overlap_tokens=overlap_tokens)
17
+
18
+
19
+ def _symbol(source: str, start_line: int = 10) -> CodeSymbol:
20
+ line_count = source.count("\n") + (0 if source.endswith("\n") else 1)
21
+ return CodeSymbol(
22
+ id="sym1",
23
+ symbol_type=CodeSymbolType.FUNCTION,
24
+ fully_qualified_name="f.py::big",
25
+ simple_name="big",
26
+ location=SourceLocation(
27
+ file_path="f.py",
28
+ start_line=start_line,
29
+ end_line=start_line + line_count - 1,
30
+ ),
31
+ source_code=source,
32
+ language="python",
33
+ )
34
+
35
+
36
+ def _multiline_source(n_lines: int = 120) -> str:
37
+ return "".join(f" value_{i} = compute_{i}(x)\n" for i in range(n_lines))
38
+
39
+
40
+ def test_no_chunk_exceeds_budget():
41
+ cfg = _cfg()
42
+ chunks = chunks_for_symbol(_symbol(_multiline_source()), cfg)
43
+ assert len(chunks) > 1
44
+ assert all(len(c.text) <= cfg.max_chunk_chars for c in chunks)
45
+
46
+
47
+ def test_every_source_line_appears_in_some_chunk():
48
+ cfg = _cfg()
49
+ source = _multiline_source()
50
+ chunks = chunks_for_symbol(_symbol(source), cfg)
51
+ joined = [c.text for c in chunks]
52
+ for line in source.splitlines():
53
+ assert any(line in text for text in joined), f"line lost: {line!r}"
54
+
55
+
56
+ def test_consecutive_windows_overlap_by_whole_lines():
57
+ # Overlap is whole-line granular: give it budget for at least one line.
58
+ cfg = _cfg(max_tokens=40, overlap_tokens=16)
59
+ chunks = chunks_for_symbol(_symbol(_multiline_source()), cfg)
60
+ assert len(chunks) > 1
61
+ for a, b in zip(chunks, chunks[1:]):
62
+ # The next window begins with whole lines that terminate the previous one:
63
+ # some non-empty line-suffix of `a` is a prefix of `b`, within budget.
64
+ a_lines = a.text.splitlines(keepends=True)
65
+ overlap = 0
66
+ for k in range(len(a_lines)):
67
+ suffix = "".join(a_lines[k:])
68
+ if b.text.startswith(suffix):
69
+ overlap = len(suffix)
70
+ break
71
+ assert overlap > 0
72
+ assert overlap <= cfg.chunk_overlap_chars
73
+
74
+
75
+ def test_body_split_window_line_metadata():
76
+ cfg = _cfg()
77
+ sym = _symbol(_multiline_source(), start_line=10)
78
+ chunks = chunks_for_symbol(sym, cfg)
79
+ for c in chunks:
80
+ start = c.metadata["start_line"]
81
+ end = c.metadata["end_line"]
82
+ assert start >= 10
83
+ assert end >= start
84
+ # end_line reflects THIS window, not the symbol's overall end.
85
+ spanned = c.text.count("\n") + (0 if c.text.endswith("\n") else 1)
86
+ assert end == start + spanned - 1
87
+ assert chunks[0].metadata["start_line"] == 10
88
+
89
+
90
+ def test_minified_line_is_not_truncated():
91
+ """A single line 10x over budget must be fully preserved across chunks."""
92
+ cfg = _cfg()
93
+ long_line = "x=1;" * (cfg.max_chunk_chars * 10 // 4)
94
+ source = long_line + "\n"
95
+ chunks = chunks_for_symbol(_symbol(source), cfg)
96
+
97
+ assert len(chunks) > 1
98
+ assert all(len(c.text) <= cfg.max_chunk_chars for c in chunks)
99
+ assert all(c.metadata.get("is_line_split") for c in chunks)
100
+
101
+ # Consecutive pieces advance by (max - overlap); every char offset is covered
102
+ # and the full line (incl. trailing newline) is reconstructible.
103
+ full = long_line + "\n"
104
+ step = cfg.max_chunk_chars - cfg.chunk_overlap_chars
105
+ buf = [""] * len(full)
106
+ for idx, c in enumerate(chunks):
107
+ start = idx * step
108
+ for j, ch in enumerate(c.text):
109
+ buf[start + j] = ch
110
+ assert "".join(buf) == full
111
+
112
+
113
+ def test_minified_line_mixed_with_normal_lines():
114
+ cfg = _cfg()
115
+ long_line = "y=2;" * (cfg.max_chunk_chars // 2)
116
+ source = "def big():\n" + f" a = {long_line}\n" + " return a\n"
117
+ sym = _symbol(source)
118
+ chunks = chunks_for_symbol(sym, cfg)
119
+ text_all = "".join(c.text for c in chunks)
120
+ assert "def big():" in text_all
121
+ assert "return a" in text_all
122
+ # The long middle line's tail must not be lost.
123
+ assert text_all.count("y=2;") >= source.count("y=2;")
124
+
125
+
126
+ def test_small_symbol_single_chunk_has_no_line_split_flag():
127
+ cfg = _cfg(max_tokens=512, overlap_tokens=64)
128
+ sym = _symbol("def small():\n return 1\n")
129
+ chunks = chunks_for_symbol(sym, cfg)
130
+ assert len(chunks) == 1
131
+ assert "is_line_split" not in chunks[0].metadata
132
+ assert "is_body_split" not in chunks[0].metadata
@@ -11,7 +11,10 @@ import pytest
11
11
  pytest.importorskip("tree_sitter_language_pack")
12
12
 
13
13
  from victor_codegraph import CodeSymbolType, parse # noqa: E402
14
- from victor_codegraph.treesitter_parser import GrammarUnavailable, parse_treesitter # noqa: E402
14
+ from victor_codegraph.treesitter_parser import (
15
+ GrammarUnavailable,
16
+ parse_treesitter,
17
+ ) # noqa: E402
15
18
 
16
19
  JS = """\
17
20
  import { x } from "./x";