codecompass-mcp 2.4.0__tar.gz → 2.5.0__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 (41) hide show
  1. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/PKG-INFO +1 -1
  2. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/PKG-INFO +1 -1
  3. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/SOURCES.txt +2 -0
  4. codecompass_mcp-2.5.0/ingestion/description_enricher.py +189 -0
  5. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/main.py +81 -9
  6. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/pyproject.toml +1 -1
  7. codecompass_mcp-2.5.0/tests/test_description_enricher.py +95 -0
  8. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/LICENSE +0 -0
  9. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/README.md +0 -0
  10. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  11. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  12. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  13. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
  14. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/config.py +0 -0
  15. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/graph/__init__.py +0 -0
  16. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/graph/cli.py +0 -0
  17. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/graph/code_graph_client.py +0 -0
  18. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/graph/code_query_cli.py +0 -0
  19. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/graph/setup.py +0 -0
  20. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/__init__.py +0 -0
  21. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/chunker.py +0 -0
  22. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/code_normalizer.py +0 -0
  23. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/code_parser.py +0 -0
  24. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/entity_resolver.py +0 -0
  25. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/file_watcher.py +0 -0
  26. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/graph_writer.py +0 -0
  27. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/hierarchy_builder.py +0 -0
  28. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/reader_agent.py +0 -0
  29. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/ingestion/source_context.py +0 -0
  30. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/models/__init__.py +0 -0
  31. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/models/code_types.py +0 -0
  32. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/models/types.py +0 -0
  33. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/setup.cfg +0 -0
  34. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_batch_impact.py +0 -0
  35. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_blast_radius.py +0 -0
  36. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_code_parser.py +0 -0
  37. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_ingestion.py +0 -0
  38. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_lit_css_parser.py +0 -0
  39. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/tests/test_php_parser.py +0 -0
  40. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/utils/__init__.py +0 -0
  41. {codecompass_mcp-2.4.0 → codecompass_mcp-2.5.0}/utils/formatting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -18,6 +18,7 @@ ingestion/__init__.py
18
18
  ingestion/chunker.py
19
19
  ingestion/code_normalizer.py
20
20
  ingestion/code_parser.py
21
+ ingestion/description_enricher.py
21
22
  ingestion/entity_resolver.py
22
23
  ingestion/file_watcher.py
23
24
  ingestion/graph_writer.py
@@ -30,6 +31,7 @@ models/types.py
30
31
  tests/test_batch_impact.py
31
32
  tests/test_blast_radius.py
32
33
  tests/test_code_parser.py
34
+ tests/test_description_enricher.py
33
35
  tests/test_ingestion.py
34
36
  tests/test_lit_css_parser.py
35
37
  tests/test_php_parser.py
