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.
Files changed (30) hide show
  1. {patchwork_conventions-0.1.0/src/patchwork_conventions.egg-info → patchwork_conventions-0.1.1}/PKG-INFO +1 -1
  2. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/pyproject.toml +1 -1
  3. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/api_patterns.py +41 -35
  4. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/imports.py +7 -4
  5. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/output/report.py +1 -1
  6. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1/src/patchwork_conventions.egg-info}/PKG-INFO +1 -1
  7. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/LICENSE +0 -0
  8. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/README.md +0 -0
  9. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/setup.cfg +0 -0
  10. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/__init__.py +0 -0
  11. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/cli.py +0 -0
  12. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/mcp/__init__.py +0 -0
  13. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/mcp/server.py +0 -0
  14. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/__init__.py +0 -0
  15. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/ast_base.py +0 -0
  16. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/config_detector.py +0 -0
  17. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/error_handling.py +0 -0
  18. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/git_patterns.py +0 -0
  19. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/naming.py +0 -0
  20. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/structure.py +0 -0
  21. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/miners/testing.py +0 -0
  22. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/output/__init__.py +0 -0
  23. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork/scanner.py +0 -0
  24. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
  25. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
  26. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
  27. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/requires.txt +0 -0
  28. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
  29. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/tests/test_naming.py +0 -0
  30. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.1}/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.0
3
+ Version: 0.1.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "patchwork-conventions"
7
- version = "0.1.0"
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" }
@@ -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
- orm_counts: Counter[str] = Counter()
112
- fw_counts: Counter[str] = Counter()
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
- has_graphql = False
116
- has_grpc = False
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
- for orm, patterns in _ORM_SIGNALS.items():
133
- for pat in patterns:
134
- if re.search(pat, text):
135
- orm_counts[orm] += 1
136
- break
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
- if re.search(pat, text):
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
- if re.search(pat, text):
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
- if client in text:
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 "graphql" in text.lower() or "GraphQL" in text or "gql`" in text:
155
- has_graphql = True
156
- if "proto" in text.lower() or "grpc" in text.lower() or "protobuf" in text.lower():
157
- has_grpc = True
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
- if colon_routes == max(colon_routes, brace_routes, angle_routes):
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 == max(colon_routes, brace_routes, angle_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=async_pattern,
188
- orm=orm,
189
- has_graphql=has_graphql,
190
- has_grpc=has_grpc,
191
- api_frameworks=api_frameworks,
192
- http_client=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.,\s]+))', re.MULTILINE)
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
- mod = (m.group(1) or m.group(2) or "").strip().split(".")[0]
64
- if mod:
65
- all_modules.append(mod)
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/yourusername/patchwork) on {ts} ",
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.0
3
+ Version: 0.1.1
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