codecompass-mcp 5.2.0__tar.gz → 6.0.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 (42) hide show
  1. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/PKG-INFO +31 -22
  2. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/README.md +32 -23
  3. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/PKG-INFO +31 -22
  4. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/SOURCES.txt +4 -3
  5. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/code_graph_client.py +114 -11
  6. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/code_queries.py +2 -1
  7. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/vector_store.py +8 -4
  8. codecompass_mcp-6.0.0/ingestion/agent_writes.py +167 -0
  9. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/main.py +171 -200
  10. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/mcp_server.py +10 -30
  11. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/pi_setup.py +29 -9
  12. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/pyproject.toml +1 -1
  13. codecompass_mcp-5.2.0/tests/test_enricher.py → codecompass_mcp-6.0.0/tests/test_agent_writes.py +38 -55
  14. codecompass_mcp-6.0.0/tests/test_descriptions.py +175 -0
  15. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_mcp_server.py +0 -1
  16. codecompass_mcp-6.0.0/tests/test_pi_setup.py +42 -0
  17. codecompass_mcp-5.2.0/ingestion/enricher.py +0 -386
  18. codecompass_mcp-5.2.0/tests/test_enrich_preserve.py +0 -137
  19. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/LICENSE +0 -0
  20. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  21. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  22. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  23. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
  24. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/config.py +0 -0
  25. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/__init__.py +0 -0
  26. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/__init__.py +0 -0
  27. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/code_normalizer.py +0 -0
  28. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/code_parser.py +0 -0
  29. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/file_watcher.py +0 -0
  30. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/hierarchy_builder.py +0 -0
  31. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/source_context.py +0 -0
  32. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/models/__init__.py +0 -0
  33. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/models/code_types.py +0 -0
  34. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/setup.cfg +0 -0
  35. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_accuracy_fixes.py +0 -0
  36. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_block_hook.py +0 -0
  37. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_code_parser.py +0 -0
  38. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_ingest_progress.py +0 -0
  39. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_init.py +0 -0
  40. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_lit_css_parser.py +0 -0
  41. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_php_parser.py +0 -0
  42. {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_vector_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 5.2.0
3
+ Version: 6.0.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
@@ -156,17 +156,21 @@ The index follows the graph's lifecycle: wiped and rebuilt at the end of every
156
156
  `ingest`, so parser nodes *and* agent-recorded ones are searchable. Without the
157
157
  extra, ingest simply skips the vector step.
158
158
 
159
- ### Enrichment (agent-in-the-loop)
159
+ ### Agent-written knowledge
160
160
 
161
- Via the MCP tools: `enrich` stages entities for an agent swarm (one-line
162
- descriptions + missing call edges), `enrich(apply=True)` merges the swarm's
163
- results, and `add_entity` / `add_call` record single parser-missed entries.
161
+ The parser extracts structure. Everything it *can't* see dynamic dispatch,
162
+ callbacks, runtime registration and everything it can't know — what an entity
163
+ is FOR comes from the agent reading the code and writing it back with
164
+ `add_entity` / `add_call`. There is no bulk enrichment pass and no LLM
165
+ backfill: the graph improves as it gets used, or not at all.
164
166
 
165
- `enrich` is a bulk, user-triggered pass. `add_entity`/`add_call` are the
166
- opportunistic version: as an agent reads code and spots something the parser
167
- missed, it records it immediately. Everything agent-written is marked
168
- `agent_inferred` and **preserved across re-ingests** — the graph gets better
169
- with use. Ambiguous call targets are skipped, never guessed.
167
+ Descriptions live in `.codecompass/description.jsonl`, one
168
+ `{"node": "<id>", "description": "..."}` per line, joined onto every query
169
+ result by node id. Keeping them out of `graph.json` means they survive the
170
+ wholesale rebuild each `ingest` performs (and a deleted `graph.json`), while
171
+ entries whose node vanishes from the source are pruned on the next ingest.
172
+ Agent-written nodes and edges are marked `agent_inferred`; ambiguous call
173
+ targets are skipped, never guessed.
170
174
 
171
175
  ### Flow: `flow` vs `flow-summary`
172
176
 
@@ -191,9 +195,8 @@ with use. Ambiguous call targets are skipped, never guessed.
191
195
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
192
196
  | `styles(element)` | CSS selectors that style an element |
193
197
  | `tree()` | Full project hierarchy |
194
- | `enrich(apply, batch_size)` | Stage/merge agent-written descriptions + missing call edges |
195
- | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity (`agent_inferred`) |
196
- | `add_call(caller, callee, line)` | Record a parser-missed CALLS edge (`agent_inferred`) |
198
+ | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity, or describe an existing one (`agent_inferred`) |
199
+ | `add_call(caller, callee, line, relation)` | Record a parser-missed CALLS/IMPORTS/INHERITS edge (`agent_inferred`) |
197
200
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
198
201
 
199
202
  ---
@@ -243,9 +246,11 @@ Source files
243
246
  ▼ code_queries traversal helpers: grep / impact / blast_radius /
244
247
  deps / flow / dead_code / tree
245
248
  ▼ mcp_server FastMCP server — the only query surface for agents
246
- enricher agent-in-the-loop: enrich batches (descriptions +
247
- missing calls) and opportunistic add_entity/add_call
248
- writes — agent_inferred, preserved across re-ingest
249
+ agent_writes add_entity / add_call: what the parser can't see,
250
+ written back by the agent — agent_inferred, preserved
251
+ across re-ingest
252
+ ▼ description.jsonl node id → description, joined onto every result;
253
+ outlives the graph rebuild, pruned when a node dies
249
254
  ▼ vector_store optional: entity embeddings in vectors.lance (LanceDB
250
255
  + fastembed), wiped & rebuilt on every ingest
251
256
  ```
@@ -258,9 +263,13 @@ Inside each indexed project:
258
263
 
259
264
  ```
260
265
  your-project/
261
- ├── .codecompass/graph.json the code knowledge graph (auto-generated)
262
- ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
263
- └── AGENTS.md discovery guide for agents (auto-updated)
266
+ ├── .codecompass/graph.json the code knowledge graph (auto-generated)
267
+ ├── .codecompass/description.jsonl agent-written descriptions, keyed by node id
268
+ ├── .codecompass/overview.md what this repo is (agent-maintained)
269
+ ├── .codecompass/memory.md how it's built (agent-maintained)
270
+ ├── .codecompass/learnings.md what to watch out for (agent-maintained)
271
+ ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
272
+ └── AGENTS.md discovery guide for agents (auto-updated)
264
273
  ```
265
274
 
266
275
  ---
@@ -268,9 +277,9 @@ your-project/
268
277
  ## Limitations
269
278
 
270
279
  - **Structure first, semantics layered on** — the parser knows what calls what,
271
- not what it means. Two optional layers close that gap: `enrich` (agent-written
272
- descriptions and missed edges, marked `agent_inferred`) and `search`
273
- (semantic vector search over those descriptions and names).
280
+ not what it means. Agents close that gap as they work (`add_entity` /
281
+ `add_call`, marked `agent_inferred`), and `search` makes the result
282
+ semantically findable. An unexplored corner of the repo stays undescribed.
274
283
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
275
284
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
276
285
  `dead_code` results are always candidates to verify.
@@ -117,17 +117,21 @@ The index follows the graph's lifecycle: wiped and rebuilt at the end of every
117
117
  `ingest`, so parser nodes *and* agent-recorded ones are searchable. Without the
118
118
  extra, ingest simply skips the vector step.
119
119
 
120
- ### Enrichment (agent-in-the-loop)
121
-
122
- Via the MCP tools: `enrich` stages entities for an agent swarm (one-line
123
- descriptions + missing call edges), `enrich(apply=True)` merges the swarm's
124
- results, and `add_entity` / `add_call` record single parser-missed entries.
125
-
126
- `enrich` is a bulk, user-triggered pass. `add_entity`/`add_call` are the
127
- opportunistic version: as an agent reads code and spots something the parser
128
- missed, it records it immediately. Everything agent-written is marked
129
- `agent_inferred` and **preserved across re-ingests** the graph gets better
130
- with use. Ambiguous call targets are skipped, never guessed.
120
+ ### Agent-written knowledge
121
+
122
+ The parser extracts structure. Everything it *can't* see dynamic dispatch,
123
+ callbacks, runtime registration and everything it can't know — what an entity
124
+ is FOR comes from the agent reading the code and writing it back with
125
+ `add_entity` / `add_call`. There is no bulk enrichment pass and no LLM
126
+ backfill: the graph improves as it gets used, or not at all.
127
+
128
+ Descriptions live in `.codecompass/description.jsonl`, one
129
+ `{"node": "<id>", "description": "..."}` per line, joined onto every query
130
+ result by node id. Keeping them out of `graph.json` means they survive the
131
+ wholesale rebuild each `ingest` performs (and a deleted `graph.json`), while
132
+ entries whose node vanishes from the source are pruned on the next ingest.
133
+ Agent-written nodes and edges are marked `agent_inferred`; ambiguous call
134
+ targets are skipped, never guessed.
131
135
 
132
136
  ### Flow: `flow` vs `flow-summary`
133
137
 
@@ -152,9 +156,8 @@ with use. Ambiguous call targets are skipped, never guessed.
152
156
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
153
157
  | `styles(element)` | CSS selectors that style an element |
154
158
  | `tree()` | Full project hierarchy |
155
- | `enrich(apply, batch_size)` | Stage/merge agent-written descriptions + missing call edges |
156
- | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity (`agent_inferred`) |
157
- | `add_call(caller, callee, line)` | Record a parser-missed CALLS edge (`agent_inferred`) |
159
+ | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity, or describe an existing one (`agent_inferred`) |
160
+ | `add_call(caller, callee, line, relation)` | Record a parser-missed CALLS/IMPORTS/INHERITS edge (`agent_inferred`) |
158
161
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
159
162
 
160
163
  ---
@@ -204,9 +207,11 @@ Source files
204
207
  ▼ code_queries traversal helpers: grep / impact / blast_radius /
205
208
  deps / flow / dead_code / tree
206
209
  ▼ mcp_server FastMCP server — the only query surface for agents
207
- enricher agent-in-the-loop: enrich batches (descriptions +
208
- missing calls) and opportunistic add_entity/add_call
209
- writes — agent_inferred, preserved across re-ingest
210
+ agent_writes add_entity / add_call: what the parser can't see,
211
+ written back by the agent — agent_inferred, preserved
212
+ across re-ingest
213
+ ▼ description.jsonl node id → description, joined onto every result;
214
+ outlives the graph rebuild, pruned when a node dies
210
215
  ▼ vector_store optional: entity embeddings in vectors.lance (LanceDB
211
216
  + fastembed), wiped & rebuilt on every ingest
212
217
  ```
@@ -219,9 +224,13 @@ Inside each indexed project:
219
224
 
220
225
  ```
221
226
  your-project/
222
- ├── .codecompass/graph.json the code knowledge graph (auto-generated)
223
- ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
224
- └── AGENTS.md discovery guide for agents (auto-updated)
227
+ ├── .codecompass/graph.json the code knowledge graph (auto-generated)
228
+ ├── .codecompass/description.jsonl agent-written descriptions, keyed by node id
229
+ ├── .codecompass/overview.md what this repo is (agent-maintained)
230
+ ├── .codecompass/memory.md how it's built (agent-maintained)
231
+ ├── .codecompass/learnings.md what to watch out for (agent-maintained)
232
+ ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
233
+ └── AGENTS.md discovery guide for agents (auto-updated)
225
234
  ```
226
235
 
227
236
  ---
@@ -229,9 +238,9 @@ your-project/
229
238
  ## Limitations
230
239
 
231
240
  - **Structure first, semantics layered on** — the parser knows what calls what,
232
- not what it means. Two optional layers close that gap: `enrich` (agent-written
233
- descriptions and missed edges, marked `agent_inferred`) and `search`
234
- (semantic vector search over those descriptions and names).
241
+ not what it means. Agents close that gap as they work (`add_entity` /
242
+ `add_call`, marked `agent_inferred`), and `search` makes the result
243
+ semantically findable. An unexplored corner of the repo stays undescribed.
235
244
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
236
245
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
237
246
  `dead_code` results are always candidates to verify.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 5.2.0
3
+ Version: 6.0.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
@@ -156,17 +156,21 @@ The index follows the graph's lifecycle: wiped and rebuilt at the end of every
156
156
  `ingest`, so parser nodes *and* agent-recorded ones are searchable. Without the
157
157
  extra, ingest simply skips the vector step.
158
158
 
159
- ### Enrichment (agent-in-the-loop)
159
+ ### Agent-written knowledge
160
160
 
161
- Via the MCP tools: `enrich` stages entities for an agent swarm (one-line
162
- descriptions + missing call edges), `enrich(apply=True)` merges the swarm's
163
- results, and `add_entity` / `add_call` record single parser-missed entries.
161
+ The parser extracts structure. Everything it *can't* see dynamic dispatch,
162
+ callbacks, runtime registration and everything it can't know — what an entity
163
+ is FOR comes from the agent reading the code and writing it back with
164
+ `add_entity` / `add_call`. There is no bulk enrichment pass and no LLM
165
+ backfill: the graph improves as it gets used, or not at all.
164
166
 
165
- `enrich` is a bulk, user-triggered pass. `add_entity`/`add_call` are the
166
- opportunistic version: as an agent reads code and spots something the parser
167
- missed, it records it immediately. Everything agent-written is marked
168
- `agent_inferred` and **preserved across re-ingests** — the graph gets better
169
- with use. Ambiguous call targets are skipped, never guessed.
167
+ Descriptions live in `.codecompass/description.jsonl`, one
168
+ `{"node": "<id>", "description": "..."}` per line, joined onto every query
169
+ result by node id. Keeping them out of `graph.json` means they survive the
170
+ wholesale rebuild each `ingest` performs (and a deleted `graph.json`), while
171
+ entries whose node vanishes from the source are pruned on the next ingest.
172
+ Agent-written nodes and edges are marked `agent_inferred`; ambiguous call
173
+ targets are skipped, never guessed.
170
174
 
171
175
  ### Flow: `flow` vs `flow-summary`
172
176
 
@@ -191,9 +195,8 @@ with use. Ambiguous call targets are skipped, never guessed.
191
195
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
192
196
  | `styles(element)` | CSS selectors that style an element |
193
197
  | `tree()` | Full project hierarchy |
194
- | `enrich(apply, batch_size)` | Stage/merge agent-written descriptions + missing call edges |
195
- | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity (`agent_inferred`) |
196
- | `add_call(caller, callee, line)` | Record a parser-missed CALLS edge (`agent_inferred`) |
198
+ | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity, or describe an existing one (`agent_inferred`) |
199
+ | `add_call(caller, callee, line, relation)` | Record a parser-missed CALLS/IMPORTS/INHERITS edge (`agent_inferred`) |
197
200
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
198
201
 
199
202
  ---
@@ -243,9 +246,11 @@ Source files
243
246
  ▼ code_queries traversal helpers: grep / impact / blast_radius /
244
247
  deps / flow / dead_code / tree
245
248
  ▼ mcp_server FastMCP server — the only query surface for agents
246
- enricher agent-in-the-loop: enrich batches (descriptions +
247
- missing calls) and opportunistic add_entity/add_call
248
- writes — agent_inferred, preserved across re-ingest
249
+ agent_writes add_entity / add_call: what the parser can't see,
250
+ written back by the agent — agent_inferred, preserved
251
+ across re-ingest
252
+ ▼ description.jsonl node id → description, joined onto every result;
253
+ outlives the graph rebuild, pruned when a node dies
249
254
  ▼ vector_store optional: entity embeddings in vectors.lance (LanceDB
250
255
  + fastembed), wiped & rebuilt on every ingest
251
256
  ```
@@ -258,9 +263,13 @@ Inside each indexed project:
258
263
 
259
264
  ```
260
265
  your-project/
261
- ├── .codecompass/graph.json the code knowledge graph (auto-generated)
262
- ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
263
- └── AGENTS.md discovery guide for agents (auto-updated)
266
+ ├── .codecompass/graph.json the code knowledge graph (auto-generated)
267
+ ├── .codecompass/description.jsonl agent-written descriptions, keyed by node id
268
+ ├── .codecompass/overview.md what this repo is (agent-maintained)
269
+ ├── .codecompass/memory.md how it's built (agent-maintained)
270
+ ├── .codecompass/learnings.md what to watch out for (agent-maintained)
271
+ ├── .codecompass/vectors.lance/ semantic search index (optional, rebuilt on ingest)
272
+ └── AGENTS.md discovery guide for agents (auto-updated)
264
273
  ```
265
274
 
266
275
  ---
@@ -268,9 +277,9 @@ your-project/
268
277
  ## Limitations
269
278
 
270
279
  - **Structure first, semantics layered on** — the parser knows what calls what,
271
- not what it means. Two optional layers close that gap: `enrich` (agent-written
272
- descriptions and missed edges, marked `agent_inferred`) and `search`
273
- (semantic vector search over those descriptions and names).
280
+ not what it means. Agents close that gap as they work (`add_entity` /
281
+ `add_call`, marked `agent_inferred`), and `search` makes the result
282
+ semantically findable. An unexplored corner of the repo stays undescribed.
274
283
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
275
284
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
276
285
  `dead_code` results are always candidates to verify.
@@ -16,22 +16,23 @@ graph/code_graph_client.py
16
16
  graph/code_queries.py
17
17
  graph/vector_store.py
18
18
  ingestion/__init__.py
19
+ ingestion/agent_writes.py
19
20
  ingestion/code_normalizer.py
20
21
  ingestion/code_parser.py
21
- ingestion/enricher.py
22
22
  ingestion/file_watcher.py
23
23
  ingestion/hierarchy_builder.py
24
24
  ingestion/source_context.py
25
25
  models/__init__.py
26
26
  models/code_types.py
27
27
  tests/test_accuracy_fixes.py
28
+ tests/test_agent_writes.py
28
29
  tests/test_block_hook.py
29
30
  tests/test_code_parser.py
30
- tests/test_enrich_preserve.py
31
- tests/test_enricher.py
31
+ tests/test_descriptions.py
32
32
  tests/test_ingest_progress.py
33
33
  tests/test_init.py
34
34
  tests/test_lit_css_parser.py
35
35
  tests/test_mcp_server.py
36
36
  tests/test_php_parser.py
37
+ tests/test_pi_setup.py
37
38
  tests/test_vector_store.py
@@ -37,6 +37,52 @@ def _now() -> str:
37
37
  return datetime.now(timezone.utc).isoformat()
38
38
 
39
39
 
40
+ # Descriptions do NOT live on the graph nodes. graph.json is parser output,
41
+ # rebuilt from scratch on every ingest; a description is the one thing in the
42
+ # graph no parser can regenerate, so it lives in its own sidecar keyed by node
43
+ # id and is joined back on read. One {"node", "description"} object per line —
44
+ # line-diffable, greppable, and survives a deleted or corrupt graph.json.
45
+ DESCRIPTIONS_FILE = os.path.join(".codecompass", "description.jsonl")
46
+
47
+
48
+ def descriptions_path(repo_path: str) -> str:
49
+ return os.path.join(repo_path, DESCRIPTIONS_FILE)
50
+
51
+
52
+ def load_descriptions(repo_path: str) -> dict[str, str]:
53
+ """Read the sidecar as {node_id: description}.
54
+
55
+ A missing file or an unparseable line yields nothing rather than raising —
56
+ a query must still answer when the sidecar is absent.
57
+ """
58
+ out: dict[str, str] = {}
59
+ try:
60
+ with open(descriptions_path(repo_path), encoding="utf-8") as f:
61
+ for line in f:
62
+ if not line.strip():
63
+ continue
64
+ try:
65
+ row = json.loads(line)
66
+ except json.JSONDecodeError:
67
+ continue
68
+ if row.get("node") and row.get("description"):
69
+ out[row["node"]] = row["description"]
70
+ except OSError:
71
+ pass
72
+ return out
73
+
74
+
75
+ def save_descriptions(repo_path: str, descriptions: dict[str, str]) -> int:
76
+ """Write the sidecar, one JSON object per line, sorted by node id."""
77
+ path = descriptions_path(repo_path)
78
+ os.makedirs(os.path.dirname(path), exist_ok=True)
79
+ with open(path, "w", encoding="utf-8") as f:
80
+ for nid in sorted(descriptions):
81
+ if descriptions[nid]:
82
+ f.write(json.dumps({"node": nid, "description": descriptions[nid]}) + "\n")
83
+ return len(descriptions)
84
+
85
+
40
86
  def _entity_id(project: str, name: str, file: str | None, owner: str | None = None) -> str:
41
87
  """Node id for an entity — file- and class-qualified so same-named entities
42
88
  stay distinct across files AND across classes in the same file. Case is
@@ -148,9 +194,38 @@ class LocalGraphClient:
148
194
 
149
195
  def __init__(self, storage_path: str) -> None:
150
196
  self.storage_path = storage_path
197
+ # .../<repo>/.codecompass/graph.json -> <repo>
198
+ self.repo_path = os.path.dirname(os.path.dirname(storage_path))
151
199
  self.graph = nx.MultiDiGraph()
200
+ self.descriptions = load_descriptions(self.repo_path)
201
+ # Ingest builds a throwaway graph and flips this on only for the final
202
+ # write, so a half-built graph never rewrites the description sidecar.
203
+ self.sync_descriptions = True
152
204
  self.load()
153
205
 
206
+ def describe(self, node_id: str) -> str:
207
+ """Description for a node, joined from the sidecar. '' when undescribed."""
208
+ return self.descriptions.get(node_id, "")
209
+
210
+ def set_description(self, node_id: str, text: str) -> None:
211
+ """Record a description for a node (empty text clears it)."""
212
+ text = (text or "").strip()
213
+ if text:
214
+ self.descriptions[node_id] = text
215
+ else:
216
+ self.descriptions.pop(node_id, None)
217
+
218
+ def prune_descriptions(self) -> int:
219
+ """Drop sidecar entries whose node no longer exists in the graph.
220
+
221
+ Called after an ingest rebuild: a renamed or deleted symbol takes its
222
+ description with it instead of lingering as a ghost forever.
223
+ """
224
+ stale = [nid for nid in self.descriptions if nid not in self.graph]
225
+ for nid in stale:
226
+ del self.descriptions[nid]
227
+ return len(stale)
228
+
154
229
  def load(self) -> None:
155
230
  """Load graph from JSON file."""
156
231
  if os.path.exists(self.storage_path):
@@ -161,13 +236,29 @@ class LocalGraphClient:
161
236
  except (json.JSONDecodeError, Exception) as e:
162
237
  print(f"Warning: Could not load graph from {self.storage_path} ({e}). Starting fresh.")
163
238
  self.graph = nx.MultiDiGraph()
239
+ self._adopt_legacy_descriptions()
240
+
241
+ def _adopt_legacy_descriptions(self) -> None:
242
+ """Move descriptions off nodes written by pre-sidecar versions.
243
+
244
+ Older graphs stored the description as a node attribute and flagged the
245
+ agent-authored ones; only those are worth keeping (the rest were
246
+ generated blurbs like "python function in x.py"). The attributes are
247
+ dropped as they are read, so this runs exactly once per graph.
248
+ """
249
+ for _nid, a in self.graph.nodes(data=True):
250
+ legacy = a.pop("description", None)
251
+ if legacy and a.get("agent_inferred"):
252
+ self.descriptions.setdefault(_nid, legacy)
164
253
 
165
254
  def save(self) -> None:
166
- """Save graph to JSON file."""
255
+ """Save the graph and its description sidecar."""
167
256
  os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
168
257
  data = nx.node_link_data(self.graph, edges="links")
169
258
  with open(self.storage_path, "w") as f:
170
259
  json.dump(data, f, indent=2)
260
+ if self.sync_descriptions:
261
+ save_descriptions(self.repo_path, self.descriptions)
171
262
 
172
263
  # ------------------------------------------------------------------
173
264
  # Structural nodes (hierarchy skeleton)
@@ -232,7 +323,6 @@ class LocalGraphClient:
232
323
  "entity_type": triple.from_type,
233
324
  "language": language,
234
325
  "kind": f"{triple.from_type}:{language}",
235
- "description": f"{language} {triple.from_type} in {triple.source_file}",
236
326
  "project": project,
237
327
  "file": triple.source_file,
238
328
  })
@@ -248,7 +338,7 @@ class LocalGraphClient:
248
338
  if getattr(triple, "owner_class", None):
249
339
  self.graph.nodes[from_id]["owner"] = triple.owner_class
250
340
 
251
- # Upsert to entity — only set language/kind/description if not already known
341
+ # Upsert to entity — only set language/kind if not already known
252
342
  self.graph.add_node(to_id)
253
343
  existing = self.graph.nodes[to_id]
254
344
  existing.setdefault("type", "Entity")
@@ -258,7 +348,6 @@ class LocalGraphClient:
258
348
  if not existing.get("language"):
259
349
  existing["language"] = language
260
350
  existing["kind"] = f"{triple.to_type}:{language}"
261
- existing["description"] = f"{language} {triple.to_type}"
262
351
  # A module's definition is line 1 of its file.
263
352
  if triple.relation_type == "DEFINED_IN" and triple.to_type == "module":
264
353
  existing.setdefault("line", 1)
@@ -487,6 +576,7 @@ class LocalGraphClient:
487
576
  "caller_type": node.get("entity_type"),
488
577
  "caller_file": call_edge.get("source_file") or node.get("file"),
489
578
  "line": call_edge.get("line"),
579
+ "description": self.describe(pred),
490
580
  "receiver": receiver,
491
581
  "receiver_type": receiver_type,
492
582
  "resolved": True,
@@ -520,6 +610,7 @@ class LocalGraphClient:
520
610
  "caller_type": node.get("entity_type"),
521
611
  "caller_file": ce.get("source_file") or node.get("file"),
522
612
  "line": ce.get("line"),
613
+ "description": self.describe(pred),
523
614
  "receiver": next((e.get("receiver") for e in rel if e.get("receiver")), None),
524
615
  "receiver_type": None,
525
616
  "resolved": False,
@@ -550,8 +641,11 @@ class LocalGraphClient:
550
641
  for _n, a in self.graph.nodes(data=True):
551
642
  if a.get("type") != "Entity" or a.get("project") != project:
552
643
  continue
644
+ description = self.describe(_n)
553
645
  for f in fields:
554
- val = str(a.get(f) or "")
646
+ # description is not a node attribute — it is joined from the
647
+ # sidecar, and grep searches it like any other field.
648
+ val = description if f == "description" else str(a.get(f) or "")
555
649
  m = rx.search(val) if val else None
556
650
  if m:
557
651
  matches.append({
@@ -559,6 +653,7 @@ class LocalGraphClient:
559
653
  "kind": a.get("kind"),
560
654
  "file": a.get("file"),
561
655
  "line": a.get("line"),
656
+ "description": description,
562
657
  "matched_field": f,
563
658
  "match": m.group(0),
564
659
  })
@@ -595,6 +690,7 @@ class LocalGraphClient:
595
690
  "dependency": node.get("name"),
596
691
  "dep_type": node.get("entity_type"),
597
692
  "line": node.get("line"),
693
+ "description": self.describe(succ),
598
694
  "depth": depth + 1
599
695
  })
600
696
 
@@ -615,7 +711,8 @@ class LocalGraphClient:
615
711
  results.append({
616
712
  "selector": node.get("name"),
617
713
  "source_file": node.get("file"),
618
- "line": edge.get("line")
714
+ "line": edge.get("line"),
715
+ "description": self.describe(pred),
619
716
  })
620
717
 
621
718
  return sorted(results, key=lambda x: x["selector"])
@@ -641,7 +738,7 @@ class LocalGraphClient:
641
738
  "id": start_id,
642
739
  "name": start_attr.get("name", start_name),
643
740
  "kind": start_attr.get("kind", ""),
644
- "description": start_attr.get("description", ""),
741
+ "description": self.describe(start_id),
645
742
  "file": start_attr.get("file", ""),
646
743
  "line": start_attr.get("line"),
647
744
  "depth": 0,
@@ -675,7 +772,7 @@ class LocalGraphClient:
675
772
  "id": succ,
676
773
  "name": succ_attr.get("name", ""),
677
774
  "kind": succ_attr.get("kind", ""),
678
- "description": succ_attr.get("description", ""),
775
+ "description": self.describe(succ),
679
776
  "file": succ_attr.get("file", ""),
680
777
  "line": succ_attr.get("line"),
681
778
  "depth": depth + 1,
@@ -712,6 +809,7 @@ class LocalGraphClient:
712
809
  "callee_type": node.get("entity_type"),
713
810
  "callee_file": node.get("file"),
714
811
  "line": node.get("line"),
812
+ "description": self.describe(succ),
715
813
  "depth": depth + 1
716
814
  })
717
815
  visited.add(succ)
@@ -784,6 +882,7 @@ class LocalGraphClient:
784
882
  "entity_type": attr.get("entity_type", ""),
785
883
  "file": attr.get("file", ""),
786
884
  "line": attr.get("line"),
885
+ "description": self.describe(node_id),
787
886
  }
788
887
  # Exported symbols and name-heuristic entry points are public API:
789
888
  # having no in-repo caller is expected, not evidence of dead code.
@@ -919,7 +1018,11 @@ class LocalGraphClient:
919
1018
  importers.append(from_file)
920
1019
  return sorted(set(importers))
921
1020
 
922
- def get_client(project_path: str) -> LocalGraphClient:
923
- """Return a LocalGraphClient for the project at project_path."""
924
- storage_path = os.path.join(project_path, ".codecompass", "graph.json")
1021
+ def get_client(project_path: str, filename: str = "graph.json") -> LocalGraphClient:
1022
+ """Return a LocalGraphClient for the project at project_path.
1023
+
1024
+ `filename` exists so ingest can build into `graph.json.copy` and swap it in
1025
+ once the rebuild succeeds, rather than clearing the live graph in place.
1026
+ """
1027
+ storage_path = os.path.join(project_path, ".codecompass", filename)
925
1028
  return LocalGraphClient(storage_path)
@@ -172,7 +172,8 @@ def fetch_flow(start_name: str, repo_path: str, project: str, max_hops: int = DE
172
172
  edges = _order_edges(edges, project, start_name)
173
173
  lean_nodes = [
174
174
  {"id": n["id"], "name": n["name"], "kind": n.get("kind", ""),
175
- "file": n.get("file", ""), "line": n.get("line"), "depth": n["depth"]}
175
+ "file": n.get("file", ""), "line": n.get("line"), "depth": n["depth"],
176
+ "description": n.get("description", "")}
176
177
  for n in sorted(nodes, key=lambda n: n["depth"])
177
178
  ]
178
179
  lean_edges = [
@@ -47,32 +47,36 @@ def _db_path(repo_path: str) -> str:
47
47
  return os.path.join(repo_path, ".codecompass", DB_DIRNAME)
48
48
 
49
49
 
50
- def _entity_text(a: dict) -> str:
50
+ def _entity_text(a: dict, description: str) -> str:
51
51
  """The string that gets embedded for one entity node."""
52
52
  parts = [a.get("kind") or "", a.get("name") or "", a.get("file") or "",
53
- a.get("description") or ""]
53
+ description]
54
54
  return " ".join(p for p in parts if p)
55
55
 
56
56
 
57
57
  def index_entities(repo_path: str) -> int:
58
58
  """Wipe and rebuild the vector index from graph.json. Returns rows indexed."""
59
+ from graph.code_graph_client import load_descriptions
60
+
59
61
  lancedb, _ = _deps()
60
62
  graph_path = os.path.join(repo_path, ".codecompass", "graph.json")
61
63
  with open(graph_path) as f:
62
64
  nodes = json.load(f).get("nodes", [])
65
+ descriptions = load_descriptions(repo_path)
63
66
 
64
67
  rows = []
65
68
  for a in nodes:
66
69
  if a.get("type") != "Entity":
67
70
  continue
71
+ description = descriptions.get(a.get("id") or "", "")
68
72
  rows.append({
69
73
  "id": a.get("id") or "",
70
74
  "name": a.get("name") or "",
71
75
  "kind": a.get("kind") or "",
72
76
  "file": a.get("file") or "",
73
77
  "line": a.get("line") or 0,
74
- "description": a.get("description") or "",
75
- "text": _entity_text(a),
78
+ "description": description,
79
+ "text": _entity_text(a, description),
76
80
  })
77
81
  if not rows:
78
82
  return 0