codecompass-mcp 3.2.1__tar.gz → 4.1.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 (38) hide show
  1. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/PKG-INFO +36 -16
  2. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/README.md +35 -15
  3. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/PKG-INFO +36 -16
  4. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/SOURCES.txt +4 -2
  5. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/top_level.txt +1 -0
  6. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/graph/code_graph_client.py +21 -84
  7. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/graph/code_query_cli.py +10 -32
  8. codecompass_mcp-4.1.0/ingestion/enricher.py +351 -0
  9. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/main.py +276 -67
  10. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/mcp_server.py +53 -31
  11. codecompass_mcp-4.1.0/pi_setup.py +176 -0
  12. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/pyproject.toml +2 -2
  13. codecompass_mcp-4.1.0/tests/test_block_hook.py +71 -0
  14. codecompass_mcp-4.1.0/tests/test_enricher.py +150 -0
  15. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_mcp_server.py +3 -0
  16. codecompass_mcp-3.2.1/ingestion/description_enricher.py +0 -189
  17. codecompass_mcp-3.2.1/tests/test_description_enricher.py +0 -95
  18. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/LICENSE +0 -0
  19. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  20. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  21. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  22. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/config.py +0 -0
  23. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/graph/__init__.py +0 -0
  24. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/__init__.py +0 -0
  25. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/code_normalizer.py +0 -0
  26. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/code_parser.py +0 -0
  27. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/file_watcher.py +0 -0
  28. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/hierarchy_builder.py +0 -0
  29. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/ingestion/source_context.py +0 -0
  30. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/models/__init__.py +0 -0
  31. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/models/code_types.py +0 -0
  32. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/setup.cfg +0 -0
  33. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_accuracy_fixes.py +0 -0
  34. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_batch_impact.py +0 -0
  35. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_blast_radius.py +0 -0
  36. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_code_parser.py +0 -0
  37. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_lit_css_parser.py +0 -0
  38. {codecompass_mcp-3.2.1 → codecompass_mcp-4.1.0}/tests/test_php_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 3.2.1
3
+ Version: 4.1.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
@@ -67,10 +67,8 @@ The graph turns navigation into a cheap, deterministic loop:
67
67
 
68
68
  | You have… | Use |
69
69
  |---|---|
70
- | a feature request that names a concept ("session timeout") | `grep` the concept |
71
- | a regex / name pattern | `grep` |
72
- | keywords | `search` |
73
- | a vague, nameless need | `map` (compact index to reason over) |
70
+ | a concept, name, or pattern | `grep` (regex over graph entities) |
71
+ | the full layout | `tree` |
74
72
 
75
73
  2. **Trace** — a relationship around a known symbol/file:
76
74
 
