codecompass-mcp 5.3.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.
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/PKG-INFO +31 -22
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/README.md +32 -23
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/PKG-INFO +31 -22
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/SOURCES.txt +4 -3
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/graph/code_graph_client.py +113 -17
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/graph/vector_store.py +8 -4
- codecompass_mcp-6.0.0/ingestion/agent_writes.py +167 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/main.py +171 -200
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/mcp_server.py +10 -30
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/pi_setup.py +29 -9
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/pyproject.toml +1 -1
- codecompass_mcp-5.3.0/tests/test_enricher.py → codecompass_mcp-6.0.0/tests/test_agent_writes.py +38 -55
- codecompass_mcp-6.0.0/tests/test_descriptions.py +175 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_mcp_server.py +0 -1
- codecompass_mcp-6.0.0/tests/test_pi_setup.py +42 -0
- codecompass_mcp-5.3.0/ingestion/enricher.py +0 -386
- codecompass_mcp-5.3.0/tests/test_enrich_preserve.py +0 -137
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/LICENSE +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/requires.txt +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/config.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/graph/__init__.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/graph/code_queries.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/__init__.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/code_normalizer.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/code_parser.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/file_watcher.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/hierarchy_builder.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/ingestion/source_context.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/models/__init__.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/models/code_types.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/setup.cfg +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_accuracy_fixes.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_block_hook.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_code_parser.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_ingest_progress.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_init.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_lit_css_parser.py +0 -0
- {codecompass_mcp-5.3.0 → codecompass_mcp-6.0.0}/tests/test_php_parser.py +0 -0
- {codecompass_mcp-5.3.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:
|
|
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
|
-
###
|
|
159
|
+
### Agent-written knowledge
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
|
|
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
|
-
| `
|
|
195
|
-
| `
|
|
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
|
-
▼
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
262
|
-
├── .codecompass/
|
|
263
|
-
|
|
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.
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
###
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
130
|
-
|
|
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
|
-
| `
|
|
156
|
-
| `
|
|
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
|
-
▼
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
223
|
-
├── .codecompass/
|
|
224
|
-
|
|
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.
|
|
233
|
-
|
|
234
|
-
|
|
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:
|
|
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
|
-
###
|
|
159
|
+
### Agent-written knowledge
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
|
|
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
|
-
| `
|
|
195
|
-
| `
|
|
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
|
-
▼
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
262
|
-
├── .codecompass/
|
|
263
|
-
|
|
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.
|
|
272
|
-
|
|
273
|
-
|
|
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/
|
|
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
|
|
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
|
|
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,7 +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"),
|
|
490
|
-
"description":
|
|
579
|
+
"description": self.describe(pred),
|
|
491
580
|
"receiver": receiver,
|
|
492
581
|
"receiver_type": receiver_type,
|
|
493
582
|
"resolved": True,
|
|
@@ -521,7 +610,7 @@ class LocalGraphClient:
|
|
|
521
610
|
"caller_type": node.get("entity_type"),
|
|
522
611
|
"caller_file": ce.get("source_file") or node.get("file"),
|
|
523
612
|
"line": ce.get("line"),
|
|
524
|
-
"description":
|
|
613
|
+
"description": self.describe(pred),
|
|
525
614
|
"receiver": next((e.get("receiver") for e in rel if e.get("receiver")), None),
|
|
526
615
|
"receiver_type": None,
|
|
527
616
|
"resolved": False,
|
|
@@ -552,8 +641,11 @@ class LocalGraphClient:
|
|
|
552
641
|
for _n, a in self.graph.nodes(data=True):
|
|
553
642
|
if a.get("type") != "Entity" or a.get("project") != project:
|
|
554
643
|
continue
|
|
644
|
+
description = self.describe(_n)
|
|
555
645
|
for f in fields:
|
|
556
|
-
|
|
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 "")
|
|
557
649
|
m = rx.search(val) if val else None
|
|
558
650
|
if m:
|
|
559
651
|
matches.append({
|
|
@@ -561,7 +653,7 @@ class LocalGraphClient:
|
|
|
561
653
|
"kind": a.get("kind"),
|
|
562
654
|
"file": a.get("file"),
|
|
563
655
|
"line": a.get("line"),
|
|
564
|
-
"description":
|
|
656
|
+
"description": description,
|
|
565
657
|
"matched_field": f,
|
|
566
658
|
"match": m.group(0),
|
|
567
659
|
})
|
|
@@ -598,7 +690,7 @@ class LocalGraphClient:
|
|
|
598
690
|
"dependency": node.get("name"),
|
|
599
691
|
"dep_type": node.get("entity_type"),
|
|
600
692
|
"line": node.get("line"),
|
|
601
|
-
"description":
|
|
693
|
+
"description": self.describe(succ),
|
|
602
694
|
"depth": depth + 1
|
|
603
695
|
})
|
|
604
696
|
|
|
@@ -620,7 +712,7 @@ class LocalGraphClient:
|
|
|
620
712
|
"selector": node.get("name"),
|
|
621
713
|
"source_file": node.get("file"),
|
|
622
714
|
"line": edge.get("line"),
|
|
623
|
-
"description":
|
|
715
|
+
"description": self.describe(pred),
|
|
624
716
|
})
|
|
625
717
|
|
|
626
718
|
return sorted(results, key=lambda x: x["selector"])
|
|
@@ -646,7 +738,7 @@ class LocalGraphClient:
|
|
|
646
738
|
"id": start_id,
|
|
647
739
|
"name": start_attr.get("name", start_name),
|
|
648
740
|
"kind": start_attr.get("kind", ""),
|
|
649
|
-
"description":
|
|
741
|
+
"description": self.describe(start_id),
|
|
650
742
|
"file": start_attr.get("file", ""),
|
|
651
743
|
"line": start_attr.get("line"),
|
|
652
744
|
"depth": 0,
|
|
@@ -680,7 +772,7 @@ class LocalGraphClient:
|
|
|
680
772
|
"id": succ,
|
|
681
773
|
"name": succ_attr.get("name", ""),
|
|
682
774
|
"kind": succ_attr.get("kind", ""),
|
|
683
|
-
"description":
|
|
775
|
+
"description": self.describe(succ),
|
|
684
776
|
"file": succ_attr.get("file", ""),
|
|
685
777
|
"line": succ_attr.get("line"),
|
|
686
778
|
"depth": depth + 1,
|
|
@@ -717,7 +809,7 @@ class LocalGraphClient:
|
|
|
717
809
|
"callee_type": node.get("entity_type"),
|
|
718
810
|
"callee_file": node.get("file"),
|
|
719
811
|
"line": node.get("line"),
|
|
720
|
-
"description":
|
|
812
|
+
"description": self.describe(succ),
|
|
721
813
|
"depth": depth + 1
|
|
722
814
|
})
|
|
723
815
|
visited.add(succ)
|
|
@@ -790,7 +882,7 @@ class LocalGraphClient:
|
|
|
790
882
|
"entity_type": attr.get("entity_type", ""),
|
|
791
883
|
"file": attr.get("file", ""),
|
|
792
884
|
"line": attr.get("line"),
|
|
793
|
-
"description":
|
|
885
|
+
"description": self.describe(node_id),
|
|
794
886
|
}
|
|
795
887
|
# Exported symbols and name-heuristic entry points are public API:
|
|
796
888
|
# having no in-repo caller is expected, not evidence of dead code.
|
|
@@ -926,7 +1018,11 @@ class LocalGraphClient:
|
|
|
926
1018
|
importers.append(from_file)
|
|
927
1019
|
return sorted(set(importers))
|
|
928
1020
|
|
|
929
|
-
def get_client(project_path: str) -> LocalGraphClient:
|
|
930
|
-
"""Return a LocalGraphClient for the project at project_path.
|
|
931
|
-
|
|
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)
|
|
932
1028
|
return LocalGraphClient(storage_path)
|
|
@@ -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
|
-
|
|
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":
|
|
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
|