diff --git a/CHANGELOG.md b/CHANGELOG.md index d41dc1a..5643437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **CLAUDE.md outdated-hint: one version source + stderr** (GH #161). + `check_outdated` compared the injected version against + `importlib.metadata` (stale under editable installs — dist-info is baked + at install time) while the hint print used module `__version__`, so a + freshly injected CLAUDE.md triggered a self-contradictory + "v0.35.1 vs v0.35.1, run update" hint on every CLI invocation. The check + now uses the module's existing `__version__` resolver (source pyproject + first, the same one `--version` uses), and the startup hint prints to + stderr so `--output json` consumers can `json.loads` stdout without + tripping over it (this once made 21 CLI JSON tests fail red on a stale + venv). + - **post-commit hook no longer overwrites scan-all's hierarchical READMEs** (GH #160). The hook re-rendered each affected directory via a per-dir `codeindex scan` subprocess, which hardcodes `level="detailed"` and diff --git a/src/codeindex/README_AI.md b/src/codeindex/README_AI.md index c8e501b..b380ef4 100644 --- a/src/codeindex/README_AI.md +++ b/src/codeindex/README_AI.md @@ -1,5 +1,5 @@ - + # codeindex diff --git a/src/codeindex/claude_md.py b/src/codeindex/claude_md.py index fa57f21..f4e0591 100644 --- a/src/codeindex/claude_md.py +++ b/src/codeindex/claude_md.py @@ -11,7 +11,6 @@ """ -import importlib.metadata import logging import re from pathlib import Path @@ -64,12 +63,18 @@ def detect_locale(content: str) -> str: def _get_current_version() -> str: - """Get current codeindex package version.""" - try: - return importlib.metadata.version("ai-codeindex") - except importlib.metadata.PackageNotFoundError: - from . import __version__ - return __version__ + """Get current codeindex package version. + + Uses the module's ``__version__`` resolver (source pyproject first, + installed metadata as fallback). GH #161: this previously did its own + importlib-first lookup, which goes stale under editable installs + (dist-info baked at install time) — a freshly injected CLAUDE.md then + triggered a self-contradictory "v0.35.1 vs v0.35.1, run update" hint + on every CLI invocation. + """ + from . import __version__ + + return __version__ def _load_template(version: str, lang: str = "en") -> str: diff --git a/src/codeindex/cli_claude_md.py b/src/codeindex/cli_claude_md.py index 26d9fe1..0be8a16 100644 --- a/src/codeindex/cli_claude_md.py +++ b/src/codeindex/cli_claude_md.py @@ -9,6 +9,7 @@ from .claude_md import check_outdated, extract_version, inject console = Console() +stderr_console = Console(stderr=True) @click.group("claude-md") @@ -90,7 +91,9 @@ def print_outdated_warning(): """Print a one-line warning if CLAUDE.md is outdated. Called on CLI startup.""" outdated_version = check_outdated() if outdated_version: - console.print( + # stderr: stdout must stay clean for machine-readable output + # (`--output json` consumers json.loads the whole stream, GH #161). + stderr_console.print( f"[dim yellow]hint: CLAUDE.md has codeindex v{outdated_version}, " f"current is v{__version__}. " f"Run `codeindex claude-md update` to refresh.[/dim yellow]" diff --git a/tests/README_AI.md b/tests/README_AI.md index 0432d37..48c179f 100644 --- a/tests/README_AI.md +++ b/tests/README_AI.md @@ -1,5 +1,5 @@ - + # tests @@ -8,7 +8,7 @@ ## Overview - **Files**: 149 -- **Symbols**: 2438 +- **Symbols**: 2443 - **Subdirectories**: 5 ## Subdirectories diff --git a/tests/test_claude_md.py b/tests/test_claude_md.py index 2de0a5e..07eb0b7 100644 --- a/tests/test_claude_md.py +++ b/tests/test_claude_md.py @@ -236,6 +236,61 @@ def test_returns_old_version_if_outdated(self, tmp_path): assert check_outdated(tmp_path) == "0.22.0" +class TestVersionSourceConsistency: + """GH #161: one version source everywhere. + + _get_current_version previously did its own importlib-first lookup while + the hint print used module __version__ — under an editable install with + stale dist-info, a fresh CLAUDE.md triggered a self-contradictory + "v0.35.1 vs v0.35.1, run update" hint. + """ + + def test_get_current_version_matches_module_version(self): + from codeindex import __version__ + from codeindex.claude_md import _get_current_version + + assert _get_current_version() == __version__ + + def test_stale_dist_info_does_not_leak(self): + """Even with importlib metadata disagreeing (editable install with + stale dist-info), the check must follow the module resolver.""" + import importlib.metadata as _m + from unittest.mock import patch as _patch + + from codeindex import __version__ + from codeindex.claude_md import _get_current_version + + with _patch.object(_m, "version", return_value="0.0.1"): + assert _get_current_version() == __version__ + + def test_fresh_claude_md_not_flagged_despite_stale_dist_info(self, tmp_path): + """End-to-end property: CLAUDE.md at the current version must NOT be + flagged, whatever dist-info claims (the original #161 symptom).""" + import importlib.metadata as _m + from unittest.mock import patch as _patch + + from codeindex import __version__ + + claude_md = tmp_path / "CLAUDE.md" + claude_md.write_text(f"\n\n") + + with _patch.object(_m, "version", return_value="0.0.1"): + assert check_outdated(tmp_path) is None + + def test_print_outdated_warning_goes_to_stderr(self, capsys): + """The startup hint must not pollute stdout (breaks --output json).""" + from unittest.mock import patch as _patch + + from codeindex.cli_claude_md import print_outdated_warning + + with _patch("codeindex.cli_claude_md.check_outdated", return_value="0.1.0"): + print_outdated_warning() + + captured = capsys.readouterr() + assert "hint:" in captured.err + assert captured.out == "" + + class TestMarkerPattern: """Tests for marker regex pattern."""