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.
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/PKG-INFO +31 -22
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/README.md +32 -23
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/PKG-INFO +31 -22
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/SOURCES.txt +4 -3
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/code_graph_client.py +114 -11
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/code_queries.py +2 -1
- {codecompass_mcp-5.2.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.2.0 → codecompass_mcp-6.0.0}/main.py +171 -200
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/mcp_server.py +10 -30
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/pi_setup.py +29 -9
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/pyproject.toml +1 -1
- codecompass_mcp-5.2.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.2.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.2.0/ingestion/enricher.py +0 -386
- codecompass_mcp-5.2.0/tests/test_enrich_preserve.py +0 -137
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/LICENSE +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/requires.txt +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/config.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/graph/__init__.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/__init__.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/code_normalizer.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/code_parser.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/file_watcher.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/hierarchy_builder.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/ingestion/source_context.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/models/__init__.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/models/code_types.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/setup.cfg +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_accuracy_fixes.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_block_hook.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_code_parser.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_ingest_progress.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_init.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_lit_css_parser.py +0 -0
- {codecompass_mcp-5.2.0 → codecompass_mcp-6.0.0}/tests/test_php_parser.py +0 -0
- {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:
|
|
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,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
|
-
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|