patchwork-conventions 0.1.1__tar.gz → 0.1.3__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.1 → patchwork_conventions-0.1.3}/PKG-INFO +30 -4
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/README.md +29 -3
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/pyproject.toml +1 -1
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/cli.py +6 -6
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/mcp/server.py +37 -5
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/api_patterns.py +35 -20
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/config_detector.py +28 -7
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/git_patterns.py +2 -2
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/PKG-INFO +30 -4
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/LICENSE +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/setup.cfg +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/__init__.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/mcp/__init__.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/__init__.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/ast_base.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/error_handling.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/imports.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/naming.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/structure.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/testing.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/output/__init__.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/output/report.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/scanner.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/requires.txt +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/tests/test_naming.py +0 -0
- {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/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.3
|
|
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.3"
|
|
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]")
|
|
@@ -36,7 +36,7 @@ def _get_or_scan(root: Path) -> ConventionReport:
|
|
|
36
36
|
cached = _CACHE.get(key)
|
|
37
37
|
if cached is not None:
|
|
38
38
|
return cached
|
|
39
|
-
opts = ScanOptions(root=root, max_files=
|
|
39
|
+
opts = ScanOptions(root=root, max_files=500)
|
|
40
40
|
report = do_scan(opts)
|
|
41
41
|
_CACHE[key] = report
|
|
42
42
|
return report
|
|
@@ -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)
|
|
@@ -305,6 +330,8 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
|
|
|
305
330
|
lines.append(f" logging: {er.logging_framework}")
|
|
306
331
|
if er.propagation_style:
|
|
307
332
|
lines.append(f" propagation: {er.propagation_style}")
|
|
333
|
+
if er.custom_exceptions:
|
|
334
|
+
lines.append(f" custom exceptions: {', '.join(er.custom_exceptions[:6])}")
|
|
308
335
|
for note in er.notes:
|
|
309
336
|
lines.append(f" note: {note}")
|
|
310
337
|
return [types.TextContent(type="text", text="\n".join(lines) or "No error patterns found.")]
|
|
@@ -360,8 +387,13 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
|
|
|
360
387
|
return [types.TextContent(type="text", text="\n".join(lines))]
|
|
361
388
|
|
|
362
389
|
elif name == "patchwork_check":
|
|
363
|
-
sym = arguments
|
|
364
|
-
kind = arguments
|
|
390
|
+
sym = arguments.get("name")
|
|
391
|
+
kind = arguments.get("kind")
|
|
392
|
+
if not sym or not kind:
|
|
393
|
+
return [types.TextContent(
|
|
394
|
+
type="text",
|
|
395
|
+
text="Error: 'name' and 'kind' are required arguments.",
|
|
396
|
+
)]
|
|
365
397
|
lang = arguments.get("language", "")
|
|
366
398
|
return [types.TextContent(
|
|
367
399
|
type="text",
|
|
@@ -391,7 +423,7 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
|
|
|
391
423
|
Route("/sse", endpoint=handle_sse),
|
|
392
424
|
Mount("/messages", app=sse.handle_post_message),
|
|
393
425
|
])
|
|
394
|
-
uvicorn.run(app, host="
|
|
426
|
+
uvicorn.run(app, host="127.0.0.1", port=port)
|
|
395
427
|
|
|
396
428
|
|
|
397
429
|
def _check_convention(name: str, kind: str, lang: str, report: ConventionReport) -> str:
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/api_patterns.py
RENAMED
|
@@ -34,10 +34,23 @@ _RESP_DATA_ERROR = re.compile(r'["\'](?:data|error)["\']', re.IGNORECASE)
|
|
|
34
34
|
_RESP_SUCCESS_DATA = re.compile(r'["\']success["\'].*["\']data["\']', re.DOTALL | re.IGNORECASE)
|
|
35
35
|
_RESP_RESULT = re.compile(r'["\']result["\']', re.IGNORECASE)
|
|
36
36
|
|
|
37
|
-
# Route parameter styles
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
# Route parameter styles — match route strings passed to known route-registration calls.
|
|
38
|
+
# We look for the route string ONLY when it appears as an argument to a route decorator
|
|
39
|
+
# or router method, to avoid matching f-strings, format strings, and other /path usages.
|
|
40
|
+
|
|
41
|
+
# FastAPI/Starlette: @app.get("/items/{item_id}") or @router.post("/x/{id:int}")
|
|
42
|
+
_ROUTE_BRACE = re.compile(
|
|
43
|
+
r'(?:@?\w+\.(?:get|post|put|patch|delete|websocket|route|add_api_route)\s*\()'
|
|
44
|
+
r'\s*[f]?["\'][^"\']*\{[a-zA-Z_]\w*(?::[a-zA-Z]+)?\}'
|
|
45
|
+
)
|
|
46
|
+
# Flask: @app.route("/items/<int:item_id>")
|
|
47
|
+
_ROUTE_ANGLE = re.compile(
|
|
48
|
+
r'(?:@?\w+\.route\s*\()\s*[f]?["\'][^"\']*<(?:[a-zA-Z_]+:)?[a-zA-Z_]\w*>'
|
|
49
|
+
)
|
|
50
|
+
# Express: router.get("/:id") or app.use("/items/:id")
|
|
51
|
+
_ROUTE_COLON = re.compile(
|
|
52
|
+
r'(?:\w+\.(?:get|post|put|patch|delete|use|all)\s*\()\s*["\'][^"\']*(?<!\{):(?!\w*\})[a-zA-Z_]\w*'
|
|
53
|
+
)
|
|
41
54
|
|
|
42
55
|
# ORM signals
|
|
43
56
|
_ORM_SIGNALS = {
|
|
@@ -57,7 +70,8 @@ _FRAMEWORK_SIGNALS = {
|
|
|
57
70
|
"FastAPI": [r"from fastapi import", r"@app\.get\(", r"@router\."],
|
|
58
71
|
"Flask": [r"from flask import", r"@app\.route\(", r"Blueprint\("],
|
|
59
72
|
"Django": [r"from django", r"urlpatterns\s*=", r"HttpResponse"],
|
|
60
|
-
|
|
73
|
+
# For Express: require both the require() AND usage — router.get alone is too generic
|
|
74
|
+
"Express": [r"require\(['\"]express['\"]\)", r"express\(\)"],
|
|
61
75
|
"Fastify": [r"require\(['\"]fastify['\"]", r"fastify\.register"],
|
|
62
76
|
"Hono": [r"from ['\"]hono['\"]", r"new Hono\("],
|
|
63
77
|
"Gin": [r"\bgin\b.*\bDefault\(\)", r"r\.GET\(", r"c\.JSON\("],
|
|
@@ -105,9 +119,10 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
105
119
|
data_error = 0
|
|
106
120
|
success_data = 0
|
|
107
121
|
result_shape = 0
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
# Track route style per FILE (not per match) to avoid single-file anomalies
|
|
123
|
+
colon_route_files = 0
|
|
124
|
+
brace_route_files = 0
|
|
125
|
+
angle_route_files = 0
|
|
111
126
|
# Track per-file hits (not per-match) to avoid false positives from
|
|
112
127
|
# code that merely mentions framework names in strings, comments, or docs.
|
|
113
128
|
orm_files: Counter[str] = Counter()
|
|
@@ -127,9 +142,9 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
127
142
|
success_data += len(_RESP_SUCCESS_DATA.findall(text))
|
|
128
143
|
result_shape += len(_RESP_RESULT.findall(text))
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
if _ROUTE_COLON.search(text): colon_route_files += 1
|
|
146
|
+
if _ROUTE_BRACE.search(text): brace_route_files += 1
|
|
147
|
+
if _ROUTE_ANGLE.search(text): angle_route_files += 1
|
|
133
148
|
|
|
134
149
|
# Skip files that are themselves pattern-definition files (e.g. this miner)
|
|
135
150
|
# to avoid self-matching on our own regex strings.
|
|
@@ -175,17 +190,17 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
|
|
|
175
190
|
elif result_shape > 3:
|
|
176
191
|
response_shape = "{result}"
|
|
177
192
|
|
|
178
|
-
# Route param style
|
|
179
|
-
|
|
193
|
+
# Route param style — pick the style seen in the most files.
|
|
194
|
+
# Require at least 2 files to avoid single-file anomalies.
|
|
180
195
|
route_style = None
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if
|
|
184
|
-
route_style = "
|
|
185
|
-
elif
|
|
186
|
-
route_style = "{id} (FastAPI style)"
|
|
187
|
-
else:
|
|
196
|
+
best = max(brace_route_files, angle_route_files, colon_route_files)
|
|
197
|
+
if best >= 2:
|
|
198
|
+
if brace_route_files == best:
|
|
199
|
+
route_style = "{id} (FastAPI/Starlette style)"
|
|
200
|
+
elif angle_route_files == best:
|
|
188
201
|
route_style = "<id> (Flask style)"
|
|
202
|
+
else:
|
|
203
|
+
route_style = ":id (Express style)"
|
|
189
204
|
|
|
190
205
|
return APIResult(
|
|
191
206
|
response_shape=response_shape,
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/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.1 → patchwork_conventions-0.1.3}/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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchwork-conventions
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/__init__.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/ast_base.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/error_handling.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/src/patchwork/miners/structure.py
RENAMED
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.1 → patchwork_conventions-0.1.3}/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
|
|
File without changes
|