@@ -0,0 +1,189 @@
1
+ """Description enrichment for the code knowledge graph — agent-swarm edition.
2
+
3
+ Tree-sitter gives us structure; this pass fills in the meaning. Rather than
4
+ calling any single vendor's API directly, it stages the work as plain files
5
+ and hands control back to whichever coding agent is driving the CLI (Claude
6
+ Code, Codex, Gemini, or anything else). That agent dispatches its own native
7
+ sub-agents — one per batch — to write descriptions, then `describe --apply`
8
+ merges the results into the graph. No API key, no vendor lock-in.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from typing import Any
16
+
17
+ from graph.code_graph_client import get_client
18
+ from ingestion.source_context import extract_entity_context
19
+
20
+ DEFAULT_BATCH_SIZE = 15
21
+ DESCRIBE_SUBDIR = os.path.join(".codecompass", "describe")
22
+
23
+ _INSTRUCTIONS_TEMPLATE = """\
24
+ # Description enrichment — agent swarm task
25
+
26
+ {num_entities} entities across {num_batches} batch file(s) in this directory
27
+ need a one-sentence description written for the code knowledge graph.
28
+
29
+ ## What to do
30
+
31
+ 1. For each `batch_XXXX.json` file here, dispatch one sub-agent (in parallel).
32
+ Give each sub-agent this task:
33
+
34
+ > Read `{describe_dir}/batch_XXXX.json`. It is a JSON list of entities,
35
+ > each with `id`, `name`, `kind`, `file`, `signature`, `docstring`, and
36
+ > `snippet`. For every entity, write ONE clear sentence describing what
37
+ > it does or its role in the codebase. If signature/docstring/snippet are
38
+ > empty, infer a sensible description from the name, kind, and file path.
39
+ > Write the result to `{describe_dir}/batch_XXXX.result.json` as a JSON
40
+ > object mapping each entity's `id` to its description string. Output
41
+ > nothing else — no markdown fences, no commentary, just the file.
42
+
43
+ 2. Once every `batch_XXXX.result.json` file exists, run:
44
+
45
+ ```
46
+ codecompass describe {repo_path} --apply
47
+ ```
48
+
49
+ This merges every result file into the graph and cleans up this directory.
50
+
51
+ ## Batches
52
+
53
+ {batch_list}
54
+ """
55
+
56
+
57
+ def _entity_context(repo_path: str, node_id: str, attr: dict[str, Any]) -> dict[str, Any]:
58
+ """Build the sub-agent payload for a single entity."""
59
+ name = attr.get("name", "")
60
+ file = attr.get("file", "")
61
+ ctx = extract_entity_context(repo_path, file, name) if file else {}
62
+ return {
63
+ "id": node_id,
64
+ "name": name,
65
+ "kind": attr.get("kind", ""),
66
+ "file": file,
67
+ "signature": ctx.get("signature", ""),
68
+ "docstring": ctx.get("docstring", ""),
69
+ "snippet": ctx.get("snippet", ""),
70
+ }
71
+
72
+
73
+ def _describe_dir(repo_path: str) -> str:
74
+ return os.path.join(os.path.abspath(repo_path), DESCRIBE_SUBDIR)
75
+
76
+
77
+ def prepare_describe_batches(
78
+ repo_path: str, batch_size: int = DEFAULT_BATCH_SIZE, force: bool = False
79
+ ) -> dict[str, Any]:
80
+ """Stage entities needing descriptions into batch files plus an instructions doc.
81
+
82
+ Returns {"num_entities", "num_batches", "describe_dir", "instructions_path"}.
83
+ Writing no batches (num_entities == 0) leaves no files behind.
84
+
85
+ Raises RuntimeError if a previous run already has result files staged —
86
+ re-running would silently discard that in-progress work. Pass force=True
87
+ to overwrite anyway.
88
+ """
89
+ repo_path = os.path.abspath(repo_path)
90
+ project = os.path.basename(repo_path)
91
+ describe_dir = _describe_dir(repo_path)
92
+
93
+ if not force and os.path.isdir(describe_dir):
94
+ existing_results = [f for f in os.listdir(describe_dir) if f.endswith(".result.json")]
95
+ if existing_results:
96
+ raise RuntimeError(
97
+ f"{len(existing_results)} result file(s) already staged at {describe_dir} "
98
+ "from a previous describe run. Run `codecompass describe "
99
+ f"{repo_path} --apply` to finish it first, or re-run with force=True "
100
+ "to discard them and restage."
101
+ )
102
+
103
+ client = get_client(repo_path)
104
+ try:
105
+ entities = [
106
+ (node_id, attr)
107
+ for node_id, attr in client.graph.nodes(data=True)
108
+ if attr.get("type") == "Entity"
109
+ and attr.get("project") == project
110
+ and attr.get("file")
111
+ ]
112
+ finally:
113
+ client.close()
114
+
115
+ result = {
116
+ "num_entities": len(entities),
117
+ "num_batches": 0,
118
+ "describe_dir": describe_dir,
119
+ "instructions_path": None,
120
+ }
121
+ if not entities:
122
+ return result
123
+
124
+ os.makedirs(describe_dir, exist_ok=True)
125
+
126
+ batches = [entities[i : i + batch_size] for i in range(0, len(entities), batch_size)]
127
+ batch_names = []
128
+ for idx, batch in enumerate(batches):
129
+ batch_name = f"batch_{idx:04d}.json"
130
+ payload = [_entity_context(repo_path, node_id, attr) for node_id, attr in batch]
131
+ with open(os.path.join(describe_dir, batch_name), "w", encoding="utf-8") as f:
132
+ json.dump(payload, f, ensure_ascii=False, indent=2)
133
+ batch_names.append(batch_name)
134
+
135
+ instructions_path = os.path.join(describe_dir, "INSTRUCTIONS.md")
136
+ with open(instructions_path, "w", encoding="utf-8") as f:
137
+ f.write(
138
+ _INSTRUCTIONS_TEMPLATE.format(
139
+ num_entities=len(entities),
140
+ num_batches=len(batches),
141
+ describe_dir=describe_dir,
142
+ repo_path=repo_path,
143
+ batch_list="\n".join(f"- {name}" for name in batch_names),
144
+ )
145
+ )
146
+
147
+ result["num_batches"] = len(batches)
148
+ result["instructions_path"] = instructions_path
149
+ return result
150
+
151
+
152
+ def apply_describe_results(repo_path: str) -> int:
153
+ """Merge every `batch_*.result.json` file into the graph and clean up.
154
+
155
+ Returns the number of nodes updated. Raises FileNotFoundError if no
156
+ staged batch directory exists (nothing to apply).
157
+ """
158
+ repo_path = os.path.abspath(repo_path)
159
+ describe_dir = _describe_dir(repo_path)
160
+ if not os.path.isdir(describe_dir):
161
+ raise FileNotFoundError(
162
+ f"No staged description batches found at {describe_dir}. "
163
+ "Run `codecompass describe <repo_path>` first."
164
+ )
165
+
166
+ descriptions: dict[str, str] = {}
167
+ result_files = sorted(f for f in os.listdir(describe_dir) if f.endswith(".result.json"))
168
+ for name in result_files:
169
+ with open(os.path.join(describe_dir, name), encoding="utf-8") as f:
170
+ try:
171
+ descriptions.update(json.load(f))
172
+ except json.JSONDecodeError:
173
+ continue
174
+
175
+ client = get_client(repo_path)
176
+ try:
177
+ updated = 0
178
+ for node_id, description in descriptions.items():
179
+ if node_id in client.graph:
180
+ client.graph.nodes[node_id]["description"] = description
181
+ updated += 1
182
+ client.save()
183
+ finally:
184
+ client.close()
185
+
186
+ import shutil
187
+
188
+ shutil.rmtree(describe_dir, ignore_errors=True)
189
+ return updated
@@ -92,32 +92,41 @@ def init_project(repo_path: str) -> None:
92
92
  _register_project_agents_md(repo_path)
93
93
 
94
94
 
95
+ _GITIGNORE_ENTRIES = [
96
+ (".codecompass/describe/", "# CodeCompass transient describe-swarm staging dir"),
97
+ ]
98
+
99
+
95
100
  def _ensure_gitignore(repo_path: str) -> None:
96
- """Add the generated graph to .gitignore so it isn't committed (notes are)."""
97
- entry = ".codecompass/graph.json"
101
+ """Add generated/transient CodeCompass artifacts to .gitignore (notes are kept)."""
98
102
  gitignore_path = os.path.join(repo_path, ".gitignore")
