diff --git a/README.md b/README.md index 7ddbb06..7fc6c50 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ arbiter serve [--port 8080] # API + dashboard ```bash pip install ".[test]" -PYTHONPATH=src python -m pytest tests/ -v # 78 tests, <7 seconds +PYTHONPATH=src python -m pytest tests/ -v ``` ## Quality Gate diff --git a/src/arbiter/__init__.py b/src/arbiter/__init__.py index 52d057b..8acecfd 100644 --- a/src/arbiter/__init__.py +++ b/src/arbiter/__init__.py @@ -1,3 +1,3 @@ """Arbiter — Agent-aware code quality system.""" -__version__ = "0.1.0" +__version__ = "0.6.0" diff --git a/src/arbiter/__main__.py b/src/arbiter/__main__.py index 7c78657..f731570 100644 --- a/src/arbiter/__main__.py +++ b/src/arbiter/__main__.py @@ -1427,7 +1427,7 @@ def _repo_dict(name: str, score: RepoScore, loc: int, density: float) -> dict: else: col_w = max(len(name_a), len(name_b), 15) print("\nArbiter Compare") - print("\u2550" * 50) + print("=" * 50) print(f"{'':24s}{name_a:<{col_w}s} {name_b}") def _score_str(s: RepoScore) -> str: diff --git a/src/arbiter/api.py b/src/arbiter/api.py index daeab15..c490148 100644 --- a/src/arbiter/api.py +++ b/src/arbiter/api.py @@ -21,6 +21,7 @@ from typing import Any from urllib.parse import parse_qs, urlparse +from arbiter import __version__ from arbiter.store import Store logger = logging.getLogger(__name__) @@ -128,7 +129,7 @@ def _handle_commit_detail(self, params: dict, commit_hash: str) -> None: self._send_json({"error": "Commit not found"}, status=404) def _handle_health(self, params: dict) -> None: - self._send_json({"status": "ok", "version": "0.2.0"}) + self._send_json({"status": "ok", "version": __version__}) def _serve_dashboard(self) -> None: index = _DASHBOARD_DIR / "index.html" diff --git a/src/arbiter/git_historian.py b/src/arbiter/git_historian.py index ae3cca4..980ce56 100644 --- a/src/arbiter/git_historian.py +++ b/src/arbiter/git_historian.py @@ -30,6 +30,21 @@ class CommitInfo: # Git log format: hash|author_name|author_email|timestamp|subject _LOG_FORMAT = "%H|%an|%ae|%aI|%s" _LOG_SEP = "|" +_FALLBACK_EXCLUDED_DIRS = { + ".git", + ".hg", + ".svn", + "__pycache__", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".tox", + ".venv", + "venv", + "build", + "dist", + "node_modules", +} def walk_commits( @@ -178,15 +193,37 @@ def get_diff_files(repo_path: str | Path, base_branch: str = "main") -> list[str def get_python_files(repo_path: str | Path) -> list[Path]: """List all tracked Python files in the repo.""" repo = Path(repo_path) - result = subprocess.run( - ["git", "-C", str(repo), "ls-files", "*.py"], - capture_output=True, text=True, timeout=10, - ) + try: + result = subprocess.run( + ["git", "-C", str(repo), "ls-files", "*.py"], + capture_output=True, text=True, timeout=10, + ) + except (FileNotFoundError, OSError, subprocess.SubprocessError): + return _fallback_python_files(repo) if result.returncode != 0: - return [] + return _fallback_python_files(repo) return [repo / f for f in result.stdout.strip().split("\n") if f] +def _fallback_python_files(repo: Path) -> list[Path]: + """Find Python files without git, used when git is unavailable.""" + if not repo.exists(): + return [] + if repo.is_file(): + return [repo] if repo.suffix == ".py" else [] + + files: list[Path] = [] + for path in repo.rglob("*.py"): + try: + rel_parts = path.relative_to(repo).parts + except ValueError: + continue + if any(part in _FALLBACK_EXCLUDED_DIRS for part in rel_parts[:-1]): + continue + files.append(path) + return sorted(files) + + def count_loc(repo_path: str | Path) -> int: """Count total lines of Python code in the repo.""" total = 0 diff --git a/tests/test_api.py b/tests/test_api.py index e2c4baa..528e9cb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,6 +7,7 @@ from pathlib import Path import pytest +from arbiter import __version__ from arbiter.api import ArbiterHandler from arbiter.scoring import RepoScore from arbiter.store import Store @@ -63,6 +64,7 @@ def test_health(self, server_url): status, data = _get(server_url, "/api/health") assert status == 200 assert data["status"] == "ok" + assert data["version"] == __version__ def test_score(self, server_url): status, data = _get(server_url, "/api/score") diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py new file mode 100644 index 0000000..001236b --- /dev/null +++ b/tests/test_release_metadata.py @@ -0,0 +1,27 @@ +"""Release metadata checks for the public package surface.""" + +from __future__ import annotations + +from pathlib import Path +import tomllib + +import arbiter + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_runtime_version_matches_pyproject() -> None: + metadata = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + + assert arbiter.__version__ == metadata["project"]["version"] + + +def test_readme_uses_distribution_name() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + + assert "pypi/v/arbiter-score" in readme + assert "https://pypi.org/project/arbiter-score/" in readme + assert "pip install arbiter-score" in readme + assert "pip install arbiter " not in readme + assert "pip install \"arbiter[analyzers]\"" not in readme