From 28be4bb65aee89f69d3b0fdd79bbcd6480815f46 Mon Sep 17 00:00:00 2001 From: hummbl-dev Date: Sun, 24 May 2026 10:45:25 -0400 Subject: [PATCH] fix(release): align arbiter package metadata --- README.md | 8 +++--- src/arbiter/__init__.py | 2 +- src/arbiter/__main__.py | 2 +- src/arbiter/api.py | 3 ++- src/arbiter/git_historian.py | 47 ++++++++++++++++++++++++++++++---- tests/test_api.py | 2 ++ tests/test_git_vitality.py | 13 +++++++--- tests/test_release_metadata.py | 27 +++++++++++++++++++ 8 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 tests/test_release_metadata.py diff --git a/README.md b/README.md index 1bdc70c..6b6ecba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Arbiter [![CI](https://github.com/hummbl-dev/arbiter/actions/workflows/ci.yml/badge.svg)](https://github.com/hummbl-dev/arbiter/actions/workflows/ci.yml) -[![PyPI](https://img.shields.io/pypi/v/arbiter)](https://pypi.org/project/arbiter/) +[![PyPI](https://img.shields.io/pypi/v/arbiter-score)](https://pypi.org/project/arbiter-score/) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) [![Dependencies](https://img.shields.io/badge/core_deps-stdlib_only-brightgreen)]() @@ -13,8 +13,8 @@ In 2026, code is written by fleets of AI agents. Arbiter knows *who* wrote each ## Install ```bash -pip install arbiter # core (stdlib only) -pip install "arbiter[analyzers]" # + ruff, radon, vulture, bandit +pip install arbiter-score # core (stdlib only) +pip install "arbiter-score[analyzers]" # + ruff, radon, vulture, bandit # Or from source git clone https://github.com/hummbl-dev/arbiter.git && cd arbiter @@ -83,7 +83,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 c9f1764..46863ec 100644 --- a/src/arbiter/__main__.py +++ b/src/arbiter/__main__.py @@ -1436,7 +1436,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_git_vitality.py b/tests/test_git_vitality.py index dad04c7..76837e0 100644 --- a/tests/test_git_vitality.py +++ b/tests/test_git_vitality.py @@ -1,4 +1,5 @@ import pytest +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch from pathlib import Path from arbiter.git_vitality import score_git_vitality, GitVitalityReport @@ -6,15 +7,20 @@ @pytest.fixture def mock_commits(): + now = datetime.now(timezone.utc) + + def days_ago(days: int) -> str: + return (now - timedelta(days=days)).isoformat().replace("+00:00", "Z") + return [ CommitInfo( hash="h1", author_name="Alice", author_email="alice@example.com", - timestamp="2026-04-10T10:00:00Z", message="feat: something\nSigned-off-by: Alice", + timestamp=days_ago(9), message="feat: something\nSigned-off-by: Alice", files_changed=1, loc_added=10, loc_removed=2, agent="gemini" ), CommitInfo( hash="h2", author_name="Bob", author_email="bob@example.com", - timestamp="2026-04-01T10:00:00Z", message="fix: bug", + timestamp=days_ago(18), message="fix: bug", files_changed=1, loc_added=5, loc_removed=1, agent="human" ) ] @@ -33,8 +39,7 @@ def test_single_committer_scores_low_bus_factor(tmp_path, mock_commits): def test_recent_commit_scores_max_recency(tmp_path, mock_commits): (tmp_path / ".git").mkdir() - # Today is 2026-04-19 (from session context) - # mock_commits[0] is from 2026-04-10 (9 days ago) + # The newest mocked commit is always nine days old. with patch("arbiter.git_vitality.walk_commits", return_value=mock_commits): with patch("subprocess.run") as mock_run: mock_run.return_value.returncode = 0 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