99
103
 
100
104
  lines = []
101
105
  if os.path.exists(gitignore_path):
102
106
  with open(gitignore_path) as f:
103
107
  lines = f.read().splitlines()
104
- if any(line.strip() == entry for line in lines):
105
- return
108
+
109
+ missing = [(entry, comment) for entry, comment in _GITIGNORE_ENTRIES
110
+ if not any(line.strip() == entry for line in lines)]
111
+ if not missing:
112
+ return
106
113
 
107
114
  with open(gitignore_path, "a") as f:
108
115
  if lines and lines[-1].strip():
109
116
  f.write("\n")
110
- f.write("# CodeCompass generated graph (regenerated by ingest-code)\n")
111
- f.write(entry + "\n")
117
+ for entry, comment in missing:
118
+ f.write(comment + "\n")
119
+ f.write(entry + "\n")
112
120
 
113
121
 
114
- def ingest_code(repo_path: str, normalize: bool = False, dump_triples: str | None = None) -> None:
122
+ def ingest_code(repo_path: str, normalize: bool = False, dump_triples: str | None = None, describe: bool = False) -> None:
115
123
  """Ingest a codebase into the local code knowledge graph.
116
124
 
117
125
  Phase 1: Walk the repo and write the Project → Folder → File skeleton.
118
126
  Phase 2: Parse every source file with tree-sitter into CodeTriples.
119
127
  Phase 3: Normalize entity names via Haiku (only if --normalize is passed).
120
128
  Phase 4: Write all triples to the local graph.json.
129
+ Phase 5 (optional): Stage entity descriptions for an agent swarm to fill in.
121
130
  """
122
131
  import json
123
132
 
@@ -185,6 +194,20 @@ def ingest_code(repo_path: str, normalize: bool = False, dump_triples: str | Non
185
194
  )
186
195
  _register_project_agents_md(repo_path)
187
196
 
