|
1
|
# Analysis API Reference |
|
2
|
|
|
3
|
```python |
|
4
|
from navegador.analysis import ( |
|
5
|
ImpactAnalyzer, |
|
6
|
FlowTracer, |
|
7
|
DeadCodeDetector, |
|
8
|
CycleDetector, |
|
9
|
TestMapper, |
|
10
|
) |
|
11
|
from navegador.graph import GraphStore |
|
12
|
``` |
|
13
|
|
|
14
|
--- |
|
15
|
|
|
16
|
## ImpactAnalyzer |
|
17
|
|
|
18
|
Traces downstream dependents of a function, class, or file by following `CALLS`, `INHERITS`, and `IMPORTS` edges. |
|
19
|
|
|
20
|
```python |
|
21
|
class ImpactAnalyzer: |
|
22
|
def __init__(self, store: GraphStore) -> None: ... |
|
23
|
``` |
|
24
|
|
|
25
|
### `analyze` |
|
26
|
|
|
27
|
```python |
|
28
|
def analyze( |
|
29
|
self, |
|
30
|
name: str, |
|
31
|
*, |
|
32
|
node_type: str = "", |
|
33
|
file: str = "", |
|
34
|
depth: int = 0, |
|
35
|
include_tests: bool = False, |
|
36
|
include_knowledge: bool = False, |
|
37
|
) -> ImpactResult |
|
38
|
``` |
|
39
|
|
|
40
|
Compute the impact set for a given node. |
|
41
|
|
|
42
|
**Parameters:** |
|
43
|
|
|
44
|
| Name | Type | Default | Description | |
|
45
|
|---|---|---|---| |
|
46
|
| `name` | `str` | — | Name of the function, class, or file to analyze | |
|
47
|
| `node_type` | `str` | `""` | Node type hint: `"Function"`, `"Class"`, `"File"` | |
|
48
|
| `file` | `str` | `""` | Optional file path to disambiguate | |
|
49
|
| `depth` | `int` | `0` | Maximum hops to follow (0 = unlimited) | |
|
50
|
| `include_tests` | `bool` | `False` | Include test files in the impact set | |
|
51
|
| `include_knowledge` | `bool` | `False` | Include linked knowledge nodes (rules, concepts, decisions) | |
|
52
|
|
|
53
|
**Returns:** `ImpactResult` |
|
54
|
|
|
55
|
--- |
|
56
|
|
|
57
|
### ImpactResult |
|
58
|
|
|
59
|
```python |
|
60
|
@dataclass |
|
61
|
class ImpactResult: |
|
62
|
root: ContextNode |
|
63
|
direct_dependents: list[ContextNode] |
|
64
|
transitive_dependents: list[ContextNode] |
|
65
|
affected_files: list[str] |
|
66
|
knowledge_nodes: list[ContextNode] # empty unless include_knowledge=True |
|
67
|
depth_reached: int |
|
68
|
``` |
|
69
|
|
|
70
|
| Field | Type | Description | |
|
71
|
|---|---|---| |
|
72
|
| `root` | `ContextNode` | The analyzed node | |
|
73
|
| `direct_dependents` | `list[ContextNode]` | Nodes one hop away | |
|
74
|
| `transitive_dependents` | `list[ContextNode]` | All nodes reachable beyond one hop | |
|
75
|
| `affected_files` | `list[str]` | Unique file paths in the full dependent set | |
|
76
|
| `knowledge_nodes` | `list[ContextNode]` | Linked concepts, rules, decisions | |
|
77
|
| `depth_reached` | `int` | Actual maximum depth traversed | |
|
78
|
|
|
79
|
**Example:** |
|
80
|
|
|
81
|
```python |
|
82
|
store = GraphStore.sqlite(".navegador/navegador.db") |
|
83
|
analyzer = ImpactAnalyzer(store) |
|
84
|
|
|
85
|
result = analyzer.analyze("validate_token", depth=3, include_knowledge=True) |
|
86
|
print(f"{len(result.direct_dependents)} direct dependents") |
|
87
|
print(f"{len(result.transitive_dependents)} transitive dependents") |
|
88
|
print(f"Affects {len(result.affected_files)} files") |
|
89
|
for rule in [n for n in result.knowledge_nodes if n.label == "Rule"]: |
|
90
|
print(f" Governed by: {rule.name} ({rule.properties.get('severity')})") |
|
91
|
``` |
|
92
|
|
|
93
|
--- |
|
94
|
|
|
95
|
## FlowTracer |
|
96
|
|
|
97
|
Finds call paths between two functions. |
|
98
|
|
|
99
|
```python |
|
100
|
class FlowTracer: |
|
101
|
def __init__(self, store: GraphStore) -> None: ... |
|
102
|
``` |
|
103
|
|
|
104
|
### `trace` |
|
105
|
|
|
106
|
```python |
|
107
|
def trace( |
|
108
|
self, |
|
109
|
from_name: str, |
|
110
|
to_name: str, |
|
111
|
*, |
|
112
|
from_file: str = "", |
|
113
|
to_file: str = "", |
|
114
|
max_paths: int = 3, |
|
115
|
max_depth: int = 10, |
|
116
|
) -> list[FlowPath] |
|
117
|
``` |
|
118
|
|
|
119
|
Find call chains from `from_name` to `to_name`. |
|
120
|
|
|
121
|
**Parameters:** |
|
122
|
|
|
123
|
| Name | Type | Default | Description | |
|
124
|
|---|---|---|---| |
|
125
|
| `from_name` | `str` | — | Starting function name | |
|
126
|
| `to_name` | `str` | — | Target function name | |
|
127
|
| `from_file` | `str` | `""` | File path to disambiguate start | |
|
128
|
| `to_file` | `str` | `""` | File path to disambiguate target | |
|
129
|
| `max_paths` | `int` | `3` | Maximum number of paths to return | |
|
130
|
| `max_depth` | `int` | `10` | Maximum call chain length | |
|
131
|
|
|
132
|
**Returns:** `list[FlowPath]` |
|
133
|
|
|
134
|
--- |
|
135
|
|
|
136
|
### FlowPath |
|
137
|
|
|
138
|
```python |
|
139
|
@dataclass |
|
140
|
class FlowPath: |
|
141
|
nodes: list[str] # function names in order |
|
142
|
node_details: list[ContextNode] |
|
143
|
length: int |
|
144
|
``` |
|
145
|
|
|
146
|
**Example:** |
|
147
|
|
|
148
|
```python |
|
149
|
tracer = FlowTracer(store) |
|
150
|
paths = tracer.trace("create_order", "process_payment", max_paths=5) |
|
151
|
for i, path in enumerate(paths, 1): |
|
152
|
print(f"Path {i}: {' -> '.join(path.nodes)}") |
|
153
|
``` |
|
154
|
|
|
155
|
--- |
|
156
|
|
|
157
|
## DeadCodeDetector |
|
158
|
|
|
159
|
Identifies functions and classes that are never called, never imported, and not entry points. |
|
160
|
|
|
161
|
```python |
|
162
|
class DeadCodeDetector: |
|
163
|
def __init__(self, store: GraphStore) -> None: ... |
|
164
|
``` |
|
165
|
|
|
166
|
### `find` |
|
167
|
|
|
168
|
```python |
|
169
|
def find( |
|
170
|
self, |
|
171
|
path: str | Path, |
|
172
|
*, |
|
173
|
exclude_tests: bool = False, |
|
174
|
min_confidence: int = 80, |
|
175
|
) -> list[DeadCodeCandidate] |
|
176
|
``` |
|
177
|
|
|
178
|
Find potentially dead code within a path. |
|
179
|
|
|
180
|
**Parameters:** |
|
181
|
|
|
182
|
| Name | Type | Default | Description | |
|
183
|
|---|---|---|---| |
|
184
|
| `path` | `str \| Path` | — | Directory or file to analyze | |
|
185
|
| `exclude_tests` | `bool` | `False` | Skip test files | |
|
186
|
| `min_confidence` | `int` | `80` | Minimum confidence score to include (0–100) | |
|
187
|
|
|
188
|
**Returns:** `list[DeadCodeCandidate]` |
|
189
|
|
|
190
|
--- |
|
191
|
|
|
192
|
### DeadCodeCandidate |
|
193
|
|
|
194
|
```python |
|
195
|
@dataclass |
|
196
|
class DeadCodeCandidate: |
|
197
|
node: ContextNode |
|
198
|
confidence: int # 0–100 |
|
199
|
reasons: list[str] # e.g. ["no callers", "no imports", "no decorator entry point"] |
|
200
|
``` |
|
201
|
|
|
202
|
| Field | Type | Description | |
|
203
|
|---|---|---| |
|
204
|
| `node` | `ContextNode` | The potentially dead node | |
|
205
|
| `confidence` | `int` | Confidence that this is truly unreachable (higher = more confident) | |
|
206
|
| `reasons` | `list[str]` | Reasons for the classification | |
|
207
|
|
|
208
|
**Example:** |
|
209
|
|
|
210
|
```python |
|
211
|
detector = DeadCodeDetector(store) |
|
212
|
candidates = detector.find("./src", exclude_tests=True, min_confidence=90) |
|
213
|
for c in candidates: |
|
214
|
print(f"[{c.confidence}%] {c.node.label}: {c.node.name} {c.node.properties['file']}") |
|
215
|
print(f" Reasons: {', '.join(c.reasons)}") |
|
216
|
``` |
|
217
|
|
|
218
|
--- |
|
219
|
|
|
220
|
## CycleDetector |
|
221
|
|
|
222
|
Finds circular dependency chains in call and import graphs. |
|
223
|
|
|
224
|
```python |
|
225
|
class CycleDetector: |
|
226
|
def __init__(self, store: GraphStore) -> None: ... |
|
227
|
``` |
|
228
|
|
|
229
|
### `find_import_cycles` |
|
230
|
|
|
231
|
```python |
|
232
|
def find_import_cycles( |
|
233
|
self, |
|
234
|
path: str | Path, |
|
235
|
*, |
|
236
|
min_length: int = 2, |
|
237
|
) -> list[Cycle] |
|
238
|
``` |
|
239
|
|
|
240
|
Find circular import chains within a path. |
|
241
|
|
|
242
|
--- |
|
243
|
|
|
244
|
### `find_call_cycles` |
|
245
|
|
|
246
|
```python |
|
247
|
def find_call_cycles( |
|
248
|
self, |
|
249
|
path: str | Path, |
|
250
|
*, |
|
251
|
min_length: int = 2, |
|
252
|
) -> list[Cycle] |
|
253
|
``` |
|
254
|
|
|
255
|
Find circular call chains within a path. |
|
256
|
|
|
257
|
--- |
|
258
|
|
|
259
|
### `find_all` |
|
260
|
|
|
261
|
```python |
|
262
|
def find_all( |
|
263
|
self, |
|
264
|
path: str | Path, |
|
265
|
*, |
|
266
|
min_length: int = 2, |
|
267
|
) -> CycleReport |
|
268
|
``` |
|
269
|
|
|
270
|
Find both import and call cycles. |
|
271
|
|
|
272
|
**Parameters (all methods):** |
|
273
|
|
|
274
|
| Name | Type | Default | Description | |
|
275
|
|---|---|---|---| |
|
276
|
| `path` | `str \| Path` | — | Directory or file to analyze | |
|
277
|
| `min_length` | `int` | `2` | Minimum cycle length to report | |
|
278
|
|
|
279
|
**Returns:** `list[Cycle]` or `CycleReport` |
|
280
|
|
|
281
|
--- |
|
282
|
|
|
283
|
### Cycle |
|
284
|
|
|
285
|
```python |
|
286
|
@dataclass |
|
287
|
class Cycle: |
|
288
|
path: list[str] # node names (files or functions) forming the cycle |
|
289
|
cycle_type: str # "import" or "call" |
|
290
|
length: int |
|
291
|
``` |
|
292
|
|
|
293
|
### CycleReport |
|
294
|
|
|
295
|
```python |
|
296
|
@dataclass |
|
297
|
class CycleReport: |
|
298
|
import_cycles: list[Cycle] |
|
299
|
call_cycles: list[Cycle] |
|
300
|
total: int |
|
301
|
``` |
|
302
|
|
|
303
|
**Example:** |
|
304
|
|
|
305
|
```python |
|
306
|
detector = CycleDetector(store) |
|
307
|
report = detector.find_all("./src") |
|
308
|
print(f"{report.total} cycles found") |
|
309
|
for cycle in report.import_cycles: |
|
310
|
print(f" Import cycle: {' -> '.join(cycle.path)}") |
|
311
|
``` |
|
312
|
|
|
313
|
--- |
|
314
|
|
|
315
|
## TestMapper |
|
316
|
|
|
317
|
Maps test functions to the production code they exercise via call graph analysis. |
|
318
|
|
|
319
|
```python |
|
320
|
class TestMapper: |
|
321
|
def __init__(self, store: GraphStore) -> None: ... |
|
322
|
``` |
|
323
|
|
|
324
|
### `map` |
|
325
|
|
|
326
|
```python |
|
327
|
def map( |
|
328
|
self, |
|
329
|
src_path: str | Path, |
|
330
|
test_path: str | Path, |
|
331
|
*, |
|
332
|
target: str = "", |
|
333
|
) -> TestCoverageMap |
|
334
|
``` |
|
335
|
|
|
336
|
Build a mapping of production functions to their covering tests. |
|
337
|
|
|
338
|
**Parameters:** |
|
339
|
|
|
340
|
| Name | Type | Default | Description | |
|
341
|
|---|---|---|---| |
|
342
|
| `src_path` | `str \| Path` | — | Production code directory | |
|
343
|
| `test_path` | `str \| Path` | — | Test directory | |
|
344
|
| `target` | `str` | `""` | If set, only map coverage for this specific function | |
|
345
|
|
|
346
|
**Returns:** `TestCoverageMap` |
|
347
|
|
|
348
|
--- |
|
349
|
|
|
350
|
### `uncovered` |
|
351
|
|
|
352
|
```python |
|
353
|
def uncovered( |
|
354
|
self, |
|
355
|
src_path: str | Path, |
|
356
|
test_path: str | Path, |
|
357
|
) -> list[ContextNode] |
|
358
|
``` |
|
359
|
|
|
360
|
Return production functions and classes with no covering tests. |
|
361
|
|
|
362
|
--- |
|
363
|
|
|
364
|
### TestCoverageMap |
|
365
|
|
|
366
|
```python |
|
367
|
@dataclass |
|
368
|
class TestCoverageMap: |
|
369
|
coverage: dict[str, list[ContextNode]] # function name -> list of test nodes |
|
370
|
uncovered: list[ContextNode] |
|
371
|
coverage_percent: float |
|
372
|
``` |
|
373
|
|
|
374
|
| Field | Type | Description | |
|
375
|
|---|---|---| |
|
376
|
| `coverage` | `dict[str, list[ContextNode]]` | Maps each production function to its test nodes | |
|
377
|
| `uncovered` | `list[ContextNode]` | Production functions with no tests | |
|
378
|
| `coverage_percent` | `float` | Percentage of functions with at least one test | |
|
379
|
|
|
380
|
**Example:** |
|
381
|
|
|
382
|
```python |
|
383
|
mapper = TestMapper(store) |
|
384
|
coverage_map = mapper.map("./src", "./tests") |
|
385
|
|
|
386
|
print(f"Coverage: {coverage_map.coverage_percent:.1f}%") |
|
387
|
print(f"Uncovered: {len(coverage_map.uncovered)} functions") |
|
388
|
|
|
389
|
for fn_name, tests in coverage_map.coverage.items(): |
|
390
|
print(f" {fn_name}: {len(tests)} tests") |
|
391
|
for test in tests: |
|
392
|
print(f" {test.properties['file']}::{test.name}") |
|
393
|
``` |
|
394
|
|