@@ -140,8 +138,6 @@ The server speaks stdio MCP and defaults to the working directory.
140
138
  ```bash
141
139
  # discover
142
140
  codecompass query --grep "^get_" # regex over graph entities
143
- codecompass query --search "session cookie" # keyword search
144
- codecompass query --map # compact {file: [symbols]} index
145
141
 
146
142
  # trace
147
143
  codecompass query --impact "Session.send" # callers (disambiguated), with file:line
@@ -155,6 +151,21 @@ codecompass query --tree # full hierarchy
155
151
 
156
152
  Add `--hops N` for traversal depth (start at 1 and follow the one path you need). Add `--rich` for tables.
157
153
 
154
+ ### Enrichment (agent-in-the-loop)
155
+
156
+ ```bash
157
+ codecompass enrich # stage entities for an agent swarm: one-line descriptions + missing call edges
158
+ codecompass enrich --apply # merge the swarm's results into the graph
159
+ codecompass add-entity <name> --file src/a.py --line 9 --description "Async helper"
160
+ codecompass add-call caller callee --line 2
161
+ ```
162
+
163
+ `enrich` is a bulk, user-triggered pass. `add-entity`/`add-call` are the
164
+ opportunistic version: as an agent reads code and spots something the parser
165
+ missed, it records it immediately. Everything agent-written is marked
166
+ `agent_inferred` and **preserved across re-ingests** — the graph gets better
167
+ with use. Ambiguous call targets are skipped, never guessed.
168
+
158
169
  ### Flow: `flow` vs `flow-summary`
159
170
 
160
171
  - **`flow`** — lean structure only (node name/kind/file/depth, edge from/to/order/line). What an agent needs to navigate; no embedded source.
@@ -167,8 +178,6 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
167
178
  | Tool | Returns |
168
179
  |---|---|
169
180
  | `grep(pattern, field, ignore_case)` | Regex search over graph entities |
170
- | `search(query, kind)` | Keyword search, ranked |
171
- | `map(include_tests)` | Compact `{file: [symbols]}` index |
172
181
  | `impact(symbol, hops)` | Callers/importers, disambiguated, with `resolved` + `line` |
173
182
  | `blast_radius(target, hops)` | Files reachable from a file or symbol |
174
183
  | `batch_impact(targets, hops)` | Union of blast radii for a multi-file change |
@@ -179,6 +188,9 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
179
188
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
180
189
  | `styles(element)` | CSS selectors that style an element |
181
190
  | `tree()` | Full project hierarchy |
191
+ | `enrich(apply, batch_size)` | Stage/merge agent-written descriptions + missing call edges |
192
+ | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity (`agent_inferred`) |
193
+ | `add_call(caller, callee, line)` | Record a parser-missed CALLS edge (`agent_inferred`) |
182
194
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
183
195
 
184
196
  ---
@@ -206,11 +218,14 @@ are language-agnostic.
206
218
  `AGENTS.md` guides any agent through the discover→trace→read→edit loop. For
207
219
  Claude Code and [pi](https://pi.dev), `init` also installs a `PreToolUse` hook
208
220
  that **blocks code *search*** (`grep`/`rg`, the `Grep`/`Glob` tools) and
209
- **whole-file `cat`**, routing discovery through the graph while leaving
210
- **targeted reads free** (the `Read` tool, `sed -n`, `head`/`tail`). The point is
211
- to change the default reflex to graph-first, not to remove reads. Both pick the
212
- files up after a one-time trust prompt (they execute shell commands). The hook
213
- is a plain file under `.claude/hooks/` edit or delete it to adjust.
221
+ **whole-file `cat` but only inside a codecompass-registered repo** (tracked in
222
+ `~/.codecompass/repos`, one line per `init`'d project). Reads outside any
223
+ registered repo pass through: no graph exists there, so nothing is blocked.
224
+ **Targeted reads stay free** (the `Read` tool, `sed -n`, `head`/`tail`). The
225
+ point is to change the default reflex to graph-first, not to remove reads.
226
+ Each project's Claude hook lives under its own `.claude/hooks/` with the
227
+ project root baked in — edit or delete it to adjust. Block messages point the
228
+ agent at the right repo's graph: `codecompass query \"<repo>\" --grep …`.
214
229
 
215
230
  ---
216
231
 
@@ -222,8 +237,11 @@ Source files
222
237
  ▼ code_parser tree-sitter extraction (no API calls) → typed CodeTriples
223
238
  ▼ graph.json NetworkX MultiDiGraph as JSON; file+class-qualified nodes,
224
239
  typed edges (CALLS/IMPORTS/INHERITS/STYLES/…), resolved calls
225
- ▼ code_query_cli traversal: grep / search / map / impact / blast-radius /
240
+ ▼ code_query_cli traversal: grep / impact / blast-radius /
226
241
  deps / flow / dead-code / tree
242
+ ▼ enricher agent-in-the-loop: enrich batches (descriptions +
243
+ missing calls) and opportunistic add_entity/add_call
244
+ writes — agent_inferred, preserved across re-ingest
227
245
  ```
228
246
 
229
247
  Everything runs locally, in-process. No network, no database, no API keys.
@@ -240,7 +258,9 @@ your-project/
240
258
 
241
259
  ## Limitations
242
260
 
243
- - **Structure, not semantics** — the graph knows what calls what, not what it means.
261
+ - **Structure first, semantics optional** — the parser knows what calls what,
262
+ not what it means. `enrich` closes that gap with agent-written descriptions
263
+ and missed edges, marked `agent_inferred`.
244
264
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
245
265
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
246
266
  `dead_code` results are always candidates to verify.
@@ -31,10 +31,8 @@ The graph turns navigation into a cheap, deterministic loop:
31
31
 
32
32
  | You have… | Use |
33
33
  |---|---|
34
- | a feature request that names a concept ("session timeout") | `grep` the concept |
35
- | a regex / name pattern | `grep` |
36
- | keywords | `search` |
37
- | a vague, nameless need | `map` (compact index to reason over) |
34
+ | a concept, name, or pattern | `grep` (regex over graph entities) |
35
+ | the full layout | `tree` |
38
36
 
39
37
  2. **Trace** — a relationship around a known symbol/file:
40
38
 
@@ -104,8 +102,6 @@ The server speaks stdio MCP and defaults to the working directory.
104
102
  ```bash
105
103
  # discover
106
104
  codecompass query --grep "^get_" # regex over graph entities
107
- codecompass query --search "session cookie" # keyword search
108
- codecompass query --map # compact {file: [symbols]} index
109
105
 
110
106
  # trace
