patchwork-conventions 0.1.0__tar.gz → 0.1.1__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/src/patchwork_conventions.egg-info → patchwork_conventions-0.1.1}/PKG-INFO +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/pyproject.toml +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/api_patterns.py +41 -35
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/imports.py +7 -4
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/output/report.py +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1/src/patchwork_conventions.egg-info}/PKG-INFO +1 -1
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/LICENSE +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/README.md +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/setup.cfg +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/cli.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/mcp/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/mcp/server.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/ast_base.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/config_detector.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/error_handling.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/git_patterns.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/naming.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/structure.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/testing.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/output/__init__.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/scanner.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/requires.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/tests/test_naming.py +0 -0
- {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/tests/test_scanner.py +0 -0
|
@@ -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.1"
|
|
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" }
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/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
|
|
|
@@ -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
|
"",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/__init__.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/ast_base.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/config_detector.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/error_handling.py
RENAMED
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/git_patterns.py
RENAMED
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/structure.py
RENAMED
|
File without changes
|
|
File without changes
|
{patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/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
|