patchwork-conventions 0.1.0__tar.gz → 0.1.2__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.
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/PKG-INFO +30 -4
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/README.md +29 -3
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/pyproject.toml +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/cli.py +6 -6
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/mcp/server.py +27 -2
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/api_patterns.py +41 -35
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/config_detector.py +28 -7
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/git_patterns.py +2 -2
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/imports.py +7 -4
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/output/report.py +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/PKG-INFO +30 -4
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/LICENSE +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/setup.cfg +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/mcp/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/ast_base.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/error_handling.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/naming.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/structure.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/testing.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/output/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/scanner.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/requires.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/tests/test_naming.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchwork-conventions
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
|
|
5
5
|
Author: patchwork contributors
|
|
6
6
|
License: MIT
|
|
@@ -228,20 +228,46 @@ patchwork scan --claude-md
|
|
|
228
228
|
|
|
229
229
|
### Option 3: MCP server
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
**Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
|
|
232
232
|
|
|
233
233
|
```json
|
|
234
234
|
{
|
|
235
235
|
"mcpServers": {
|
|
236
236
|
"patchwork": {
|
|
237
237
|
"command": "patchwork",
|
|
238
|
-
"args": ["serve", "
|
|
238
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"mcpServers": {
|
|
249
|
+
"patchwork": {
|
|
250
|
+
"command": "patchwork",
|
|
251
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Cursor** — add to `.cursor/mcp.json` in your project root:
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"mcpServers": {
|
|
262
|
+
"patchwork": {
|
|
263
|
+
"command": "patchwork",
|
|
264
|
+
"args": ["serve", ".", "--stdio"]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Then your AI agent can use 8 on-demand tools:
|
|
245
271
|
|
|
246
272
|
| Tool | When to use |
|
|
247
273
|
|---|---|
|
|
@@ -183,20 +183,46 @@ patchwork scan --claude-md
|
|
|
183
183
|
|
|
184
184
|
### Option 3: MCP server
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
**Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
|
|
187
187
|
|
|
188
188
|
```json
|
|
189
189
|
{
|
|
190
190
|
"mcpServers": {
|
|
191
191
|
"patchwork": {
|
|
192
192
|
"command": "patchwork",
|
|
193
|
-
"args": ["serve", "
|
|
193
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"mcpServers": {
|
|
204
|
+
"patchwork": {
|
|
205
|
+
"command": "patchwork",
|
|
206
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Cursor** — add to `.cursor/mcp.json` in your project root:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"mcpServers": {
|
|
217
|
+
"patchwork": {
|
|
218
|
+
"command": "patchwork",
|
|
219
|
+
"args": ["serve", ".", "--stdio"]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Then your AI agent can use 8 on-demand tools:
|
|
200
226
|
|
|
201
227
|
| Tool | When to use |
|
|
202
228
|
|---|---|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "patchwork-conventions"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -111,7 +111,7 @@ def scan(
|
|
|
111
111
|
|
|
112
112
|
if claude_md and out_path.exists():
|
|
113
113
|
# Append patchwork section to existing CLAUDE.md
|
|
114
|
-
existing = out_path.read_text()
|
|
114
|
+
existing = out_path.read_text(encoding="utf-8")
|
|
115
115
|
marker = "<!-- patchwork:start -->"
|
|
116
116
|
end_marker = "<!-- patchwork:end -->"
|
|
117
117
|
if marker in existing:
|
|
@@ -126,7 +126,7 @@ def scan(
|
|
|
126
126
|
else:
|
|
127
127
|
text = existing.rstrip() + f"\n\n{marker}\n{text}\n{end_marker}\n"
|
|
128
128
|
|
|
129
|
-
out_path.write_text(text)
|
|
129
|
+
out_path.write_text(text, encoding="utf-8")
|
|
130
130
|
|
|
131
131
|
if not quiet:
|
|
132
132
|
_print_summary(report, out_path)
|
|
@@ -145,7 +145,7 @@ def update(path: str, output: str | None) -> None:
|
|
|
145
145
|
# Load any existing manual annotations
|
|
146
146
|
manual_sections: dict[str, str] = {}
|
|
147
147
|
if out_path.exists():
|
|
148
|
-
manual_sections = _extract_manual_sections(out_path.read_text())
|
|
148
|
+
manual_sections = _extract_manual_sections(out_path.read_text(encoding="utf-8"))
|
|
149
149
|
|
|
150
150
|
opts = ScanOptions(root=root)
|
|
151
151
|
with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
|
|
@@ -158,7 +158,7 @@ def update(path: str, output: str | None) -> None:
|
|
|
158
158
|
for heading, content in manual_sections.items():
|
|
159
159
|
text += f"\n\n## {heading} (manual)\n\n{content}"
|
|
160
160
|
|
|
161
|
-
out_path.write_text(text)
|
|
161
|
+
out_path.write_text(text, encoding="utf-8")
|
|
162
162
|
console.print(f"[green]✓[/green] Updated [bold]{out_path}[/bold]")
|
|
163
163
|
if manual_sections:
|
|
164
164
|
console.print(f" Preserved {len(manual_sections)} manual section(s)")
|
|
@@ -182,7 +182,7 @@ def diff(path: str) -> None:
|
|
|
182
182
|
console.print("[yellow]No existing CONVENTIONS.md — run `patchwork scan` first[/yellow]")
|
|
183
183
|
sys.exit(1)
|
|
184
184
|
|
|
185
|
-
old_text = out_path.read_text()
|
|
185
|
+
old_text = out_path.read_text(encoding="utf-8")
|
|
186
186
|
diffs = list(difflib.unified_diff(
|
|
187
187
|
old_text.splitlines(),
|
|
188
188
|
new_text.splitlines(),
|
|
@@ -241,7 +241,7 @@ def watch(path: str, interval: float) -> None:
|
|
|
241
241
|
if changed:
|
|
242
242
|
last_mtime = current
|
|
243
243
|
report = do_scan(opts)
|
|
244
|
-
out_path.write_text(report.to_markdown())
|
|
244
|
+
out_path.write_text(report.to_markdown(), encoding="utf-8")
|
|
245
245
|
console.print(f"[green]↺[/green] CONVENTIONS.md updated")
|
|
246
246
|
except KeyboardInterrupt:
|
|
247
247
|
console.print("\n[dim]Stopped[/dim]")
|
|
@@ -189,9 +189,34 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
|
|
|
189
189
|
),
|
|
190
190
|
]
|
|
191
191
|
|
|
192
|
+
def _resolve_scan_root(raw_path: str | None) -> Path:
|
|
193
|
+
"""Resolve and validate the scan path.
|
|
194
|
+
|
|
195
|
+
Rejects paths that escape the filesystem root or point to sensitive
|
|
196
|
+
system directories, and normalises symlinks so containment checks
|
|
197
|
+
are reliable. The MCP server is a read-only tool, but we still
|
|
198
|
+
validate inputs at the boundary to follow least-privilege.
|
|
199
|
+
"""
|
|
200
|
+
candidate = Path(raw_path).resolve() if raw_path else root.resolve()
|
|
201
|
+
|
|
202
|
+
# Reject obviously dangerous system paths
|
|
203
|
+
_BLOCKED = {"/etc", "/proc", "/sys", "/dev", "/private/etc"}
|
|
204
|
+
for blocked in _BLOCKED:
|
|
205
|
+
if str(candidate).startswith(blocked):
|
|
206
|
+
raise ValueError(f"Scanning system path '{candidate}' is not allowed.")
|
|
207
|
+
|
|
208
|
+
# Must be an existing directory (not a file or a socket etc.)
|
|
209
|
+
if not candidate.is_dir():
|
|
210
|
+
raise ValueError(f"Path '{candidate}' is not a directory.")
|
|
211
|
+
|
|
212
|
+
return candidate
|
|
213
|
+
|
|
192
214
|
@server.call_tool()
|
|
193
215
|
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
194
|
-
|
|
216
|
+
try:
|
|
217
|
+
scan_root = _resolve_scan_root(arguments.get("path"))
|
|
218
|
+
except ValueError as exc:
|
|
219
|
+
return [types.TextContent(type="text", text=f"Error: {exc}")]
|
|
195
220
|
|
|
196
221
|
if arguments.get("refresh"):
|
|
197
222
|
_invalidate(scan_root)
|
|
@@ -391,7 +416,7 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
|
|
|
391
416
|
Route("/sse", endpoint=handle_sse),
|
|
392
417
|
Mount("/messages", app=sse.handle_post_message),
|
|
393
418
|
])
|
|
394
|
-
uvicorn.run(app, host="
|
|
419
|
+
uvicorn.run(app, host="127.0.0.1", port=port)
|
|
395
420
|
|
|
396
421
|
|
|
397
422
|
def _check_convention(name: str, kind: str, lang: str, report: ConventionReport) -> str:
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/api_patterns.py
RENAMED
|
@@ -108,12 +108,14 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
108
108
|
colon_routes = 0
|
|
109
109
|
brace_routes = 0
|
|
110
110
|
angle_routes = 0
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
# Track per-file hits (not per-match) to avoid false positives from
|
|
112
|
+
# code that merely mentions framework names in strings, comments, or docs.
|
|
113
|
+
orm_files: Counter[str] = Counter()
|
|
114
|
+
fw_files: Counter[str] = Counter()
|
|
113
115
|
async_counts: Counter[str] = Counter()
|
|
114
116
|
http_client_counts: Counter[str] = Counter()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
gql_files = 0
|
|
118
|
+
grpc_files = 0
|
|
117
119
|
|
|
118
120
|
for path in paths[:200]:
|
|
119
121
|
try:
|
|
@@ -129,32 +131,40 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
129
131
|
brace_routes += len(_ROUTE_BRACE.findall(text))
|
|
130
132
|
angle_routes += len(_ROUTE_ANGLE.findall(text))
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
# Skip files that are themselves pattern-definition files (e.g. this miner)
|
|
135
|
+
# to avoid self-matching on our own regex strings.
|
|
136
|
+
if "_ORM_SIGNALS" in text or "_FRAMEWORK_SIGNALS" in text:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Count each ORM/framework once per file (not per pattern match)
|
|
140
|
+
for orm_name, patterns in _ORM_SIGNALS.items():
|
|
141
|
+
if any(re.search(pat, text) for pat in patterns):
|
|
142
|
+
orm_files[orm_name] += 1
|
|
137
143
|
|
|
138
144
|
for fw, patterns in _FRAMEWORK_SIGNALS.items():
|
|
139
|
-
for pat in patterns:
|
|
140
|
-
|
|
141
|
-
fw_counts[fw] += 1
|
|
142
|
-
break
|
|
145
|
+
if any(re.search(pat, text) for pat in patterns):
|
|
146
|
+
fw_files[fw] += 1
|
|
143
147
|
|
|
144
148
|
for style, patterns in _ASYNC_SIGNALS.get(lang, {}).items():
|
|
145
|
-
for pat in patterns:
|
|
146
|
-
|
|
147
|
-
async_counts[style] += 1
|
|
148
|
-
break
|
|
149
|
+
if any(re.search(pat, text) for pat in patterns):
|
|
150
|
+
async_counts[style] += 1
|
|
149
151
|
|
|
150
152
|
for client in _HTTP_CLIENTS.get(lang, []):
|
|
151
|
-
|
|
153
|
+
# Match actual import/usage, not just the string appearing in comments or dicts
|
|
154
|
+
if re.search(rf'(?:import\s+{re.escape(client)}|from\s+{re.escape(client)}\s|{re.escape(client)}\.)', text):
|
|
152
155
|
http_client_counts[client] += 1
|
|
153
156
|
|
|
154
|
-
if
|
|
155
|
-
|
|
156
|
-
if
|
|
157
|
-
|
|
157
|
+
if re.search(r'\bgraphql\b|gql`', text, re.IGNORECASE):
|
|
158
|
+
gql_files += 1
|
|
159
|
+
if re.search(r'\bgrpc\b|\bprotobuf\b|\.proto["\']', text, re.IGNORECASE):
|
|
160
|
+
grpc_files += 1
|
|
161
|
+
|
|
162
|
+
# Require signal in ≥3 files to filter out false positives.
|
|
163
|
+
# Single or double-file mentions are often: config files, the tool's own
|
|
164
|
+
# pattern dictionaries, test fixtures, or README-like docstrings.
|
|
165
|
+
min_files = 3
|
|
166
|
+
orm_files = Counter({k: v for k, v in orm_files.items() if v >= min_files})
|
|
167
|
+
fw_files = Counter({k: v for k, v in fw_files.items() if v >= min_files})
|
|
158
168
|
|
|
159
169
|
# Response shape
|
|
160
170
|
response_shape = None
|
|
@@ -169,27 +179,23 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
169
179
|
route_total = colon_routes + brace_routes + angle_routes
|
|
170
180
|
route_style = None
|
|
171
181
|
if route_total > 0:
|
|
172
|
-
|
|
182
|
+
best = max(colon_routes, brace_routes, angle_routes)
|
|
183
|
+
if colon_routes == best:
|
|
173
184
|
route_style = ":id (Express style)"
|
|
174
|
-
elif brace_routes ==
|
|
185
|
+
elif brace_routes == best:
|
|
175
186
|
route_style = "{id} (FastAPI style)"
|
|
176
187
|
else:
|
|
177
188
|
route_style = "<id> (Flask style)"
|
|
178
189
|
|
|
179
|
-
async_pattern = async_counts.most_common(1)[0][0] if async_counts else None
|
|
180
|
-
orm = orm_counts.most_common(1)[0][0] if orm_counts else None
|
|
181
|
-
http_client = http_client_counts.most_common(1)[0][0] if http_client_counts else None
|
|
182
|
-
api_frameworks = [fw for fw, _ in fw_counts.most_common(3)]
|
|
183
|
-
|
|
184
190
|
return APIResult(
|
|
185
191
|
response_shape=response_shape,
|
|
186
192
|
route_param_style=route_style,
|
|
187
|
-
async_pattern=
|
|
188
|
-
orm=
|
|
189
|
-
has_graphql=
|
|
190
|
-
has_grpc=
|
|
191
|
-
api_frameworks=
|
|
192
|
-
http_client=
|
|
193
|
+
async_pattern=async_counts.most_common(1)[0][0] if async_counts else None,
|
|
194
|
+
orm=orm_files.most_common(1)[0][0] if orm_files else None,
|
|
195
|
+
has_graphql=gql_files >= 3,
|
|
196
|
+
has_grpc=grpc_files >= 3,
|
|
197
|
+
api_frameworks=[fw for fw, _ in fw_files.most_common(3)],
|
|
198
|
+
http_client=http_client_counts.most_common(1)[0][0] if http_client_counts else None,
|
|
193
199
|
)
|
|
194
200
|
|
|
195
201
|
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/config_detector.py
RENAMED
|
@@ -17,6 +17,19 @@ except ImportError:
|
|
|
17
17
|
except ImportError:
|
|
18
18
|
tomllib = None # type: ignore
|
|
19
19
|
|
|
20
|
+
# Hard limit on config file size to prevent DoS from pathologically large files
|
|
21
|
+
_MAX_CONFIG_BYTES = 1 * 1024 * 1024 # 1 MB
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_read(path: Path) -> str | None:
|
|
25
|
+
"""Read a config file, returning None if it exceeds the size limit."""
|
|
26
|
+
try:
|
|
27
|
+
if path.stat().st_size > _MAX_CONFIG_BYTES:
|
|
28
|
+
return None
|
|
29
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
30
|
+
except OSError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
20
33
|
|
|
21
34
|
@dataclass
|
|
22
35
|
class ProjectConfig:
|
|
@@ -129,9 +142,12 @@ class ConfigDetector:
|
|
|
129
142
|
return cfg
|
|
130
143
|
|
|
131
144
|
def _read_package_json(self, path: Path, cfg: ProjectConfig) -> None:
|
|
145
|
+
text = _safe_read(path)
|
|
146
|
+
if text is None:
|
|
147
|
+
return
|
|
132
148
|
try:
|
|
133
|
-
data = json.loads(
|
|
134
|
-
except
|
|
149
|
+
data = json.loads(text)
|
|
150
|
+
except json.JSONDecodeError:
|
|
135
151
|
return
|
|
136
152
|
|
|
137
153
|
cfg.language = "javascript/typescript"
|
|
@@ -179,8 +195,11 @@ class ConfigDetector:
|
|
|
179
195
|
cfg.language = "python"
|
|
180
196
|
if tomllib is None:
|
|
181
197
|
return
|
|
198
|
+
text = _safe_read(path)
|
|
199
|
+
if text is None:
|
|
200
|
+
return
|
|
182
201
|
try:
|
|
183
|
-
data = tomllib.loads(
|
|
202
|
+
data = tomllib.loads(text)
|
|
184
203
|
except Exception:
|
|
185
204
|
return
|
|
186
205
|
|
|
@@ -225,9 +244,8 @@ class ConfigDetector:
|
|
|
225
244
|
def _read_go_mod(self, path: Path, cfg: ProjectConfig) -> None:
|
|
226
245
|
cfg.language = "go"
|
|
227
246
|
cfg.package_manager = "go"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
except OSError:
|
|
247
|
+
text = _safe_read(path)
|
|
248
|
+
if text is None:
|
|
231
249
|
return
|
|
232
250
|
m = re.search(r'^module\s+(\S+)', text, re.MULTILINE)
|
|
233
251
|
if m:
|
|
@@ -253,8 +271,11 @@ class ConfigDetector:
|
|
|
253
271
|
cfg.package_manager = "cargo"
|
|
254
272
|
if tomllib is None:
|
|
255
273
|
return
|
|
274
|
+
text = _safe_read(path)
|
|
275
|
+
if text is None:
|
|
276
|
+
return
|
|
256
277
|
try:
|
|
257
|
-
data = tomllib.loads(
|
|
278
|
+
data = tomllib.loads(text)
|
|
258
279
|
except Exception:
|
|
259
280
|
return
|
|
260
281
|
pkg = data.get("package", {})
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/git_patterns.py
RENAMED
|
@@ -9,7 +9,7 @@ GitPatternMiner — Mines git history for workflow conventions:
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import re
|
|
12
|
-
import subprocess
|
|
12
|
+
import subprocess # nosec B404 — used only for 'git' with a hardcoded arg list, no shell=True
|
|
13
13
|
from collections import Counter, defaultdict
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
15
|
from pathlib import Path
|
|
@@ -40,7 +40,7 @@ _BRANCH_FIX = re.compile(r'^(fix|hotfix|bugfix)/[\w/-]+$')
|
|
|
40
40
|
|
|
41
41
|
def _run_git(args: list[str], cwd: Path, max_bytes: int = 500_000) -> str:
|
|
42
42
|
try:
|
|
43
|
-
result = subprocess.run(
|
|
43
|
+
result = subprocess.run( # nosec B603 — list args, no shell=True, cwd is a validated Path
|
|
44
44
|
["git"] + args,
|
|
45
45
|
cwd=str(cwd),
|
|
46
46
|
capture_output=True,
|
|
@@ -29,7 +29,7 @@ class ImportResult:
|
|
|
29
29
|
|
|
30
30
|
_PY_RELATIVE = re.compile(r'^\s*from\s+\.', re.MULTILINE)
|
|
31
31
|
_PY_ABSOLUTE = re.compile(r'^\s*(?:import|from)\s+(?!\.)', re.MULTILINE)
|
|
32
|
-
_PY_IMPORT = re.compile(r'^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w
|
|
32
|
+
_PY_IMPORT = re.compile(r'^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w]+(?:\s*,\s*[\w]+)*))', re.MULTILINE)
|
|
33
33
|
|
|
34
34
|
_JS_RELATIVE = re.compile(r"""(?:import|require)\s*\(?['"](\./|\.\./)""", re.MULTILINE)
|
|
35
35
|
_JS_ABSOLUTE_ALIAS = re.compile(r"""(?:import|require)\s*\(?['"](@\w+/|~/)""", re.MULTILINE)
|
|
@@ -60,9 +60,12 @@ def _detect_py_imports(paths: list[Path]) -> ImportResult:
|
|
|
60
60
|
relative_count += len(_PY_RELATIVE.findall(text))
|
|
61
61
|
absolute_count += len(_PY_ABSOLUTE.findall(text))
|
|
62
62
|
for m in _PY_IMPORT.finditer(text):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
raw = (m.group(1) or m.group(2) or "").strip()
|
|
64
|
+
# group(1) is a dotted module path; group(2) may be 'os, sys, re'
|
|
65
|
+
for part in raw.split(","):
|
|
66
|
+
mod = part.strip().split(".")[0]
|
|
67
|
+
if mod and mod.isidentifier():
|
|
68
|
+
all_modules.append(mod)
|
|
66
69
|
# Detect src/ or similar path aliases in pyproject/setup.cfg
|
|
67
70
|
if "@" in text or "from src." in text:
|
|
68
71
|
aliases.add("src/")
|
|
@@ -50,7 +50,7 @@ class ConventionReport:
|
|
|
50
50
|
|
|
51
51
|
lines += [
|
|
52
52
|
f"# {filename}",
|
|
53
|
-
f"> Auto-generated by [patchwork](https://github.com/
|
|
53
|
+
f"> Auto-generated by [patchwork](https://github.com/SaiNarayana-B/patchwork) on {ts} ",
|
|
54
54
|
f"> Scanned {self.file_count} files in {self.elapsed:.1f}s",
|
|
55
55
|
f"> **Do not edit manually** — run `patchwork update` to refresh",
|
|
56
56
|
"",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchwork-conventions
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
|
|
5
5
|
Author: patchwork contributors
|
|
6
6
|
License: MIT
|
|
@@ -228,20 +228,46 @@ patchwork scan --claude-md
|
|
|
228
228
|
|
|
229
229
|
### Option 3: MCP server
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
**Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
|
|
232
232
|
|
|
233
233
|
```json
|
|
234
234
|
{
|
|
235
235
|
"mcpServers": {
|
|
236
236
|
"patchwork": {
|
|
237
237
|
"command": "patchwork",
|
|
238
|
-
"args": ["serve", "
|
|
238
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"mcpServers": {
|
|
249
|
+
"patchwork": {
|
|
250
|
+
"command": "patchwork",
|
|
251
|
+
"args": ["serve", "/path/to/your/project", "--stdio"]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Cursor** — add to `.cursor/mcp.json` in your project root:
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"mcpServers": {
|
|
262
|
+
"patchwork": {
|
|
263
|
+
"command": "patchwork",
|
|
264
|
+
"args": ["serve", ".", "--stdio"]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Then your AI agent can use 8 on-demand tools:
|
|
245
271
|
|
|
246
272
|
| Tool | When to use |
|
|
247
273
|
|---|---|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/__init__.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/ast_base.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/error_handling.py
RENAMED
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/structure.py
RENAMED
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/output/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|