111
107
  codecompass query --impact "Session.send" # callers (disambiguated), with file:line
@@ -119,6 +115,21 @@ codecompass query --tree # full hierarchy
119
115
 
120
116
  Add `--hops N` for traversal depth (start at 1 and follow the one path you need). Add `--rich` for tables.
121
117
 
118
+ ### Enrichment (agent-in-the-loop)
119
+
120
+ ```bash
121
+ codecompass enrich # stage entities for an agent swarm: one-line descriptions + missing call edges
122
+ codecompass enrich --apply # merge the swarm's results into the graph
123
+ codecompass add-entity <name> --file src/a.py --line 9 --description "Async helper"
124
+ codecompass add-call caller callee --line 2
125
+ ```
126
+
127
+ `enrich` is a bulk, user-triggered pass. `add-entity`/`add-call` are the
128
+ opportunistic version: as an agent reads code and spots something the parser
129
+ missed, it records it immediately. Everything agent-written is marked
130
+ `agent_inferred` and **preserved across re-ingests** — the graph gets better
131
+ with use. Ambiguous call targets are skipped, never guessed.
132
+
122
133
  ### Flow: `flow` vs `flow-summary`
123
134
 
124
135
  - **`flow`** — lean structure only (node name/kind/file/depth, edge from/to/order/line). What an agent needs to navigate; no embedded source.
@@ -131,8 +142,6 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
131
142
  | Tool | Returns |
132
143
  |---|---|
133
144
  | `grep(pattern, field, ignore_case)` | Regex search over graph entities |
134
- | `search(query, kind)` | Keyword search, ranked |
135
- | `map(include_tests)` | Compact `{file: [symbols]}` index |
136
145
  | `impact(symbol, hops)` | Callers/importers, disambiguated, with `resolved` + `line` |
137
146
  | `blast_radius(target, hops)` | Files reachable from a file or symbol |
138
147
  | `batch_impact(targets, hops)` | Union of blast radii for a multi-file change |
@@ -143,6 +152,9 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
143
152
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
144
153
  | `styles(element)` | CSS selectors that style an element |
145
154
  | `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`) |
146
158
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
147
159
 
148
160
  ---
@@ -170,11 +182,14 @@ are language-agnostic.
170
182
  `AGENTS.md` guides any agent through the discover→trace→read→edit loop. For
