contextl 1.0.1 → 1.1.1
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.
- package/README.md +57 -78
- package/package.json +1 -1
- package/python/impact_analysis.py +196 -0
- package/python/mcp_server.py +85 -2
package/README.md
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
# contextl
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Architecture intelligence for AI coding agents.**
|
|
4
|
+
|
|
5
|
+
Stop letting your AI agent read your entire codebase to make one small change. `contextl` finds the exact files that matter — using graph theory, not guesswork.
|
|
5
6
|
|
|
6
7
|
```
|
|
7
|
-
"fix the
|
|
8
|
+
"fix the broken checkout flow"
|
|
9
|
+
↓
|
|
10
|
+
components/Checkout.tsx [high confidence]
|
|
11
|
+
lib/api.ts [high confidence]
|
|
12
|
+
types/index.ts [medium confidence]
|
|
8
13
|
```
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
No LLM. No embeddings. No API keys. No vector database. Pure dependency graph + text scoring — runs entirely on your machine, your code never leaves your laptop.
|
|
11
16
|
|
|
12
17
|
---
|
|
13
18
|
|
|
14
|
-
##
|
|
15
|
-
|
|
16
|
-
**Requires Python 3.9+** on your PATH. Everything else (`networkx`, `mcp`) is installed automatically on first run.
|
|
19
|
+
## Install (60 seconds)
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
**Requires Python 3.9+** on your PATH. Everything else (`networkx`, `mcp`) installs automatically on first run.
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
Add this to your IDE's MCP config file:
|
|
21
24
|
|
|
22
25
|
```json
|
|
23
26
|
{
|
|
@@ -30,103 +33,79 @@ Paste this JSON into your IDE's MCP config file (paths listed below):
|
|
|
30
33
|
}
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
**Where to find your config file:**
|
|
34
37
|
|
|
35
|
-
| IDE | Config
|
|
38
|
+
| IDE | Config path |
|
|
36
39
|
|-----|-------------|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
40
|
+
| Antigravity | `~/.gemini/config/mcp_config.json` |
|
|
41
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
42
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
43
|
+
| Claude Code | `~/.claude.json` |
|
|
44
|
+
| VS Code | `.vscode/mcp.json` |
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Restart your IDE. Done — no cloning, no Python setup beyond having it installed.
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
---
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
You: "fix the file upload error handler"
|
|
49
|
-
IDE: calls query_repo → gets [FileUploader.tsx, lib/upload.ts, …]
|
|
50
|
-
IDE: reads only those 5 files instead of the whole repo
|
|
51
|
-
```
|
|
50
|
+
## What it gives your agent
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
Three tools, automatically available once connected:
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
### `query_repo`
|
|
55
|
+
*"Find the files relevant to this change."*
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Ranks every file in your repo against a natural-language query using filename matching, content matching, and graph proximity. Returns confidence-scored results with plain-English reasoning.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
### `analyze_impact`
|
|
60
|
+
*"If I change this file, what breaks?"*
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
- `repo_path` — absolute path to the repository root
|
|
63
|
-
- `query` — natural-language description of the change (e.g. `"change the download button color"`)
|
|
64
|
-
- `top_n` — max results to return (default `5`, max `20`)
|
|
62
|
+
Walks the dependency graph upstream from any file to find every direct and transitive dependent. Flags likely test files so your agent knows what to re-run. Essential before touching shared files like `types/`, `utils/`, or config.
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
"query": "change the download button",
|
|
70
|
-
"repo": "/path/to/repo",
|
|
71
|
-
"total_files_scanned": 142,
|
|
72
|
-
"results": [
|
|
73
|
-
{
|
|
74
|
-
"rank": 1,
|
|
75
|
-
"path": "components/DownloadButton.tsx",
|
|
76
|
-
"score": 0.9800,
|
|
77
|
-
"confidence": "high",
|
|
78
|
-
"matched_terms": ["button", "download"],
|
|
79
|
-
"reasoning": "Filename strongly matches query terms; file contents heavily reference query terms."
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
```
|
|
64
|
+
### `scan_repo`
|
|
65
|
+
*"What files exist here?"*
|
|
84
66
|
|
|
85
|
-
|
|
67
|
+
Lists every source file `contextl` can see — useful for the agent to orient itself before doing anything else.
|
|
86
68
|
|
|
87
|
-
|
|
69
|
+
---
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{ "path": "components/Button.tsx", "extension": ".tsx", "size_bytes": 1024 }
|
|
96
|
-
]
|
|
97
|
-
}
|
|
98
|
-
```
|
|
71
|
+
## How the ranking works
|
|
72
|
+
|
|
73
|
+
1. **Keyword match** — does the filename contain query terms?
|
|
74
|
+
2. **Content match** — does the file's source code mention the terms?
|
|
75
|
+
3. **Graph proximity** — files connected to high-scoring files get a relevance boost
|
|
76
|
+
4. **Centrality (PageRank)** — heavily-connected files rank higher when scores tie
|
|
99
77
|
|
|
100
78
|
---
|
|
101
79
|
|
|
102
|
-
##
|
|
80
|
+
## Supported languages
|
|
103
81
|
|
|
104
|
-
|
|
82
|
+
TypeScript, TSX, JavaScript, JSX — built for Next.js and React codebases first.
|
|
105
83
|
|
|
106
|
-
|
|
84
|
+
Python, Go, and Rust support are on the roadmap.
|
|
107
85
|
|
|
108
|
-
|
|
109
|
-
|--------|--------|-------------|
|
|
110
|
-
| Keyword match | 0.5 | Query terms in the file path / name |
|
|
111
|
-
| Content match | 0.5 | Query terms inside the file source |
|
|
112
|
-
| Neighbor bonus | +0.15 | Files near high-scoring files in the import graph |
|
|
113
|
-
| PageRank | 0.05 | Tiebreaker: more connected files rank slightly higher |
|
|
86
|
+
---
|
|
114
87
|
|
|
115
|
-
|
|
88
|
+
## Why this exists
|
|
89
|
+
|
|
90
|
+
AI coding agents are increasingly good at writing code. They're still bad at knowing *where* to look. On a 5,000-file repo, an agent might read 100+ files just to change a logo. `contextl` exists to fix that — and to eventually give agents a real model of your codebase's architecture, not just a file list.
|
|
116
91
|
|
|
117
92
|
---
|
|
118
93
|
|
|
119
|
-
##
|
|
94
|
+
## Also available for Python
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install contextl-mcp
|
|
98
|
+
```
|
|
120
99
|
|
|
121
|
-
|
|
122
|
-
|-------------|---------|
|
|
123
|
-
| Node.js | ≥ 18 |
|
|
124
|
-
| Python | ≥ 3.9 |
|
|
125
|
-
| `networkx` | auto-installed |
|
|
126
|
-
| `mcp` | auto-installed |
|
|
100
|
+
Same engine, same tools — installable via PyPI for Python-first workflows.
|
|
127
101
|
|
|
128
102
|
---
|
|
129
103
|
|
|
104
|
+
## Links
|
|
105
|
+
|
|
106
|
+
- [GitHub](https://github.com/DS0710-coder/contextl)
|
|
107
|
+
- [PyPI package](https://pypi.org/project/contextl-mcp)
|
|
108
|
+
|
|
130
109
|
## License
|
|
131
110
|
|
|
132
111
|
MIT
|
package/package.json
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Repository Intelligence Engine
|
|
3
|
+
Step 6: Change Impact Analysis
|
|
4
|
+
|
|
5
|
+
Answers: "If I modify this file, what else is affected?"
|
|
6
|
+
|
|
7
|
+
Walks the dependency graph upstream (predecessors) from a given file to find
|
|
8
|
+
every file that directly or transitively depends on it. Also flags likely
|
|
9
|
+
test files so the impact report highlights what needs re-testing.
|
|
10
|
+
|
|
11
|
+
Pure graph traversal — no LLM, no embeddings. Uses the same RepoGraph
|
|
12
|
+
built in graph_builder.py.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from scanner import scan_repo
|
|
18
|
+
from import_parser import parse_imports
|
|
19
|
+
from graph_builder import build_graph, RepoGraph
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Heuristics for detecting test files by path/name
|
|
23
|
+
TEST_MARKERS = ("test", "spec", "__tests__", "__mocks__")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ImpactedFile:
|
|
28
|
+
"""A single file affected by a change, with its distance from the source."""
|
|
29
|
+
path: str
|
|
30
|
+
distance: int # 1 = directly imports the changed file, 2 = imports something that does, etc.
|
|
31
|
+
is_test_file: bool
|
|
32
|
+
via: str # the immediate file that pulled this one in (for tracing the chain)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ImpactReport:
|
|
37
|
+
"""Full result of an impact analysis for one target file."""
|
|
38
|
+
target: str
|
|
39
|
+
directly_affected: list[ImpactedFile] = field(default_factory=list)
|
|
40
|
+
transitively_affected: list[ImpactedFile] = field(default_factory=list)
|
|
41
|
+
test_files: list[str] = field(default_factory=list)
|
|
42
|
+
total_affected: int = 0
|
|
43
|
+
|
|
44
|
+
def summary(self) -> str:
|
|
45
|
+
lines = [
|
|
46
|
+
f"Impact analysis for: {self.target}",
|
|
47
|
+
f"Total affected files: {self.total_affected}",
|
|
48
|
+
"",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if self.directly_affected:
|
|
52
|
+
lines.append("Directly affected (import this file):")
|
|
53
|
+
for f in self.directly_affected:
|
|
54
|
+
marker = " [test]" if f.is_test_file else ""
|
|
55
|
+
lines.append(f" {f.path}{marker}")
|
|
56
|
+
lines.append("")
|
|
57
|
+
|
|
58
|
+
if self.transitively_affected:
|
|
59
|
+
lines.append("Transitively affected (depend on a direct dependent):")
|
|
60
|
+
for f in self.transitively_affected:
|
|
61
|
+
marker = " [test]" if f.is_test_file else ""
|
|
62
|
+
lines.append(f" {f.path} (via {f.via}, distance {f.distance}){marker}")
|
|
63
|
+
lines.append("")
|
|
64
|
+
|
|
65
|
+
if self.test_files:
|
|
66
|
+
lines.append("Suggested tests to run:")
|
|
67
|
+
for t in self.test_files:
|
|
68
|
+
lines.append(f" {t}")
|
|
69
|
+
else:
|
|
70
|
+
lines.append("No test files found in the impact radius.")
|
|
71
|
+
|
|
72
|
+
return "\n".join(lines)
|
|
73
|
+
|
|
74
|
+
def to_tree_string(self) -> str:
|
|
75
|
+
"""Render as an ASCII dependency tree, similar to `tree` output."""
|
|
76
|
+
lines = [self.target]
|
|
77
|
+
direct = self.directly_affected
|
|
78
|
+
for i, f in enumerate(direct):
|
|
79
|
+
is_last_direct = (i == len(direct) - 1)
|
|
80
|
+
connector = "└──" if is_last_direct else "├──"
|
|
81
|
+
marker = " [test]" if f.is_test_file else ""
|
|
82
|
+
lines.append(f"{connector} {f.path}{marker}")
|
|
83
|
+
|
|
84
|
+
# Find transitive files that came via this direct file
|
|
85
|
+
children = [t for t in self.transitively_affected if t.via == f.path]
|
|
86
|
+
for j, child in enumerate(children):
|
|
87
|
+
is_last_child = (j == len(children) - 1)
|
|
88
|
+
prefix = " " if is_last_direct else "│ "
|
|
89
|
+
child_connector = "└──" if is_last_child else "├──"
|
|
90
|
+
child_marker = " [test]" if child.is_test_file else ""
|
|
91
|
+
lines.append(f"{prefix}{child_connector} {child.path}{child_marker}")
|
|
92
|
+
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_test_file(path: str) -> bool:
|
|
97
|
+
lowered = path.lower()
|
|
98
|
+
return any(marker in lowered for marker in TEST_MARKERS)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def analyze_impact(
|
|
102
|
+
target_file: str,
|
|
103
|
+
repo_graph: RepoGraph,
|
|
104
|
+
max_depth: int = 5,
|
|
105
|
+
) -> ImpactReport:
|
|
106
|
+
"""
|
|
107
|
+
Find every file affected by a change to target_file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
target_file: Relative path of the file being changed (must exist in repo_graph.nodes).
|
|
111
|
+
repo_graph: Built graph from build_graph().
|
|
112
|
+
max_depth: Maximum number of hops to traverse upstream (default 5).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
ImpactReport listing direct and transitive dependents.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If target_file is not found in the graph.
|
|
119
|
+
"""
|
|
120
|
+
if target_file not in repo_graph.nodes:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"File not found in repository graph: {target_file}. "
|
|
123
|
+
f"Make sure the path is relative to the repo root."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
report = ImpactReport(target=target_file)
|
|
127
|
+
|
|
128
|
+
visited: set[str] = {target_file}
|
|
129
|
+
direct_dependents = repo_graph.get_dependents(target_file)
|
|
130
|
+
|
|
131
|
+
# --- Distance 1: direct dependents ---
|
|
132
|
+
frontier: list[tuple[str, str]] = [] # (path, via)
|
|
133
|
+
for dep in direct_dependents:
|
|
134
|
+
if dep in visited:
|
|
135
|
+
continue
|
|
136
|
+
visited.add(dep)
|
|
137
|
+
is_test = _is_test_file(dep)
|
|
138
|
+
report.directly_affected.append(
|
|
139
|
+
ImpactedFile(path=dep, distance=1, is_test_file=is_test, via=target_file)
|
|
140
|
+
)
|
|
141
|
+
if is_test:
|
|
142
|
+
report.test_files.append(dep)
|
|
143
|
+
frontier.append((dep, dep))
|
|
144
|
+
|
|
145
|
+
# --- Distance 2+: transitive dependents (BFS upstream) ---
|
|
146
|
+
distance = 2
|
|
147
|
+
while frontier and distance <= max_depth:
|
|
148
|
+
next_frontier: list[tuple[str, str]] = []
|
|
149
|
+
|
|
150
|
+
for current_path, origin_direct_file in frontier:
|
|
151
|
+
for dep in repo_graph.get_dependents(current_path):
|
|
152
|
+
if dep in visited:
|
|
153
|
+
continue
|
|
154
|
+
visited.add(dep)
|
|
155
|
+
is_test = _is_test_file(dep)
|
|
156
|
+
report.transitively_affected.append(
|
|
157
|
+
ImpactedFile(
|
|
158
|
+
path=dep,
|
|
159
|
+
distance=distance,
|
|
160
|
+
is_test_file=is_test,
|
|
161
|
+
via=origin_direct_file, # trace back to the original direct dependent
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
if is_test:
|
|
165
|
+
report.test_files.append(dep)
|
|
166
|
+
next_frontier.append((dep, origin_direct_file))
|
|
167
|
+
|
|
168
|
+
frontier = next_frontier
|
|
169
|
+
distance += 1
|
|
170
|
+
|
|
171
|
+
report.total_affected = len(report.directly_affected) + len(report.transitively_affected)
|
|
172
|
+
|
|
173
|
+
return report
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
import sys
|
|
178
|
+
|
|
179
|
+
if len(sys.argv) < 3:
|
|
180
|
+
print("Usage: python impact_analysis.py <repo_path> <target_file>")
|
|
181
|
+
print("Example: python impact_analysis.py ../sample_repo types/files.ts")
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
repo_path = sys.argv[1]
|
|
185
|
+
target = sys.argv[2]
|
|
186
|
+
|
|
187
|
+
scan = scan_repo(repo_path)
|
|
188
|
+
parse = parse_imports(scan)
|
|
189
|
+
repo_graph = build_graph(scan, parse)
|
|
190
|
+
|
|
191
|
+
report = analyze_impact(target, repo_graph)
|
|
192
|
+
|
|
193
|
+
print(report.summary())
|
|
194
|
+
print()
|
|
195
|
+
print("Dependency tree:")
|
|
196
|
+
print(report.to_tree_string())
|
package/python/mcp_server.py
CHANGED
|
@@ -11,8 +11,9 @@ Usage (the IDE does this for you, but you can test manually):
|
|
|
11
11
|
python mcp_server.py
|
|
12
12
|
|
|
13
13
|
Tools exposed:
|
|
14
|
-
query_repo
|
|
15
|
-
scan_repo
|
|
14
|
+
query_repo — rank files by relevance to a natural-language query
|
|
15
|
+
scan_repo — list all source files the engine can see in a repo
|
|
16
|
+
analyze_impact — find every file affected by changing a target file
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
import asyncio
|
|
@@ -32,6 +33,7 @@ from import_parser import parse_imports
|
|
|
32
33
|
from graph_builder import build_graph
|
|
33
34
|
from query_engine import query as engine_query
|
|
34
35
|
from main import _confidence, _reasoning
|
|
36
|
+
from impact_analysis import analyze_impact
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
# ---------------------------------------------------------------------------
|
|
@@ -100,6 +102,42 @@ async def list_tools() -> list[types.Tool]:
|
|
|
100
102
|
"required": ["repo_path"],
|
|
101
103
|
},
|
|
102
104
|
),
|
|
105
|
+
types.Tool(
|
|
106
|
+
name="analyze_impact",
|
|
107
|
+
description=(
|
|
108
|
+
"Find every file affected by changing a specific file. "
|
|
109
|
+
"Walks the dependency graph upstream to find direct and transitive "
|
|
110
|
+
"dependents — files that import the target file, and files that import "
|
|
111
|
+
"those files. Flags likely test files so you know what to re-test. "
|
|
112
|
+
"Use this BEFORE modifying a shared file (types, utils, config) to "
|
|
113
|
+
"understand the blast radius of the change."
|
|
114
|
+
),
|
|
115
|
+
inputSchema={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"repo_path": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Absolute or relative path to the repository root.",
|
|
121
|
+
},
|
|
122
|
+
"target_file": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": (
|
|
125
|
+
"Path of the file being changed, relative to repo_path. "
|
|
126
|
+
"Example: 'src/types/index.ts' or 'lib/api.ts'. "
|
|
127
|
+
"Use scan_repo first if unsure of the exact path."
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
"max_depth": {
|
|
131
|
+
"type": "integer",
|
|
132
|
+
"description": "Maximum hops to traverse upstream (default: 5).",
|
|
133
|
+
"default": 5,
|
|
134
|
+
"minimum": 1,
|
|
135
|
+
"maximum": 10,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"required": ["repo_path", "target_file"],
|
|
139
|
+
},
|
|
140
|
+
),
|
|
103
141
|
]
|
|
104
142
|
|
|
105
143
|
|
|
@@ -115,6 +153,9 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
|
115
153
|
if name == "scan_repo":
|
|
116
154
|
return await _handle_scan(arguments)
|
|
117
155
|
|
|
156
|
+
if name == "analyze_impact":
|
|
157
|
+
return await _handle_impact(arguments)
|
|
158
|
+
|
|
118
159
|
return [types.TextContent(
|
|
119
160
|
type="text",
|
|
120
161
|
text=json.dumps({"error": f"Unknown tool: {name}"}),
|
|
@@ -185,6 +226,48 @@ def _run_scan(repo_path: str) -> dict:
|
|
|
185
226
|
}
|
|
186
227
|
|
|
187
228
|
|
|
229
|
+
async def _handle_impact(args: dict) -> list[types.TextContent]:
|
|
230
|
+
repo_path = args.get("repo_path", "")
|
|
231
|
+
target_file = args.get("target_file", "")
|
|
232
|
+
max_depth = min(int(args.get("max_depth", 5)), 10)
|
|
233
|
+
|
|
234
|
+
loop = asyncio.get_event_loop()
|
|
235
|
+
try:
|
|
236
|
+
result = await loop.run_in_executor(None, _run_impact, repo_path, target_file, max_depth)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
result = {"error": str(e), "repo": repo_path, "target_file": target_file}
|
|
239
|
+
|
|
240
|
+
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _run_impact(repo_path: str, target_file: str, max_depth: int) -> dict:
|
|
244
|
+
"""Synchronous pipeline — called from a thread executor."""
|
|
245
|
+
scan = scan_repo(repo_path)
|
|
246
|
+
parse = parse_imports(scan)
|
|
247
|
+
repo_graph = build_graph(scan, parse)
|
|
248
|
+
report = analyze_impact(target_file, repo_graph, max_depth=max_depth)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"target_file": report.target,
|
|
252
|
+
"total_affected": report.total_affected,
|
|
253
|
+
"directly_affected": [
|
|
254
|
+
{"path": f.path, "is_test_file": f.is_test_file}
|
|
255
|
+
for f in report.directly_affected
|
|
256
|
+
],
|
|
257
|
+
"transitively_affected": [
|
|
258
|
+
{
|
|
259
|
+
"path": f.path,
|
|
260
|
+
"distance": f.distance,
|
|
261
|
+
"via": f.via,
|
|
262
|
+
"is_test_file": f.is_test_file,
|
|
263
|
+
}
|
|
264
|
+
for f in report.transitively_affected
|
|
265
|
+
],
|
|
266
|
+
"suggested_tests": report.test_files,
|
|
267
|
+
"dependency_tree": report.to_tree_string(),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
188
271
|
# ---------------------------------------------------------------------------
|
|
189
272
|
# Entry point
|
|
190
273
|
# ---------------------------------------------------------------------------
|