197
+ if describe:
198
+ from ingestion.description_enricher import prepare_describe_batches
199
+ console.print("[dim]Phase 5/5 — Staging descriptions for the agent swarm…[/]")
200
+ staged = prepare_describe_batches(repo_path)
201
+ if staged["num_entities"] == 0:
202
+ console.print("[dim] Nothing to describe.[/]")
203
+ else:
204
+ console.print(
205
+ f"[bold green]Staged[/] {staged['num_entities']} entities in "
206
+ f"{staged['num_batches']} batch(es). Read {staged['instructions_path']} "
207
+ "and dispatch a sub-agent per batch, then run "
208
+ f"`codecompass describe {repo_path} --apply`."
209
+ )
210
+
188
211
 
189
212
  def _register_project_agents_md(repo_path: str) -> None:
190
213
  """Write or update the Code graph section in the project's AGENTS.md."""
@@ -286,6 +309,18 @@ doc → `memory.md`; code-comment warning to the next person → `learnings.md`.
286
309
  - After adding, renaming, or deleting source files
287
310
  - After major refactors (moved functions, renamed classes)
288
311
  - If query results look stale or incomplete
312
+
313
+ ### Description enrichment — user-triggered ONLY
314
+
315
+ `codecompass describe` (and `ingest-code --describe`) stage entity descriptions
316
+ for an agent swarm to fill in (see `.codecompass/describe/INSTRUCTIONS.md` when
317
+ staged). This is expensive and **must only run when the user explicitly asks**
318
+ for descriptions to be added or refreshed (e.g. "describe this codebase",
319
+ "add descriptions", "enrich the graph").
320
+
321
+ **Do NOT run `describe` automatically** after re-ingesting, editing files, or
322
+ any other routine step — routine re-ingestion is `codecompass ingest-code`
323
+ with no `--describe` flag.
289
324
  {_CODECOMPASS_END}"""
290
325
 
291
326
  agents_md_path = os.path.join(repo_path, "AGENTS.md")
@@ -356,7 +391,8 @@ def main():
356
391
  usage = (
357
392
  f"[bold]Usage:[/]\n"
358
393
  f" {prog} init [italic]<repo_path>[/]\n"
359
- f" {prog} ingest-code [italic]<repo_path>[/] [--normalize] [--dump-triples [italic]<out.json>[/]]\n"
394
+ f" {prog} ingest-code [italic]<repo_path>[/] [--normalize] [--dump-triples [italic]<out.json>[/]] [--describe]\n"
395
+ f" {prog} describe [italic]<repo_path>[/] [--batch-size N] [--force] [--apply]\n"
360
396
  f" {prog} query [italic]<--flag> <arg> <repo_path>[/]\n"
361
397
  f" {prog} load-triples [italic]<triples.json> <repo_path>[/]\n"
362
398
  f" {prog} watch [italic]<repo_path>[/]"
@@ -377,12 +413,48 @@ def main():
377
413
  non_flag_args = [a for a in args if not a.startswith("--")]
378
414
  repo_path = non_flag_args[0] if non_flag_args else "."
379
415
  normalize = "--normalize" in args
416
+ describe = "--describe" in args
380
417
  dump_triples = None
381
418
  if "--dump-triples" in args:
382
419
  idx = args.index("--dump-triples")
383
420
  if idx + 1 < len(args):
384
421
  dump_triples = args[idx + 1]
385
- ingest_code(repo_path, normalize=normalize, dump_triples=dump_triples)
422
+ ingest_code(repo_path, normalize=normalize, dump_triples=dump_triples, describe=describe)
423
+
424
+ elif command == "describe":
425
+ args = sys.argv[2:]
426
+ repo_path = "."
427
+ batch_size = 15
428
+ apply = "--apply" in args
429
+ force = "--force" in args
430
+ i = 0
431
+ while i < len(args):
432
+ a = args[i]
433
+ if a == "--batch-size" and i + 1 < len(args):
434
+ batch_size = int(args[i + 1])
435
+ i += 2
436
+ elif a in ("--apply", "--force"):
437
+ i += 1
438
+ else:
439
+ repo_path = a
440
+ i += 1
441
+
442
+ if apply:
443
+ from ingestion.description_enricher import apply_describe_results
444
+ updated = apply_describe_results(repo_path)
445
+ console.print(f"[bold green]Applied[/] descriptions for {updated} entities.")
446
+ else:
447
+ from ingestion.description_enricher import prepare_describe_batches
448
+ staged = prepare_describe_batches(repo_path, batch_size=batch_size, force=force)
449
+ if staged["num_entities"] == 0:
450
+ console.print("[dim]Nothing to describe.[/]")
451
+ else:
452
+ console.print(
453
+ f"[bold green]Staged[/] {staged['num_entities']} entities in "
454
+ f"{staged['num_batches']} batch(es) at {staged['describe_dir']}.\n"
455
+ f"Read {staged['instructions_path']} and dispatch a sub-agent per "
456
+ f"batch, then run `codecompass describe {repo_path} --apply`."
457
+ )
386
458
 
387
459
  elif command == "load-triples":
388
460
  args = sys.argv[2:]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codecompass-mcp"
3
- version = "2.4.0"
3
+ version = "2.5.0"
4
4
  description = "Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,95 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from graph.code_graph_client import get_client
6
+ from ingestion.description_enricher import apply_describe_results, prepare_describe_batches
7
+ from models.code_types import FileNode
8
+
9
+
10
+ def _seed_entity(repo_path, project, entity_id, name="greet"):
11
+ src_dir = repo_path / "src"
12
+ src_dir.mkdir(exist_ok=True)
13
+ (src_dir / "greet.py").write_text('def greet(name):\n return f"Hello {name}"\n')
14
+
15
+ client = get_client(str(repo_path))
16
+ client.merge_project_node(f"{project}:project", project, str(repo_path))
17
+ file_node = FileNode(path="src/greet.py", name="greet.py", extension=".py", depth=1)
18
+ client.merge_file_node(f"{project}:file:src/greet.py", file_node, project)
19
+ client.graph.add_node(
20
+ entity_id,
21
+ type="Entity",
22
+ name=name,
23
+ kind="function:python",
24
+ entity_type="function",
25
+ language="python",
26
+ file="src/greet.py",
27
+ project=project,
28
+ description="old generic description",
29
+ )
30
+ client.save()
31
+ client.close()
32
+
33
+
34
+ def test_prepare_describe_batches_writes_batch_and_instructions(tmp_path):
35
+ project = "demo"
36
+ repo_path = tmp_path / project
37
+ repo_path.mkdir()
38
+ entity_id = f"{project}:greet"
39
+ _seed_entity(repo_path, project, entity_id)
40
+
41
+ staged = prepare_describe_batches(str(repo_path), batch_size=15)
42
+
43
+ assert staged["num_entities"] == 1
44
+ assert staged["num_batches"] == 1
45
+ describe_dir = repo_path / ".codecompass" / "describe"
46
+ assert (describe_dir / "batch_0000.json").exists()
47
+ assert (describe_dir / "INSTRUCTIONS.md").exists()
48
+
49
+ batch = json.loads((describe_dir / "batch_0000.json").read_text())
50
+ assert batch[0]["id"] == entity_id
51
+ assert batch[0]["name"] == "greet"
52
+
53
+
54
+ def test_prepare_describe_batches_noop_when_nothing_to_describe(tmp_path):
55
+ project = "empty"
56
+ repo_path = tmp_path / project
57
+ repo_path.mkdir()
58
+ client = get_client(str(repo_path))
59
+ client.merge_project_node(f"{project}:project", project, str(repo_path))
60
+ client.save()
61
+ client.close()
62
+
63
+ staged = prepare_describe_batches(str(repo_path))
64
+
65
+ assert staged["num_entities"] == 0
66
+ assert not (repo_path / ".codecompass" / "describe").exists()
67
+
68
+
69
+ def test_apply_describe_results_updates_nodes_and_cleans_up(tmp_path):
70
+ project = "demo"
71
+ repo_path = tmp_path / project
72
+ repo_path.mkdir()
73
+ entity_id = f"{project}:greet"
74
+ _seed_entity(repo_path, project, entity_id)
75
+
76
+ staged = prepare_describe_batches(str(repo_path), batch_size=15)
77
+ describe_dir = repo_path / ".codecompass" / "describe"
78
+ result_path = describe_dir / "batch_0000.result.json"
79
+ result_path.write_text(json.dumps({entity_id: "Greets a user by name."}))
80
+
81
+ updated = apply_describe_results(str(repo_path))
82
+ assert updated == 1
83
+
84
+ client = get_client(str(repo_path))
85
+ assert client.graph.nodes[entity_id]["description"] == "Greets a user by name."
86
+ client.close()
87
+
88
+ assert not describe_dir.exists()
89
+
90
+
91
+ def test_apply_describe_results_without_staged_batches_raises(tmp_path):
92
+ repo_path = tmp_path / "nothing-staged"
93
+ repo_path.mkdir()
94
+ with pytest.raises(FileNotFoundError, match="No staged description batches"):
95
+ apply_describe_results(str(repo_path))
File without changes