171
183
  Claude Code and [pi](https://pi.dev), `init` also installs a `PreToolUse` hook
172
184
  that **blocks code *search*** (`grep`/`rg`, the `Grep`/`Glob` tools) and
173
- **whole-file `cat`**, routing discovery through the graph while leaving
174
- **targeted reads free** (the `Read` tool, `sed -n`, `head`/`tail`). The point is
175
- to change the default reflex to graph-first, not to remove reads. Both pick the
176
- files up after a one-time trust prompt (they execute shell commands). The hook
177
- is a plain file under `.claude/hooks/` edit or delete it to adjust.
185
+ **whole-file `cat` but only inside a codecompass-registered repo** (tracked in
186
+ `~/.codecompass/repos`, one line per `init`'d project). Reads outside any
187
+ registered repo pass through: no graph exists there, so nothing is blocked.
188
+ **Targeted reads stay free** (the `Read` tool, `sed -n`, `head`/`tail`). The
189
+ point is to change the default reflex to graph-first, not to remove reads.
190
+ Each project's Claude hook lives under its own `.claude/hooks/` with the
191
+ project root baked in — edit or delete it to adjust. Block messages point the
192
+ agent at the right repo's graph: `codecompass query \"<repo>\" --grep …`.
178
193
 
179
194
  ---
180
195
 
@@ -186,8 +201,11 @@ Source files
186
201
  ▼ code_parser tree-sitter extraction (no API calls) → typed CodeTriples
187
202
  ▼ graph.json NetworkX MultiDiGraph as JSON; file+class-qualified nodes,
188
203
  typed edges (CALLS/IMPORTS/INHERITS/STYLES/…), resolved calls
189
- ▼ code_query_cli traversal: grep / search / map / impact / blast-radius /
204
+ ▼ code_query_cli traversal: grep / impact / blast-radius /
190
205
  deps / flow / dead-code / tree
206
+ ▼ enricher agent-in-the-loop: enrich batches (descriptions +
207
+ missing calls) and opportunistic add_entity/add_call
208
+ writes — agent_inferred, preserved across re-ingest
191
209
  ```
192
210
 
193
211
  Everything runs locally, in-process. No network, no database, no API keys.
@@ -204,7 +222,9 @@ your-project/
204
222
 
205
223
  ## Limitations
206
224
 
207
- - **Structure, not semantics** — the graph knows what calls what, not what it means.
225
+ - **Structure first, semantics optional** — the parser knows what calls what,
226
+ not what it means. `enrich` closes that gap with agent-written descriptions
227
+ and missed edges, marked `agent_inferred`.
208
228
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
209
229
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
210
230
  `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.2.1
3
+ Version: 4.1.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
@@ -67,10 +67,8 @@ The graph turns navigation into a cheap, deterministic loop:
67
67
 
68
68
  | You have… | Use |
69
69
  |---|---|
70
- | a feature request that names a concept ("session timeout") | `grep` the concept |
71
- | a regex / name pattern | `grep` |
72
- | keywords | `search` |
73
- | a vague, nameless need | `map` (compact index to reason over) |
70
+ | a concept, name, or pattern | `grep` (regex over graph entities) |
71
+ | the full layout | `tree` |
74
72
 
75
73
  2. **Trace** — a relationship around a known symbol/file:
76
74
 
@@ -140,8 +138,6 @@ The server speaks stdio MCP and defaults to the working directory.
140
138
  ```bash
141
139
  # discover
142
140
  codecompass query --grep "^get_" # regex over graph entities
143
- codecompass query --search "session cookie" # keyword search
144
- codecompass query --map # compact {file: [symbols]} index
145
141
 
146
142
  # trace
147
143
  codecompass query --impact "Session.send" # callers (disambiguated), with file:line
@@ -155,6 +151,21 @@ codecompass query --tree # full hierarchy
155
151
 
156
152
  Add `--hops N` for traversal depth (start at 1 and follow the one path you need). Add `--rich` for tables.
157
153
 
154
+ ### Enrichment (agent-in-the-loop)
155
+
156
+ ```bash
157
+ codecompass enrich # stage entities for an agent swarm: one-line descriptions + missing call edges
158
+ codecompass enrich --apply # merge the swarm's results into the graph
159
+ codecompass add-entity <name> --file src/a.py --line 9 --description "Async helper"
160
+ codecompass add-call caller callee --line 2
161
+ ```
162
+
163
+ `enrich` is a bulk, user-triggered pass. `add-entity`/`add-call` are the
164
+ opportunistic version: as an agent reads code and spots something the parser
165
+ missed, it records it immediately. Everything agent-written is marked
166
+ `agent_inferred` and **preserved across re-ingests** — the graph gets better
167
+ with use. Ambiguous call targets are skipped, never guessed.
168
+
158
169
  ### Flow: `flow` vs `flow-summary`
159
170
 
160
171
  - **`flow`** — lean structure only (node name/kind/file/depth, edge from/to/order/line). What an agent needs to navigate; no embedded source.
@@ -167,8 +178,6 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
167
178
  | Tool | Returns |
168
179
  |---|---|
169
180
  | `grep(pattern, field, ignore_case)` | Regex search over graph entities |
170
- | `search(query, kind)` | Keyword search, ranked |
171
- | `map(include_tests)` | Compact `{file: [symbols]}` index |
172
181
  | `impact(symbol, hops)` | Callers/importers, disambiguated, with `resolved` + `line` |
173
182
  | `blast_radius(target, hops)` | Files reachable from a file or symbol |
174
183
  | `batch_impact(targets, hops)` | Union of blast radii for a multi-file change |
@@ -179,6 +188,9 @@ Add `--hops N` for traversal depth (start at 1 and follow the one path you need)
179
188
  | `dead_code(include_entrypoints)` | Entities with no inbound caller |
180
189
  | `styles(element)` | CSS selectors that style an element |
181
190
  | `tree()` | Full project hierarchy |
191
+ | `enrich(apply, batch_size)` | Stage/merge agent-written descriptions + missing call edges |
192
+ | `add_entity(name, kind, file, line, description)` | Record a parser-missed entity (`agent_inferred`) |
193
+ | `add_call(caller, callee, line)` | Record a parser-missed CALLS edge (`agent_inferred`) |
182
194
  | `set_repo` / `get_repo` / `init` / `ingest` | Project selection & indexing |
183
195
 
184
196
  ---
@@ -206,11 +218,14 @@ are language-agnostic.
206
218
  `AGENTS.md` guides any agent through the discover→trace→read→edit loop. For
207
219
  Claude Code and [pi](https://pi.dev), `init` also installs a `PreToolUse` hook
208
220
  that **blocks code *search*** (`grep`/`rg`, the `Grep`/`Glob` tools) and
209
- **whole-file `cat`**, routing discovery through the graph while leaving
210
- **targeted reads free** (the `Read` tool, `sed -n`, `head`/`tail`). The point is
211
- to change the default reflex to graph-first, not to remove reads. Both pick the
212
- files up after a one-time trust prompt (they execute shell commands). The hook
213
- is a plain file under `.claude/hooks/` edit or delete it to adjust.
221
+ **whole-file `cat` but only inside a codecompass-registered repo** (tracked in
222
+ `~/.codecompass/repos`, one line per `init`'d project). Reads outside any
223
+ registered repo pass through: no graph exists there, so nothing is blocked.
224
+ **Targeted reads stay free** (the `Read` tool, `sed -n`, `head`/`tail`). The
225
+ point is to change the default reflex to graph-first, not to remove reads.
226
+ Each project's Claude hook lives under its own `.claude/hooks/` with the
227
+ project root baked in — edit or delete it to adjust. Block messages point the
228
+ agent at the right repo's graph: `codecompass query \"<repo>\" --grep …`.
214
229
 
215
230
  ---
216
231
 
@@ -222,8 +237,11 @@ Source files
222
237
  ▼ code_parser tree-sitter extraction (no API calls) → typed CodeTriples
223
238
  ▼ graph.json NetworkX MultiDiGraph as JSON; file+class-qualified nodes,
224
239
  typed edges (CALLS/IMPORTS/INHERITS/STYLES/…), resolved calls
225
- ▼ code_query_cli traversal: grep / search / map / impact / blast-radius /
240
+ ▼ code_query_cli traversal: grep / impact / blast-radius /
226
241
  deps / flow / dead-code / tree
242
+ ▼ enricher agent-in-the-loop: enrich batches (descriptions +
243
+ missing calls) and opportunistic add_entity/add_call
244
+ writes — agent_inferred, preserved across re-ingest
227
245
  ```
228
246
 
229
247
  Everything runs locally, in-process. No network, no database, no API keys.
@@ -240,7 +258,9 @@ your-project/
240
258
 
241
259
  ## Limitations
242
260
 
243
- - **Structure, not semantics** — the graph knows what calls what, not what it means.
261
+ - **Structure first, semantics optional** — the parser knows what calls what,
262
+ not what it means. `enrich` closes that gap with agent-written descriptions
263
+ and missed edges, marked `agent_inferred`.
244
264
  - **Static analysis** — dynamic dispatch, reflection, and string-based invocation
245
265
  can't be fully resolved. `impact` surfaces those flagged `resolved: false`, and
246
266
  `dead_code` results are always candidates to verify.
@@ -3,6 +3,7 @@ README.md
3
3
  config.py
4
4
  main.py
5
5
  mcp_server.py
6
+ pi_setup.py
6
7
  pyproject.toml
7
8
  codecompass_mcp.egg-info/PKG-INFO
8
9
  codecompass_mcp.egg-info/SOURCES.txt
@@ -16,7 +17,7 @@ graph/code_query_cli.py
16
17
  ingestion/__init__.py
17
18
  ingestion/code_normalizer.py
18
19
  ingestion/code_parser.py
19
- ingestion/description_enricher.py
20
+ ingestion/enricher.py
20
21
  ingestion/file_watcher.py
21
22
  ingestion/hierarchy_builder.py
22
23
  ingestion/source_context.py
@@ -25,8 +26,9 @@ models/code_types.py
25
26
  tests/test_accuracy_fixes.py
26
27
  tests/test_batch_impact.py
27
28
  tests/test_blast_radius.py
29
+ tests/test_block_hook.py
28
30
  tests/test_code_parser.py
29
- tests/test_description_enricher.py
31
+ tests/test_enricher.py
30
32
  tests/test_lit_css_parser.py
31
33
  tests/test_mcp_server.py
32
34
  tests/test_php_parser.py
@@ -4,3 +4,4 @@ ingestion
4
4
  main
5
5
  mcp_server
6
6
  models
7
+ pi_setup
@@ -236,6 +236,11 @@ class LocalGraphClient:
236
236
  "project": project,
237
237
  "file": triple.source_file,
238
238
  })
239
+ # Definition line comes only from DEFINED_IN triples — a CALLS triple's
240
+ # line_number is the call site, not the definition. External symbols
241
+ # (never defined in-project) legitimately have no line.
242
+ if triple.relation_type == "DEFINED_IN" and triple.line_number:
243
+ self.graph.nodes[from_id]["line"] = triple.line_number
239
244
  # A symbol is public if any of its definitions is exported. Never let a
240
245
  # later non-exported definition clear a flag an earlier one set.
241
246
  if getattr(triple, "is_exported", False):
@@ -254,6 +259,9 @@ class LocalGraphClient:
254
259
  existing["language"] = language
255
260
  existing["kind"] = f"{triple.to_type}:{language}"
256
261
  existing["description"] = f"{language} {triple.to_type}"
262
+ # A module's definition is line 1 of its file.
263
+ if triple.relation_type == "DEFINED_IN" and triple.to_type == "module":
264
+ existing.setdefault("line", 1)
257
265
 
258
266
  self.graph.add_edge(
259
267
  from_id,
@@ -321,6 +329,13 @@ class LocalGraphClient:
321
329
  to_id = self._resolve_to_id(triple, project, defs_by_name, class_file)
322
330
  self.write_code_triple(triple, file_id, project, from_id=from_id, to_id=to_id)
323
331
 
332
+ # Module nodes are the to-side of DEFINED_IN and may exist under a
333
+ # different resolved id — give every in-project module its line (1).
334
+ for _id, attr in self.graph.nodes(data=True):
335
+ if (attr.get("type") == "Entity" and attr.get("entity_type") == "module"
336
+ and attr.get("file") and "line" not in attr):
337
+ attr["line"] = 1
338
+
324
339
  self.save()
325
340
  return len(triples)
326
341
 
@@ -543,6 +558,7 @@ class LocalGraphClient:
543
558
  "name": a.get("name"),
544
559
  "kind": a.get("kind"),
545
560
  "file": a.get("file"),
561
+ "line": a.get("line"),
546
562
  "matched_field": f,
547
563
  "match": m.group(0),
548
564
  })
@@ -552,90 +568,6 @@ class LocalGraphClient:
552
568
  matches.sort(key=lambda r: (r["file"] or "", r["name"] or ""))
553
569
  return {"pattern": pattern, "matches": matches, "count": len(matches)}
554
570
 
555
- def search_entities(self, query: str, project: str, limit: int = 30,
556
- kind: str | None = None) -> list[dict]:
557
- """Find graph entities matching a keyword — the way in when you don't yet
558
- know a symbol name.
559
-
560
- Matches the query (case-insensitive) against each entity's name, file
561
- path, and description. Any term may match (OR), and entities matching
562
- more terms — and matching in the name rather than file/description — rank
563
- highest, so the most on-point symbols come first.
564
- Optional `kind` filters by entity_type (e.g. "function", "class").
565
- Returns a lean list (name/kind/file/entity_type) to then feed into
566
- impact/flow/deps.
567
- """
568
- terms = [t for t in query.lower().split() if t]
569
- if not terms:
570
- return []
571
-
572
- scored = []
573
- for _id, a in self.graph.nodes(data=True):
574
- if a.get("type") != "Entity" or a.get("project") != project:
575
- continue
576
- if not a.get("file"):
577
- continue
578
- if kind and a.get("entity_type") != kind:
579
- continue
580
- name = (a.get("name") or "")
581
- file = (a.get("file") or "")
582
- desc = (a.get("description") or "")
583
- hay_name = name.lower()
584
- hay_rest = f"{file} {desc}".lower()
585
- # any term may match (OR); score rewards name hits and more terms matched
586
- score = 0
587
- for t in terms:
588
- if t == hay_name:
589
- score += 100
590
- elif t in hay_name:
591
- score += 10
592
- elif t in hay_rest:
593
- score += 1
594
- if score == 0:
595
- continue
596
- scored.append((score, {
597
- "name": name,
598
- "entity_type": a.get("entity_type", ""),
599
- "kind": a.get("kind", ""),
600
- "file": file,
601
- }))
602
-
603
- scored.sort(key=lambda s: (-s[0], s[1]["file"], s[1]["name"]))
604
- return [row for _s, row in scored[:limit]]
605
-
606
- def symbol_map(self, project: str, include_tests: bool = False) -> dict:
607
- """A compact index of the codebase for an agent to reason over.
608
-
609
- Groups every source entity under its file as `{file: [names...]}` —
610
- names only, no depth/kind/description bloat — so the whole map fits in a
611
- few thousand tokens. The agent reads it once and uses its own judgment to
612
- pick what's relevant to a vague task ("where would caching go?"), then
613
- drills in with impact/flow/deps. This is the semantic-discovery entry
614
- point that keyword `search` can't be: an LLM knows caching lives near the
615
- request handler and response path even when nothing is named "cache".
616
-
617
- `include_tests=False` (default) drops test/example files so the map shows
618
- the actual implementation surface.
619
- """
620
- by_file: dict[str, list[str]] = {}
621
- for _id, a in self.graph.nodes(data=True):
622
- if a.get("type") != "Entity" or a.get("project") != project:
623
- continue
624
- f = a.get("file")
625
- if not f:
626
- continue
627
- if not include_tests and ("test/" in f or "test\\" in f
628
- or f.startswith("test") or "example" in f):
629
- continue
630
- name = a.get("name")
631
- if name:
632
- by_file.setdefault(f, [])
633
- if name not in by_file[f]:
634
- by_file[f].append(name)
635
- for f in by_file:
636
- by_file[f].sort()
637
- return dict(sorted(by_file.items()))
638
-
639
571
  def find_dependencies(self, file_path: str, project: str, max_hops: int = 3) -> list[dict]:
640
572
  """Return all modules imported (directly or transitively) by file_path."""
641
573
  file_node = next((n for n, attr in self.graph.nodes(data=True)
@@ -662,6 +594,7 @@ class LocalGraphClient:
662
594
  results.append({
663
595
  "dependency": node.get("name"),
664
596
  "dep_type": node.get("entity_type"),
597
+ "line": node.get("line"),
665
598
  "depth": depth + 1
666
599
  })
667
600
 
@@ -710,6 +643,7 @@ class LocalGraphClient:
710
643
  "kind": start_attr.get("kind", ""),
711
644
  "description": start_attr.get("description", ""),
712
645
  "file": start_attr.get("file", ""),
646
+ "line": start_attr.get("line"),
713
647
  "depth": 0,
714
648
  }
715
649
 
@@ -743,6 +677,7 @@ class LocalGraphClient:
743
677
  "kind": succ_attr.get("kind", ""),
744
678
  "description": succ_attr.get("description", ""),
745
679
  "file": succ_attr.get("file", ""),
680
+ "line": succ_attr.get("line"),
746
681
  "depth": depth + 1,
747
682
  }
748
683
  queue.append((succ, depth + 1))
@@ -776,6 +711,7 @@ class LocalGraphClient:
776
711
  "callee_name": node.get("name"),
777
712
  "callee_type": node.get("entity_type"),
778
713
  "callee_file": node.get("file"),
714
+ "line": node.get("line"),
779
715
  "depth": depth + 1
780
716
  })
781
717
  visited.add(succ)
@@ -847,6 +783,7 @@ class LocalGraphClient:
847
783
  "kind": attr.get("kind", ""),
848
784
  "entity_type": attr.get("entity_type", ""),
849
785
  "file": attr.get("file", ""),
786
+ "line": attr.get("line"),
850
787
  }
851
788
  # Exported symbols and name-heuristic entry points are public API:
852
789
  # having no in-repo caller is expected, not evidence of dead code.
@@ -52,21 +52,6 @@ def fetch_grep(pattern: str, repo_path: str, project: str, field: str = "all",
52
52
  ignore_case=ignore_case, limit=limit)
53
53
 
54
54
 
55
- def fetch_search(query: str, repo_path: str, project: str, limit: int = 30,
56
- kind: str | None = None) -> dict:
57
- """Search the graph for entities matching a keyword — the discovery entry point."""
58
- with closing(get_client(repo_path)) as client:
59
- rows = client.search_entities(query, project, limit=limit, kind=kind)
60
- return {"query": query, "matches": rows, "count": len(rows)}
61
-
62
-
63
- def fetch_map(repo_path: str, project: str, include_tests: bool = False) -> dict:
64
- """Compact `{file: [symbols]}` index for an agent to reason over during discovery."""
65
- with closing(get_client(repo_path)) as client:
66
- m = client.symbol_map(project, include_tests=include_tests)
67
- return {"project": project, "files": m, "file_count": len(m)}
68
-
69
-
70
55
  def fetch_deps(file_path: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS) -> dict:
71
56
  """Return what file_path imports as structured data."""
72
57
  with closing(get_client(repo_path)) as client:
@@ -204,7 +189,7 @@ def fetch_flow(start_name: str, repo_path: str, project: str, max_hops: int = DE
204
189
  edges = _order_edges(edges, project, start_name)
205
190
  lean_nodes = [
206
191
  {"id": n["id"], "name": n["name"], "kind": n.get("kind", ""),
207
- "file": n.get("file", ""), "depth": n["depth"]}
192
+ "file": n.get("file", ""), "line": n.get("line"), "depth": n["depth"]}
208
193
  for n in sorted(nodes, key=lambda n: n["depth"])
209
194
  ]
210
195
  lean_edges = [
@@ -291,10 +276,6 @@ def main() -> None:
291
276
  run_flow(args.flow_summary, repo_path, project, max_hops=args.hops, rich=rich,
292
277
  output=args.flow_output, include_external=args.include_external,
293
278
  fmt=args.format if args.format != "drawio" else "mermaid")
294
- elif args.map:
295
- print(json.dumps(fetch_map(repo_path, project), indent=2))
296
- elif args.search:
297
- print(json.dumps(fetch_search(args.search, repo_path, project), indent=2))
298
279
  elif args.grep:
299
280
  print(json.dumps(fetch_grep(args.grep, repo_path, project), indent=2))
300
281
  elif args.dead_code:
@@ -325,16 +306,16 @@ def run_impact(entity_name: str, repo_path: str, project: str, max_hops: int = D
325
306
  console.print(f"\n[bold blue]Impact analysis:[/] {entity_name}")
326
307
  if stamp:
327
308
  console.print(stamp)
328
- table = _make_table(title=f"Callers of '{entity_name}'", columns=["Caller", "Type", "File", "Depth"])
309
+ table = _make_table(title=f"Callers of '{entity_name}'", columns=["Caller", "Type", "File", "Line", "Depth"])
329
310
  for row in rows:
330
- table.add_row(row.get("caller_name",""), row.get("caller_type",""), row.get("caller_file",""), str(row.get("depth","")))
311
+ table.add_row(row.get("caller_name",""), row.get("caller_type",""), row.get("caller_file",""), str(row.get("line") or ""), str(row.get("depth","")))
331
312
  console.print(table)
332
313
  else:
333
314
  if stamp:
334
315
  print(stamp)
335
316
  print(f"Callers of '{entity_name}':")
336
317
  for row in rows:
337
- print(f" {row.get('caller_name','')} ({row.get('caller_type','')}) in {row.get('caller_file','')} [depth {row.get('depth','')}]")
318
+ print(f" {row.get('caller_name','')} ({row.get('caller_type','')}) in {row.get('caller_file','')}:{row.get('line') or '?'} [depth {row.get('depth','')}]")
338
319
 
339
320
 
340
321
  def run_deps(file_path: str, repo_path: str, project: str, max_hops: int = DEFAULT_HOPS, rich: bool = False) -> None:
@@ -352,16 +333,16 @@ def run_deps(file_path: str, repo_path: str, project: str, max_hops: int = DEFAU
352
333
  console.print(f"\n[bold blue]Dependencies of:[/] {file_path}")
353
334
  if stamp:
354
335
  console.print(stamp)
355
- table = _make_table(title=f"Dependencies of '{file_path}'", columns=["Module", "Type", "Depth"])
336
+ table = _make_table(title=f"Dependencies of '{file_path}'", columns=["Module", "Type", "Line", "Depth"])
356
337
  for row in rows:
357
- table.add_row(row.get("dependency",""), row.get("dep_type",""), str(row.get("depth","")))
338
+ table.add_row(row.get("dependency",""), row.get("dep_type",""), str(row.get("line") or ""), str(row.get("depth","")))
358
339
  console.print(table)
359
340
  else:
360
341
  if stamp:
361
342
  print(stamp)
362
343
  print(f"Dependencies of '{file_path}':")
363
344
  for row in rows:
364
- print(f" {row.get('dependency','')} ({row.get('dep_type','')}) [depth {row.get('depth','')}]")
345
+ print(f" {row.get('dependency','')} ({row.get('dep_type','')}) :{row.get('line') or '?'} [depth {row.get('depth','')}]")
365
346
 
366
347
 
367
348
  def run_styles(element_name: str, repo_path: str, project: str, rich: bool = False) -> None:
@@ -480,10 +461,11 @@ def run_dead_code(repo_path: str, project: str, rich: bool = False,
480
461
  print(f" {f}")
481
462
  for r in by_file[f]:
482
463
  et = r.get("entity_type", "")
464
+ line = r.get("line") or "?"
483
465
  if rich:
484
- console.print(f" [yellow]{r['name']}[/] [dim]({et})[/]")
466
+ console.print(f" [yellow]{r['name']}[/] [dim]({et}) :{line}[/]")
485
467
  else:
486
- print(f" {r['name']} ({et})")
468
+ print(f" {r['name']} ({et}) :{line}")
487
469
 
488
470
  header = "[bold red]Dead-code candidates[/]" if rich else "Dead-code candidates"
489
471
  if rich:
@@ -948,10 +930,6 @@ def _parse_args() -> argparse.Namespace:
948
930
  parser.add_argument("--flow", metavar="ENTITY")
949
931
  parser.add_argument("--flow-summary", metavar="ENTITY",
950
932
  help="Flow trace + rendered narration (mermaid by default) for a human")
951
- parser.add_argument("--map", action="store_true",
952
- help="Compact {file: [symbols]} index to reason over during discovery")
953
- parser.add_argument("--search", metavar="KEYWORDS",
954
- help="Keyword search over entity names/files/descriptions")
955
933
  parser.add_argument("--grep", metavar="REGEX",
956
934
  help="Regex search over graph entities (grep for the graph)")
957
935
  parser.add_argument("--flow-output", metavar="PATH", default=None)