codeabc 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl

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.
backend/models.py CHANGED
@@ -183,6 +183,21 @@ class ErrorHandling(BaseModel):
183
183
  notes: list[str] = []
184
184
 
185
185
 
186
+ class UnusedExport(BaseModel):
187
+ path: str
188
+ name: str
189
+ kind: str # "function" | "class"
190
+ line: int
191
+ reason: str # plain-language explanation for a non-programmer
192
+
193
+
194
+ class UnusedExports(BaseModel):
195
+ total: int = 0
196
+ files_affected: int = 0
197
+ findings: list[UnusedExport] = []
198
+ notes: list[str] = []
199
+
200
+
186
201
  class ExternalService(BaseModel):
187
202
  name: str
188
203
  category: str # AI / 大模型 | 云服务 | 数据库 | 支付 | 通讯 | ...
@@ -202,6 +217,7 @@ class EnvVar(BaseModel):
202
217
  name: str
203
218
  required: bool # read via os.environ["X"] somewhere, so it must be set
204
219
  count: int = 1 # how many places read it
220
+ documented: bool = True # mentioned in .env.example / README / docs anywhere
205
221
 
206
222
 
207
223
  class EntryPoint(BaseModel):
@@ -484,6 +500,7 @@ class ProjectMeta(BaseModel):
484
500
  action_plan: ActionPlan | None = None
485
501
  doc_coverage: DocCoverage | None = None
486
502
  error_handling: ErrorHandling | None = None
503
+ unused_exports: UnusedExports | None = None
487
504
  integrations: Integrations | None = None
488
505
  release: ReleaseSummary | None = None
489
506
  contribution: ContributionGuide | None = None
@@ -614,7 +631,10 @@ class FileOutline(BaseModel):
614
631
 
615
632
 
616
633
  class GitHubRequest(BaseModel):
617
- url: str = Field(..., pattern=r"^https?://github\.com/.+/.+")
634
+ # Shape validation lives in github_clone._parse_github_url, which already
635
+ # accepts bare github.com/... links, inner-page URLs, git@ SSH and the
636
+ # owner/repo shorthand; a regex here rejected those before reaching it.
637
+ url: str = Field(..., min_length=1, description="GitHub URL or owner/repo shorthand")
618
638
 
619
639
 
620
640
  class UploadedFile(BaseModel):
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import uuid
6
6
 
7
- from fastapi import APIRouter, HTTPException
7
+ from fastapi import APIRouter, HTTPException, Request
8
8
  from fastapi.responses import Response
9
9
 
