mirror of
https://github.com/Shubhamsaboo/awesome-llm-apps.git
synced 2026-08-28 19:11:53 +08:00
fix(dependency-doctor): tighten findings precision per review
- Make stdlib-shadowing and abandoned-backport mutually exclusive so a name that is both (e.g. pathlib, dataclasses) yields one root-cause finding instead of two or three. - Skip conflicting-constraints between same-package entries carrying differing non-python_version markers (e.g. platform_system Linux vs Darwin), which are idiomatic platform splits, not conflicts. - Soften the SKILL.md/README pitch to what the tool actually does (surface-level direct-manifest footguns), not diagnosing failed resolves.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
# Dependency Doctor Agent Skill
|
||||
|
||||
Dependency Doctor inspects one dependency manifest and explains common sources
|
||||
of install drift and runtime breakage. It is a local, user-invoked development
|
||||
tool, not a repository CI rule.
|
||||
Dependency Doctor inspects one dependency manifest for surface-level,
|
||||
direct-manifest footguns. It catches unpinned versions, standard-library
|
||||
shadowing, obsolete backports, and obvious intra-manifest conflicts; it does
|
||||
not diagnose a failed pip or uv dependency resolution. It is a local,
|
||||
user-invoked development tool, not a repository CI rule.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
name: dependency-doctor
|
||||
description: >-
|
||||
Diagnoses requirements.txt, pyproject.toml, and package.json dependency
|
||||
manifests for standard-library shadowing pins, abandoned backports, unpinned
|
||||
dependencies, duplicate or conflicting constraints, and opt-in PyPI yanked
|
||||
releases. Use when the user says "check my requirements.txt for dependency
|
||||
problems", asks "why won't my deps install" or "is anything wrong with my
|
||||
dependencies", wants a dependency autopsy, or suspects dependency manifest
|
||||
rot. Runs offline by default as a local tool for the user's own project, not
|
||||
repository CI.
|
||||
Checks requirements.txt, pyproject.toml, and package.json dependency manifests
|
||||
for surface-level direct-dependency footguns: standard-library shadowing pins,
|
||||
abandoned backports, unpinned dependencies, and obvious intra-manifest
|
||||
conflicts, plus opt-in PyPI yanked releases. Use when the user asks to check a
|
||||
manifest for dependency problems, wants a dependency autopsy, or suspects
|
||||
dependency manifest rot. Runs offline by default as a local tool for the
|
||||
user's own project, not repository CI.
|
||||
license: Apache-2.0
|
||||
compatibility: "Python 3.11+. Offline by default. Network access to pypi.org occurs only when the user explicitly approves --online."
|
||||
metadata:
|
||||
@@ -19,9 +18,9 @@ metadata:
|
||||
|
||||
# Dependency Doctor
|
||||
|
||||
Autopsy one dependency manifest on the user's machine. Find quiet sources of
|
||||
install drift and runtime breakage, explain each finding in plain language,
|
||||
then offer a small, reviewable fix.
|
||||
Inspect one dependency manifest on the user's machine for direct, surface-level
|
||||
footguns. Explain each finding in plain language, then offer a small,
|
||||
reviewable fix. This does not diagnose a failed pip or uv resolution.
|
||||
|
||||
This is a local developer tool for a project the user chooses. It is not a
|
||||
repository-wide lint rule, a CI gate, or a proposal to enforce dependency
|
||||
@@ -30,7 +29,7 @@ policy across unrelated apps.
|
||||
## When to use
|
||||
|
||||
- The user asks to check, audit, diagnose, or autopsy a dependency manifest
|
||||
- A `requirements.txt` install fails for unclear dependency reasons
|
||||
- The user wants to rule out direct-manifest issues before deeper install debugging
|
||||
- The user suspects stale pins, backports, duplicate entries, or dependency rot
|
||||
- The user asks whether anything looks wrong with their dependencies
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ PyPI lookups for exact Python pins so fully yanked releases can be reported.
|
||||
|
||||
import argparse
|
||||
from collections import Counter, defaultdict
|
||||
from itertools import combinations
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -229,6 +230,26 @@ def marker_applies(entry):
|
||||
return True
|
||||
|
||||
|
||||
def non_python_version_marker(entry):
|
||||
"""Return normalized marker clauses that describe a non-Python axis."""
|
||||
if entry["ecosystem"] != "python" or not entry["marker"]:
|
||||
return ""
|
||||
clauses = re.split(r"\s+(?:and|or)\s+", entry["marker"], flags=re.IGNORECASE)
|
||||
non_python_clauses = [
|
||||
" ".join(clause.split()).lower()
|
||||
for clause in clauses
|
||||
if not PYTHON_MARKER_RE.match(clause.strip())
|
||||
]
|
||||
return " && ".join(sorted(non_python_clauses))
|
||||
|
||||
|
||||
def has_differing_non_python_markers(left, right):
|
||||
"""Return whether entries are split across distinct non-Python markers."""
|
||||
left_marker = non_python_version_marker(left)
|
||||
right_marker = non_python_version_marker(right)
|
||||
return bool(left_marker and right_marker and left_marker != right_marker)
|
||||
|
||||
|
||||
def exact_pin(entry):
|
||||
"""Return an exact version, or None for ranges and direct references."""
|
||||
spec = entry["spec"].strip()
|
||||
@@ -264,7 +285,18 @@ def offline_findings(entries):
|
||||
remove_instead_of_pin = False
|
||||
if entry["ecosystem"] == "python":
|
||||
module_name = package.replace("-", "_")
|
||||
if module_name in stdlib_names:
|
||||
if package in BACKPORTS:
|
||||
# A known backport is the more specific, actionable root cause.
|
||||
remove_instead_of_pin = True
|
||||
findings.append(make_finding(
|
||||
"high",
|
||||
"abandoned-backport",
|
||||
entry,
|
||||
BACKPORTS[package] + " The old backport can conflict with supported runtimes.",
|
||||
"Remove %s on supported Python versions. If an older runtime is required, guard the backport with a Python-version marker."
|
||||
% package,
|
||||
))
|
||||
elif module_name in stdlib_names:
|
||||
remove_instead_of_pin = True
|
||||
findings.append(make_finding(
|
||||
"high",
|
||||
@@ -275,16 +307,6 @@ def offline_findings(entries):
|
||||
"Remove %s from the manifest and import the standard-library module directly."
|
||||
% package,
|
||||
))
|
||||
if package in BACKPORTS:
|
||||
remove_instead_of_pin = True
|
||||
findings.append(make_finding(
|
||||
"high",
|
||||
"abandoned-backport",
|
||||
entry,
|
||||
BACKPORTS[package] + " The old backport can conflict with supported runtimes.",
|
||||
"Remove %s on supported Python versions. If an older runtime is required, guard the backport with a Python-version marker."
|
||||
% package,
|
||||
))
|
||||
|
||||
if not has_version_spec(entry):
|
||||
if remove_instead_of_pin:
|
||||
@@ -313,16 +335,25 @@ def offline_findings(entries):
|
||||
for (_ecosystem, package), package_entries in sorted(grouped.items()):
|
||||
if len(package_entries) < 2:
|
||||
continue
|
||||
pins = {pin for pin in (exact_pin(item) for item in package_entries) if pin}
|
||||
conflicting_pairs = [
|
||||
(left, right)
|
||||
for left, right in combinations(package_entries, 2)
|
||||
if exact_pin(left)
|
||||
and exact_pin(right)
|
||||
and exact_pin(left) != exact_pin(right)
|
||||
and not has_differing_non_python_markers(left, right)
|
||||
]
|
||||
lines = ", ".join(str(item["line"]) for item in package_entries)
|
||||
representative = package_entries[1]
|
||||
if len(pins) > 1:
|
||||
if conflicting_pairs:
|
||||
conflict_entries = conflicting_pairs[0]
|
||||
conflict_lines = ", ".join(str(item["line"]) for item in conflict_entries)
|
||||
findings.append(make_finding(
|
||||
"high",
|
||||
"conflicting-constraints",
|
||||
representative,
|
||||
conflict_entries[1],
|
||||
"%s is pinned to incompatible exact versions on lines %s."
|
||||
% (package, lines),
|
||||
% (package, conflict_lines),
|
||||
"Choose one compatible version for %s and keep a single constraint."
|
||||
% package,
|
||||
))
|
||||
|
||||
@@ -65,6 +65,14 @@ def finding(report, kind, package):
|
||||
)
|
||||
|
||||
|
||||
def finding_count(report, kinds, package):
|
||||
"""Return the number of findings for a package in the supplied kinds."""
|
||||
return sum(
|
||||
item.get("kind") in kinds and item.get("package") == package
|
||||
for item in report.get("findings", [])
|
||||
)
|
||||
|
||||
|
||||
def check_finding(report, kind, package, severity, fix_fragment):
|
||||
"""Assert classification, severity, location, explanation, and fix."""
|
||||
item = finding(report, kind, package)
|
||||
@@ -77,7 +85,7 @@ def check_finding(report, kind, package, severity, fix_fragment):
|
||||
check("%s suggests a fix" % kind, fix_fragment.lower() in item["fix"].lower())
|
||||
|
||||
|
||||
def test_requirements(root):
|
||||
def check_requirements(root):
|
||||
manifest = root / "requirements.txt"
|
||||
manifest.write_text(
|
||||
"pathlib==1.0.1\n"
|
||||
@@ -93,7 +101,11 @@ def test_requirements(root):
|
||||
report = run_json(manifest)
|
||||
|
||||
check("report names the input file", report.get("file") == str(manifest))
|
||||
check_finding(report, "stdlib-shadowing", "pathlib", "high", "remove")
|
||||
check_finding(report, "abandoned-backport", "pathlib", "high", "remove")
|
||||
check(
|
||||
"known backports do not also produce stdlib-shadowing findings",
|
||||
finding_count(report, {"stdlib-shadowing", "abandoned-backport"}, "pathlib") == 1,
|
||||
)
|
||||
check_finding(report, "abandoned-backport", "enum34", "high", "remove")
|
||||
check_finding(report, "unpinned", "requests", "medium", "pin")
|
||||
check_finding(report, "duplicate-constraint", "requests", "medium", "one")
|
||||
@@ -113,7 +125,7 @@ def test_requirements(root):
|
||||
)
|
||||
|
||||
|
||||
def test_pyproject(root):
|
||||
def check_pyproject(root):
|
||||
manifest = root / "pyproject.toml"
|
||||
manifest.write_text(
|
||||
"[project]\n"
|
||||
@@ -132,9 +144,13 @@ def test_pyproject(root):
|
||||
check_finding(report, "unpinned", "httpx", "medium", "pin")
|
||||
check("Poetry range is treated as a constraint", finding(report, "unpinned", "rich") is None)
|
||||
check_finding(report, "unpinned", "dataclasses", "medium", "do not add")
|
||||
check(
|
||||
"Poetry backports produce one root-cause finding",
|
||||
finding_count(report, {"stdlib-shadowing", "abandoned-backport"}, "dataclasses") == 1,
|
||||
)
|
||||
|
||||
|
||||
def test_version_marker(root):
|
||||
def check_version_marker(root):
|
||||
manifest = root / "guarded-requirements.txt"
|
||||
manifest.write_text(
|
||||
"dataclasses; python_version < \"3.7\"\n",
|
||||
@@ -147,7 +163,24 @@ def test_version_marker(root):
|
||||
)
|
||||
|
||||
|
||||
def test_package_json(root):
|
||||
def check_platform_markers(root):
|
||||
manifest = root / "platform-requirements.txt"
|
||||
manifest.write_text(
|
||||
"torch==2.1.0; platform_system == \"Linux\"\n"
|
||||
"torch==2.0.0; platform_system == \"Darwin\"\n"
|
||||
"requests==2.31.0; platform_system == \"Linux\"\n"
|
||||
"requests==2.32.0; platform_system == \"Linux\"\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
report = run_json(manifest)
|
||||
check(
|
||||
"platform-split pins are not conflicts",
|
||||
finding(report, "conflicting-constraints", "torch") is None,
|
||||
)
|
||||
check_finding(report, "conflicting-constraints", "requests", "high", "choose")
|
||||
|
||||
|
||||
def check_package_json(root):
|
||||
manifest = root / "package.json"
|
||||
manifest.write_text(
|
||||
json.dumps({"dependencies": {"left-pad": "*", "chalk": "5.3.0"}}, indent=2) + "\n",
|
||||
@@ -165,10 +198,11 @@ def main():
|
||||
print("dependency-doctor eval:")
|
||||
with tempfile.TemporaryDirectory(prefix="dependency-doctor-eval-") as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
test_requirements(root)
|
||||
test_pyproject(root)
|
||||
test_version_marker(root)
|
||||
test_package_json(root)
|
||||
check_requirements(root)
|
||||
check_pyproject(root)
|
||||
check_version_marker(root)
|
||||
check_platform_markers(root)
|
||||
check_package_json(root)
|
||||
|
||||
print()
|
||||
passed = sum(CHECKS)
|
||||
@@ -181,3 +215,8 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
def test_dependency_doctor_eval():
|
||||
"""Expose the standalone eval harness to pytest."""
|
||||
assert main() == 0
|
||||
|
||||
Reference in New Issue
Block a user