10
10
  from backend.models import (
@@ -71,6 +71,8 @@ from backend.models import (
71
71
  TechDebtFile,
72
72
  TestCoverageSummary,
73
73
  TunableSetting,
74
+ UnusedExport,
75
+ UnusedExports,
74
76
  UploadedFile,
75
77
  )
76
78
  from backend.services import (
@@ -116,6 +118,7 @@ from backend.services import (
116
118
  techdebt,
117
119
  too_many_params,
118
120
  typing_coverage,
121
+ unused_exports,
119
122
  )
120
123
  from backend.services import (
121
124
  health_score as health_score_svc,
@@ -252,6 +255,7 @@ def _content_analyses(
252
255
  """
253
256
  tech_debt = techdebt.scan_tech_debt(file_contents)
254
257
  env = envscan.scan_env_vars(file_contents)
258
+ undocumented_env = set(envscan.find_undocumented_env_vars(env, file_contents))
255
259
  entries = entrypoints.find_entry_points(file_contents)
256
260
  cli_commands = commands.find_cli_commands(file_contents)
257
261
  data_models = datamodels.find_data_models(file_contents)
@@ -266,6 +270,7 @@ def _content_analyses(
266
270
  api = apimap.scan_api_routes(file_contents)
267
271
  doc = docs.assess_doc_coverage(file_contents)
268
272
  silent = error_handling.find_swallowed_errors(file_contents)
273
+ dead = unused_exports.find_unused_exports(file_contents)
269
274
  integ = integrations.detect_external_services(file_contents)
270
275
  rel = release_map.find_release_info(file_contents)
271
276
  contrib = contributing.find_contribution_guide(file_contents)
@@ -285,7 +290,13 @@ def _content_analyses(
285
290
  TechDebtFile(path=f["path"], count=f["count"]) for f in tech_debt["files"]
286
291
  ],
287
292
  "env_vars": [
288
- EnvVar(name=v["name"], required=v["required"], count=v["count"]) for v in env["vars"]
293
+ EnvVar(
294
+ name=v["name"],
295
+ required=v["required"],
296
+ count=v["count"],
297
+ documented=v["name"] not in undocumented_env,
298
+ )
299
+ for v in env["vars"]
289
300
  ],
290
301
  "entry_points": [
291
302
  EntryPoint(path=e["path"], kind=e["kind"], command=e["command"], reason=e["reason"])
@@ -410,6 +421,12 @@ def _content_analyses(
410
421
  findings=[SilentFailure(**f) for f in silent["findings"]],
411
422
  notes=silent["notes"],
412
423
  ),
424
+ "unused_exports": UnusedExports(
425
+ total=dead["total"],
426
+ files_affected=dead["files_affected"],
427
+ findings=[UnusedExport(**f) for f in dead["findings"]],
428
+ notes=dead["notes"],
429
+ ),
413
430
  "integrations": Integrations(
414
431
  total=integ["total"],
415
432
  services=[ExternalService(**s) for s in integ["services"]],
@@ -796,15 +813,20 @@ async def get_references(project_id: str, name: str):
796
813
 
797
814
 
798
815
  @router.get("/pr-reading")
799
- async def get_pr_reading(url: str):
816
+ async def get_pr_reading(url: str, request: Request, explain: bool = False):
800
817
  """Turn a PR link into a plain-language diff walkthrough.
801
818
 
802
- Deterministic (no LLM): fetches the PR's unified diff, breaks it into
803
- per-file changes with add/del counts, classifies each file as code, test,
804
- docs, or config, and proposes a review reading order (biggest production
805
- diffs first). An LLM explanation layer can sit on this payload later.
819
+ The deterministic core (no LLM) fetches the PR's unified diff, breaks it
820
+ into per-file changes with add/del counts, classifies each file as code,
821
+ test, docs, or config, and proposes a review reading order (biggest
822
+ production diffs first). With explain=true the payload also carries
823
+ per-file plain-language explanations: LLM-written when the caller passes
824
+ an x-api-key, deterministic diff-derived notes otherwise.
806
825
  """
807
826
  try:
827
+ if explain:
828
+ api_key = request.headers.get("x-api-key")
829
+ return await pr_reading.analyze_pr_explained(url, explain=True, api_key=api_key)
808
830
  return pr_reading.analyze_pr(url)
809
831
  except ValueError as exc:
810
832
  raise HTTPException(400, str(exc)) from exc
@@ -198,6 +198,13 @@ def _too_many_params_actions(too_many_params_files: list[dict] | None) -> list[d
198
198
  return actions
199
199
 
200
200
 
201
+ def _missing_count(f: dict) -> int:
202
+ # typing_coverage emits the sample of unannotated symbol names (a list);
203
+ # older cached payloads may carry a bare count. Either way compare counts.
204
+ m = f.get("missing", 0)
205
+ return len(m) if isinstance(m, list) else m
206
+
207
+
201
208
  def _typing_actions(typing_files: list[dict] | None) -> list[dict]:
202
209
  if not typing_files:
203
210
  return []
@@ -205,11 +212,11 @@ def _typing_actions(typing_files: list[dict] | None) -> list[dict]:
205
212
  # Files with the most unannotated public symbols and low coverage first;
206
213
  # only surface genuinely under-annotated files (not a stray missing hint).
207
214
  weak = sorted(
208
- (f for f in typing_files if f.get("missing", 0) >= 3 and f.get("coverage", 1) < 0.6),
209
- key=lambda f: (-f.get("missing", 0), f.get("coverage", 1)),
215
+ (f for f in typing_files if _missing_count(f) >= 3 and f.get("coverage", 1) < 0.6),
216
+ key=lambda f: (-_missing_count(f), f.get("coverage", 1)),
210
217
  )
211
218
  for f in weak[:_MAX_PER_CATEGORY]:
212
- missing = f.get("missing", 0)
219
+ missing = _missing_count(f)
213
220
  pct = round(f.get("coverage", 0) * 100)
214
221
  actions.append(
215
222
  {
@@ -165,3 +165,47 @@ def render_env_markdown(project_name: str, env_data: dict | None) -> str:
165
165
  lines.append("")
166
166
 
167
167
  return "\n".join(lines).rstrip() + "\n"
168
+
169
+
170
+ _DOC_BASENAMES = (
171
+ ".env.example",
172
+ ".env.sample",
173
+ ".env.template",
174
+ ".env.defaults",
175
+ )
176
+
177
+
178
+ def _is_doc_candidate(path: str) -> bool:
179
+ """Whether a file is a place a newcomer would look for env configuration."""
180
+ lower = path.lower()
181
+ base = lower.rsplit("/", 1)[-1]
182
+ if base in _DOC_BASENAMES or base.startswith("readme"):
183
+ return True
184
+ if base.endswith(".md") and (
185
+ "/" not in lower or lower.startswith(("docs/", "doc/", ".github/"))
186
+ ):
187
+ return True
188
+ return False
189
+
190
+
191
+ def find_undocumented_env_vars(scan: dict, file_contents: dict[str, str]) -> list[str]:
192
+ """Names of env vars that no documentation file mentions.
193
+
194
+ ``.env.example`` and the README are where a newcomer learns what to
195
+ configure; a variable the code reads but nobody writes down is a setup
196
+ landmine, doubly so when it is required. Matching is a whole-word search
197
+ over every documentation candidate, so ``HOST`` does not count as
198
+ documented just because ``DB_HOST`` appears. Pure over the scan result and
199
+ the file texts, so it is unit-testable with plain dicts.
200
+ """
201
+ doc_text = "\n".join(
202
+ content for path, content in file_contents.items() if _is_doc_candidate(path)
203
+ )
204
+ undocumented = []
205
+ for var in scan.get("vars", []):
206
+ name = var.get("name")
207
+ if not name:
208
+ continue
209
+ if not re.search(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", doc_text):
210
+ undocumented.append(name)
211
+ return undocumented
@@ -7,6 +7,7 @@ order. An LLM explanation layer can sit on top of this payload later.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import asyncio
10
11
  import json
11
12
  import re
12
13
  import subprocess
@@ -179,3 +180,131 @@ def render_markdown(analysis: dict) -> str:
179
180
 
180
181
  def to_json(analysis: dict) -> str:
181
182
  return json.dumps(analysis, ensure_ascii=False, indent=2)
183
+
184
+
185
+ _EXPLAIN_MAX_FILES = 8
186
+ _DIFF_EXCERPT_MAX_LINES = 120
187
+
188
+ _SYMBOL_RE = re.compile(
189
+ r"^\+\s*(?:async\s+def|def|class|function|export\s+(?:async\s+)?function|export\s+class)\s+([A-Za-z_][\w]*)"
190
+ )
191
+
192
+
193
+ def _diff_sections(diff_text: str) -> dict[str, list[str]]:
194
+ """Map each file path to its raw diff lines (headers + hunks)."""
195
+ sections: dict[str, list[str]] = {}
196
+ current: str | None = None
197
+ for line in diff_text.splitlines():
198
+ if line.startswith("diff --git "):
199
+ match = re.match(r"diff --git a/(\S+) b/(\S+)", line)
200
+ current = match.group(2) if match else None
201
+ if current is not None:
202
+ sections[current] = [line]
203
+ elif current is not None:
204
+ sections[current].append(line)
205
+ return sections
206
+
207
+
208
+ def _symbols_from_excerpt(excerpt_lines: list[str]) -> list[str]:
209
+ names: list[str] = []
210
+ for line in excerpt_lines:
211
+ m = _SYMBOL_RE.match(line)
212
+ if m and m.group(1) not in names:
213
+ names.append(m.group(1))
214
+ return names
215
+
216
+
217
+ def _deterministic_note(change: FileChange, excerpt_lines: list[str]) -> str:
218
+ """Plain-language note without any LLM: what kind of file it is, its size, and the
219
+ functions/classes it adds or updates (from added definition lines in the diff)."""
220
+ symbols = _symbols_from_excerpt(excerpt_lines)
221
+ base = {
222
+ "code": "production code",
223
+ "test": "test",
224
+ "docs": "documentation",
225
+ "config": "configuration",
226
+ }.get(change.change_type, "other")
227
+ size = f"+{change.added} / -{change.deleted} lines"
228
+ if symbols:
229
+ return f"This {base} file ({size}) adds or updates: {', '.join(symbols[:6])}."
230
+ return f"This {base} file ({size}) is edited; open it to see the exact changes."
231
+
232
+
233
+ def _excerpt(lines: list[str]) -> str:
234
+ if len(lines) <= _DIFF_EXCERPT_MAX_LINES:
235
+ return "\n".join(lines)
236
+ head = "\n".join(lines[:_DIFF_EXCERPT_MAX_LINES])
237
+ return head + f"\n... ({len(lines) - _DIFF_EXCERPT_MAX_LINES} more diff lines trimmed)"
238
+
239
+
240
+ async def explain_files(
241
+ files: list[FileChange],
242
+ diff_text: str,
243
+ *,
244
+ api_key: str | None = None,
245
+ model: str | None = None,
246
+ ) -> dict[str, str]:
247
+ """Plain-language note per file. LLM-backed when a key is configured; otherwise a
248
+ deterministic note built from the diff itself, so the feature works key-free."""
249
+ from backend.services.llm import call_llm, is_error_text
250
+
251
+ sections = _diff_sections(diff_text)
252
+ out: dict[str, str] = {}
253
+
254
+ async def explain_one(change: FileChange) -> tuple[str, str]:
255
+ excerpt_lines = sections.get(change.path, [])
256
+ excerpt = _excerpt(excerpt_lines)
257
+ fallback = _deterministic_note(change, excerpt_lines)
258
+ if not api_key:
259
+ return change.path, fallback
260
+ prompt = (
261
+ "You are explaining one file's part of a pull request to someone who does not code.\n"
262
+ f"File: {change.path} ({change.change_type}, +{change.added} / -{change.deleted}).\n"
263
+ f"Diff excerpt:\n{excerpt}\n\n"
264
+ "In 2-3 plain sentences: what changed in this file and why it might matter. "
265
+ "No jargon; say the file name, not the path, when you refer to it."
266
+ )
267
+ text = (await call_llm(prompt, api_key=api_key, model=model)).strip()
268
+ if not text or is_error_text(text):
269
+ return change.path, fallback
270
+ return change.path, text
271
+
272
+ ordered = files[:_EXPLAIN_MAX_FILES]
273
+ results = await asyncio.gather(*(explain_one(f) for f in ordered))
274
+ out.update(results)
275
+ # Files beyond the cap still get the deterministic note so the UI never shows a gap.
276
+ for f in files[_EXPLAIN_MAX_FILES:]:
277
+ out[f.path] = _deterministic_note(f, sections.get(f.path, []))
278
+ return out
279
+
280
+
281
+ async def analyze_pr_explained(
282
+ url: str, *, explain: bool, api_key: str | None = None, model: str | None = None
283
+ ) -> dict:
284
+ """analyze_pr plus optional per-file explanations (LLM when keyed, deterministic otherwise)."""
285
+ ref = parse_pr_url(url)
286
+ diff_text = await asyncio.to_thread(fetch_pr_diff, ref)
287
+ files = parse_diff_files(diff_text)
288
+ ordered = reading_order(files)
289
+ analysis = {
290
+ "url": url,
291
+ "owner": ref.owner,
292
+ "repo": ref.repo,
293
+ "number": ref.number,
294
+ "summary": summarize_diff(files),
295
+ "files": [
296
+ {
297
+ "path": f.path,
298
+ "added": f.added,
299
+ "deleted": f.deleted,
300
+ "change_type": f.change_type,
301
+ "hunks": f.hunks,
302
+ }
303
+ for f in ordered
304
+ ],
305
+ }
306
+ if explain:
307
+ analysis["explanations"] = await explain_files(
308
+ ordered, diff_text, api_key=api_key, model=model
309
+ )
310
+ return analysis
@@ -0,0 +1,185 @@
1
+ """Unused exports: public symbols that nothing else in the project references.
2
+
3
+ Orphan modules answer "which files does nobody import?". This answers the
4
+ finer question one level down: "which functions and classes inside otherwise
5
+ used files does nobody call?". A public helper that was written for a caller
6
+ that no longer exists is dead weight a new reader still has to load into
7
+ their head, and it tends to be the code that breaks first in a refactor
8
+ because nothing exercises it.
9
+
10
+ :func:`find_unused_exports` walks the already-read file contents and flags
11
+ top-level ``def`` / ``class`` definitions (Python) and ``export``ed
12
+ ``function`` / ``class`` / ``const`` declarations (JS/TS) whose name never
13
+ shows up anywhere outside their own file.
14
+
15
+ It deliberately stays conservative to keep the signal worth reading:
16
+
17
+ * Anything decorated is skipped. Decorators are how frameworks register
18
+ things (FastAPI routes, pytest fixtures, click commands, tasks), and a
19
+ decorated definition is used by the framework even when no caller is
20
+ visible in the text.
21
+ * Dunder names, private names (leading ``_``), and definitions inside test
22
+ files are skipped. Tests are consumers, not candidates.
23
+ * A definition referenced only inside its own module still counts as a
24
+ candidate: a public symbol used only locally is effectively private and
25
+ could have been a ``_helper``.
26
+
27
+ Findings are *candidates for human review, not verdicts*. String references,
28
+ ``getattr`` lookups, dynamic imports, and re-export barrels are not tracked,
29
+ so anything here deserves a second look before deletion.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import re
35
+
36
+ _PY = {"py", "pyi"}
37
+ _JSTS = {"js", "jsx", "ts", "tsx", "mjs", "cjs", "mts", "cts"}
38
+
39
+ _TEST_SEGMENTS = {"test", "tests", "__tests__", "spec", "specs"}
40
+
41
+ _PY_DEF_RE = re.compile(r"^(async\s+def|def|class)\s+([A-Za-z_][A-Za-z0-9_]*)")
42
+ _JS_EXPORT_RE = re.compile(
43
+ r"^\s*export\s+(?:async\s+)?(?:function|class|const|let)\s+([A-Za-z_$][A-Za-z0-9_$]*)"
44
+ )
45
+
46
+ # Names that look dead but are entry points by convention.
47
+ _ENTRY_NAMES = {
48
+ "main",
49
+ "app",
50
+ "application",
51
+ "handler",
52
+ "create_app",
53
+ "run",
54
+ "cli",
55
+ "register",
56
+ "setup",
57
+ "teardown",
58
+ "ready",
59
+ "default",
60
+ }
61
+
62
+ _MAX_FINDINGS = 200
63
+
64
+
65
+ def _ext(path: str) -> str:
66
+ name = path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
67
+ return name.rsplit(".", 1)[-1].lower() if "." in name else ""
68
+
69
+
70
+ def _is_test(path: str) -> bool:
71
+ norm = path.replace("\\", "/")
72
+ segments = [s for s in norm.split("/") if s]
73
+ if any(s in _TEST_SEGMENTS for s in segments):
74
+ return True
75
+ name = segments[-1] if segments else ""
76
+ stem = name.rsplit(".", 1)[0] if "." in name else name
77
+ return stem.startswith("test_") or stem.endswith("_test") or ".test" in name or ".spec" in name
78
+
79
+
80
+ def _collect_python_defs(path: str, text: str) -> list[dict]:
81
+ """Top-level, undecorated def/class definitions in one Python file."""
82
+ defs: list[dict] = []
83
+ pending_decorated = False
84
+ for lineno, line in enumerate(text.splitlines(), start=1):
85
+ stripped = line.strip()
86
+ if not stripped or stripped.startswith("#"):
87
+ continue
88
+ indent = len(line) - len(line.lstrip())
89
+ if stripped.startswith("@"):
90
+ if indent == 0:
91
+ pending_decorated = True
92
+ continue
93
+ if indent != 0:
94
+ pending_decorated = False
95
+ continue
96
+ m = _PY_DEF_RE.match(line)
97
+ if not m:
98
+ pending_decorated = False
99
+ continue
100
+ name = m.group(2)
101
+ decorated = pending_decorated
102
+ pending_decorated = False
103
+ if decorated or name.startswith("_") or name in _ENTRY_NAMES:
104
+ continue
105
+ kind = "class" if m.group(1) == "class" else "function"
106
+ defs.append({"path": path, "name": name, "kind": kind, "line": lineno})
107
+ return defs
108
+
109
+
110
+ def _collect_js_defs(path: str, text: str) -> list[dict]:
111
+ defs: list[dict] = []
112
+ for lineno, line in enumerate(text.splitlines(), start=1):
113
+ m = _JS_EXPORT_RE.match(line)
114
+ if not m:
115
+ continue
116
+ name = m.group(1)
117
+ if name.startswith("_") or name in _ENTRY_NAMES:
118
+ continue
119
+ kind = "class" if "class" in line.split("export", 1)[1].split(name)[0] else "function"
120
+ defs.append({"path": path, "name": name, "kind": kind, "line": lineno})
121
+ return defs
122
+
123
+
124
+ def _referenced_outside(name: str, home: str, contents: dict[str, str]) -> bool:
125
+ """True when the name appears anywhere outside its own file.
126
+
127
+ Test files count as consumers here: a symbol exercised only by tests is
128
+ alive, it is just not part of the runtime surface.
129
+ """
130
+ word = re.compile(r"\b" + re.escape(name) + r"\b")
131
+ for path, text in contents.items():
132
+ if path == home:
133
+ continue
134
+ if word.search(text):
135
+ return True
136
+ return False
137
+
138
+
139
+ def _referenced_locally_only(name: str, home: str, text: str) -> bool:
140
+ """True when the name is used in its own file beyond the definition line."""
141
+ return len(re.findall(r"\b" + re.escape(name) + r"\b", text)) > 1
142
+
143
+
144
+ def find_unused_exports(file_contents: dict[str, str]) -> dict:
145
+ """Flag public definitions with no references outside their own file.
146
+
147
+ Returns a summary dict with ``total``, ``files_affected``, ``findings``
148
+ (path/name/kind/line/reason), and ``notes`` explaining the blind spots.
149
+ """
150
+ candidates: list[dict] = []
151
+ for path, text in file_contents.items():
152
+ ext = _ext(path)
153
+ if ext not in _PY and ext not in _JSTS:
154
+ continue
155
+ if _is_test(path):
156
+ continue
157
+ defs = _collect_python_defs(path, text) if ext in _PY else _collect_js_defs(path, text)
158
+ for d in defs:
159
+ if _referenced_outside(d["name"], path, file_contents):
160
+ continue
161
+ local = _referenced_locally_only(d["name"], path, text)
162
+ reason = (
163
+ f"public {d['kind']} used only inside its own module; "
164
+ "nothing imports it elsewhere (effectively private)"
165
+ if local
166
+ else f"public {d['kind']} with no references anywhere else in the project"
167
+ )
168
+ candidates.append({**d, "reason": reason})
169
+
170
+ candidates.sort(key=lambda c: (c["path"], c["line"]))
171
+ findings = candidates[:_MAX_FINDINGS]
172
+ return {
173
+ "total": len(candidates),
174
+ "files_affected": len({c["path"] for c in candidates}),
175
+ "findings": findings,
176
+ "notes": [
177
+ "Findings are candidates for human review, not proof of dead code: "
178
+ "string references, getattr, dynamic imports, and re-export barrels "
179
+ "are not tracked.",
180
+ "Decorated definitions are skipped because frameworks (FastAPI, "
181
+ "pytest, click, task queues) register them without a visible caller.",
182
+ "A symbol used only inside its own module is flagged as effectively "
183
+ "private, not necessarily dead.",
184
+ ],
185
+ }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeabc
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: AI-powered code reader for non-programmers
5
5
  Author-email: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -22,7 +22,7 @@ Requires-Python: >=3.10
22
22
  Requires-Dist: aiofiles>=24.0.0
23
23
  Requires-Dist: aiosqlite>=0.20.0
24
24
  Requires-Dist: fastapi>=0.115.0
25
- Requires-Dist: litellm>=1.40.0
25
+ Requires-Dist: litellm!=1.100.0,!=1.98.0,!=1.99.0,>=1.40.0
26
26
  Requires-Dist: pydantic>=2.0.0
27
27
  Requires-Dist: python-multipart>=0.0.9
28
28
  Requires-Dist: uvicorn[standard]>=0.30.0
@@ -125,6 +125,10 @@ Before trusting a script you didn't write, you usually want to know what it reac
125
125
 
126
126
  The most dangerous line in a codebase is often the one that quietly hides a problem: a bare `except:` that catches everything, an empty `catch {}` that throws the error away, a failure that gets logged and then ignored. To a non-coder these are invisible; to a maintainer they are where bugs go to hide. CodeABC flags these silent-failure spots and explains, in everyday terms, why "the program kept going as if nothing happened" can be worse than crashing loudly.
127
127
 
128
+ ### Which Functions Does Nobody Call?
129
+
130
+ Orphan modules tell you which files nobody imports; this goes one level finer and asks which functions and classes inside otherwise used files nobody calls. A public helper written for a caller that no longer exists is dead weight every reader still has to load into their head, and it is usually the code that breaks first in a refactor because nothing exercises it. CodeABC flags top-level functions, classes, and exported symbols whose name never appears outside their own file, with the reasoning spelled out. Decorated definitions (routes, fixtures, commands) are skipped on purpose, and the report calls itself a candidate list for human review rather than a verdict, because string references and dynamic imports are invisible to any text scan.
131
+
128
132
  ### Which Files Are Begging for Docs?
129
133
 
130
134
  Not every file needs a comment, but the ones that other files lean on heavily and that still have almost no explanation are the ones that cost a newcomer the most. CodeABC scores each file on how under-documented it is relative to how central it is, and surfaces the handful that would help the most if someone wrote a few sentences at the top. It is a reading aid as much as a writing one: these are usually the files worth understanding first.
@@ -1,7 +1,7 @@
1
1
  backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  backend/app.py,sha256=diNqZZhw7NjIWG7OYR7fuwH16tMyLQIpP6o1AMtccbc,3059
3
3
  backend/desktop_server.py,sha256=sBMYZOxoa2diFMtVAALJvqCyntmf6uEvYFu4xVz3D1M,1875
4
- backend/models.py,sha256=bPHvSnh8IdXTGSTu4V1zzFFyUXTJKFikocbucSxgJNs,18055
4
+ backend/models.py,sha256=UmP2DdvOYGC4xLP8JYAPlbbbIKIA5HvUvQZf-4gf7vk,18763
5
5
  backend/prompts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  backend/prompts/annotate.py,sha256=Hiuz8QrWg5gh5dgZrfyMZjnB-HiDwR1nydtkc4XnPKA,1805
7
7
  backend/prompts/edit.py,sha256=dPlKxLNxpfHAnfU_hmWwIZtM103EY3j7dDXNfhOJxss,2111
@@ -9,9 +9,9 @@ backend/prompts/overview.py,sha256=nQGN4NrbCYXxWIli0S1CzttF4SoHrlIPLceW08UbAL4,2
9
9
  backend/prompts/qa.py,sha256=6g2crmr7uM4XnlqzE0juXuUtzyaXlPLly58DCE1mDN0,1653
10
10
  backend/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  backend/routers/analyze.py,sha256=kXyXO785zHEiwqWL6j77roBM2KZ_TiDyc6vSgI8LQh8,9474
12
- backend/routers/project.py,sha256=HPcen_cbJlfzsUwXTbgq1D4U3E0xT-a_5etPy-ATzr8,32483
12
+ backend/routers/project.py,sha256=H6YqbBZiDTSQcmAQNffPmliUqvUv6qk8KifLNcW6nUE,33410
13
13
  backend/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- backend/services/action_plan.py,sha256=9-drt30acjZ-gX7h5FeelcYIhAaYJH-DaqdGGbiKi5E,14649
14
+ backend/services/action_plan.py,sha256=cMOHFzVVplsR1OSZGNkBqiD9rM7ON-1qE__NQVk6Clg,14913
15
15
  backend/services/activity.py,sha256=86cDXEN_1eBrL9CWRM4U4avQUTC2r7wY_3UQvw0gopY,11125
16
16
  backend/services/apimap.py,sha256=-CNwurd5kvUCPL3DUzf5e2OV1zGY3Zclim-UMEs8w9w,13988
17
17
  backend/services/async_surface.py,sha256=vlLAeqh-pTaIFNRQLgDEXAmhOxJ9aYzF46AYDh_x3Io,4807
@@ -31,7 +31,7 @@ backend/services/docs.py,sha256=9bzTtlVgnw0uu7tfx0yT8OnjptqThsIm8kvMHWK17b0,8076
31
31
  backend/services/docstrings.py,sha256=VPBQETdldMRf4l--nDZxeHA_86yAwE0LH6OMcOaYC8E,6736
32
32
  backend/services/duplicate_code.py,sha256=8uybJ21JAIGK7BW4Ff7Zp4OjfsY9WkuXs7fCnK2HR7E,6776
33
33
  backend/services/entrypoints.py,sha256=gvT4ebaKYaUXZqkmbjYEt52svWGh47A9FhWn_K5rqDs,6417
34
- backend/services/envscan.py,sha256=_0WLGL2O3FO5xt9RG1q5VL9MqyhXbZD5OnJ9-UnfQaE,7724
34
+ backend/services/envscan.py,sha256=7QY-zymr4NV5WMxZLWiDUla28bQKvMKz4EcRAz3wvfM,9247
35
35
  backend/services/error_handling.py,sha256=_tm8z71nD08SNAlDE-2XB5Mkhr7svhnUhQgzGKY_Vo0,8889
36
36
  backend/services/filenames.py,sha256=W01M2z1pm26JT2ONKD3sG3oELUygnglZ9T0ir7QxKcU,11875
37
37
  backend/services/github_clone.py,sha256=JXjr2T76R_9anJlxCiJL2LDZc2tjfexg6gRIyEj1n3I,4393
@@ -44,7 +44,7 @@ backend/services/llm.py,sha256=BFKOIxPkO8vIsdzMRMgQus5kWd564EzKBgm3BHzQi68,4410
44
44
  backend/services/long_functions.py,sha256=gDQrYsU-WE9wSQgI6ZkDS7gmakWeOACgpXqKyCJzVX4,4452
45
45
  backend/services/ownership.py,sha256=4BzcCbYEht-Wv8YHEjepfzhzutoT10LEpDRVhqTZnY0,7311
46
46
  backend/services/package_purposes.py,sha256=LIjvOPcfSdF2ynhHqQMaTnhcl4tyyPYNFy5Nz2t1j6c,7162
47
- backend/services/pr_reading.py,sha256=DbTV3drBO9p8ncb6Y55Y7kghbNe8TWQ_PaHqEYPgkbM,6222
47
+ backend/services/pr_reading.py,sha256=HGYM7Wp8Aqi5fU_e26A3m4K5NK0pm_vO-PjiFLun1IE,11010
48
48
  backend/services/release_map.py,sha256=nimpVDnqg8SWTQ5tgeBaW-1HCC3VkwmvGwbvnqlp61A,15998
49
49
  backend/services/report_export.py,sha256=vxzSuwGx7l6HvL_BzyfjRMVilyutcamfx9Qxscb4rps,8529
50
50
  backend/services/risk.py,sha256=NNtInmaklWz6MYsOMwPg0_VKmQ_Wtrdj2sk4LhikTbM,3375
@@ -56,8 +56,9 @@ backend/services/symbols.py,sha256=2sLmLavz0OeJF0IBCV6msWwE1wBlMhkHpl1-SQJKTno,2
56
56
  backend/services/techdebt.py,sha256=og7-mzIsYjKzYRHOnO0DVKmnjDrOgeyZ2FVhHoy0Jsw,4720
57
57
  backend/services/too_many_params.py,sha256=z4VW2uX13iameFhmzjby8qgu1YUGBCWVDsT6ITj-aGs,4432
58
58
  backend/services/typing_coverage.py,sha256=4qH_HnXlzK1OJtm6gD5QZp2qaePj5k1eIcS70XlZF_U,7451
59
- codeabc-0.1.2.dist-info/METADATA,sha256=7ngPjg1Xb_VCdqiBQqJiLY8LzadbVEiRiMMi1-kwJ8k,20343
60
- codeabc-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
61
- codeabc-0.1.2.dist-info/entry_points.txt,sha256=VX1hw16wgWy0owLA2Dgc7FPh0tx3qWFbBvBuTKSLj9o,60
62
- codeabc-0.1.2.dist-info/licenses/LICENSE,sha256=Gjpln1GILd5PHMjjXZpdojQ-DGlvmxFP_nr19R6m9xo,1066
63
- codeabc-0.1.2.dist-info/RECORD,,
59
+ backend/services/unused_exports.py,sha256=4_lm7Xr9-xl9ZuLbeVLj9zybx3oHPMn4kzXChB8Kmuc,7033
60
+ codeabc-0.1.3.dist-info/METADATA,sha256=ylszuXllXMlIacMSOLP74Ks7WmQ1npSaijFQRuaXwos,21163
61
+ codeabc-0.1.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
62
+ codeabc-0.1.3.dist-info/entry_points.txt,sha256=VX1hw16wgWy0owLA2Dgc7FPh0tx3qWFbBvBuTKSLj9o,60
63
+ codeabc-0.1.3.dist-info/licenses/LICENSE,sha256=Gjpln1GILd5PHMjjXZpdojQ-DGlvmxFP_nr19R6m9xo,1066
64
+ codeabc-0.1.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any