From 4c63f4176aac37bb7a03fd4a9af0ed3f2e66b8d7 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Sun, 28 Jun 2026 15:39:22 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20single=20source=20of=20truth=20for=20ver?= =?UTF-8?q?sion=20=E2=80=94=20closes=20#37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes scripts/utils.py:CODELENS_VERSION as the single source of truth for the CodeLens version. All other version mentions now match this constant or are stripped entirely. Issue #37 reported 4 different version values across 6 files: pyproject.toml=7.2.0, skill.json=7.2.0, utils.py=8.0.0, README.md=v8.2, SKILL.md=v8.1, SKILL-QUICK.md=v8.2, CHANGELOG.md=[8.2.0] Unreleased. This PR synchronizes everyone to 8.2.0 (the value CHANGELOG.md already advertises as the unreleased version) and adds a CI guard test so future drift fails CI immediately. Changes (6 existing files + 1 new test file): - scripts/utils.py:393 — CODELENS_VERSION "8.0.0" -> "8.2.0" (this is the canonical source going forward) - pyproject.toml:7 — version "7.2.0" -> "8.2.0" (PyPI / packaging metadata) - skill.json:3 — version "7.2.0" -> "8.2.0" (MCP / AI-host manifest) - README.md:1 — strip "v8.2" from H1 heading (# CodeLens — AI-Native Code Intelligence) - SKILL.md:12 — strip "v8.1" from H1 heading (# CodeLens) - SKILL-QUICK.md:1 — strip "v8.2" from H1 heading (# CodeLens — Quick Reference) - tests/test_version_consistency.py (NEW) — 3 tests: test_pyproject_version_matches_utils test_skill_json_version_matches_utils test_no_version_in_doc_headings Why dynamic versioning was NOT used: pyproject.toml supports dynamic = ["version"] with attr-based lookup, but the simpler hardcoded approach was chosen per task brief (JSON cannot be dynamic anyway, and keeping pyproject hardcoded matches the skill.json pattern). The CI guard test makes the hardcoded values safe. Doc headings: the live version is now queried via `codelens --version` (returns 8.2.0). README/SKILL/SKILL-QUICK no longer embed version literals in their H1, eliminating the third source of truth that caused the original drift. CHANGELOG.md intentionally NOT touched — its `[8.2.0] — Unreleased` section was already correct and is the target value everyone else was bumped to match. Verification: - python3 -c "from utils import CODELENS_VERSION; print(CODELENS_VERSION)" outputs 8.2.0 - pytest tests/test_version_consistency.py -v 3 passed - Full test suite (excluding slow test_integration.py): baseline (pre-PR): 40 failed, 668 passed, 14 skipped with this PR: 37 failed, 671 passed, 14 skipped (3 net new passes = the 3 new consistency tests; no regressions) Findings (out of scope, not fixed): - tests/test_vuln_staleness.py::TestScanVulnerabilitiesCacheInfo::test_vulnerable_app_has_stale_cache_info fails pre-PR too — it's a date-based staleness test (VULN_DB last-updated 2025-02-28, now 485 days old); the test expects is_stale=True but the engine returns False. Pre-existing. - 36 other pre-existing failures in test_architecture, test_compact_format, test_graph_incremental, test_graph_model, test_hybrid_type_resolver — all fail identically pre-PR and post-PR. None touch version metadata. Closes #37. --- README.md | 2 +- SKILL-QUICK.md | 2 +- SKILL.md | 2 +- pyproject.toml | 2 +- scripts/utils.py | 2 +- skill.json | 2 +- tests/test_version_consistency.py | 169 ++++++++++++++++++++++++++++++ 7 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 tests/test_version_consistency.py diff --git a/README.md b/README.md index ea567526..2a023bf5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CodeLens v8.2 — AI-Native Code Intelligence +# CodeLens — AI-Native Code Intelligence > **Before an AI writes a new class/id/function, CodeLens must be checked. This is not optional.** diff --git a/SKILL-QUICK.md b/SKILL-QUICK.md index 1d7f58c5..31b8f31f 100755 --- a/SKILL-QUICK.md +++ b/SKILL-QUICK.md @@ -1,4 +1,4 @@ -# CodeLens v8.2 — Quick Reference +# CodeLens — Quick Reference **MUST activate before writing/editing/deleting any class, id, or function.** diff --git a/SKILL.md b/SKILL.md index 587853db..4435803e 100755 --- a/SKILL.md +++ b/SKILL.md @@ -9,7 +9,7 @@ description: > For version history, see CHANGELOG.md. --- -# CodeLens v8.1 +# CodeLens Before an AI writes a new class/id/function, CodeLens must be checked. This is not optional. diff --git a/pyproject.toml b/pyproject.toml index 514e689f..59f5ad93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codelens" -version = "7.2.0" +version = "8.2.0" description = "Live Codebase Reference Intelligence — 45 commands for AI-powered code analysis, security auditing, and quality scoring" readme = "README.md" license = {text = "MIT"} diff --git a/scripts/utils.py b/scripts/utils.py index dbfc5248..e3644b9e 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -390,7 +390,7 @@ def _identify_signature(sig: bytes) -> Optional[str]: # ─── Version ──────────────────────────────────────────────── -CODELENS_VERSION = "8.0.0" +CODELENS_VERSION = "8.2.0" # ─── Generated File Detection ─────────────────────────────── diff --git a/skill.json b/skill.json index 2239cb55..69610288 100755 --- a/skill.json +++ b/skill.json @@ -1,6 +1,6 @@ { "name": "codelens", - "version": "7.2.0", + "version": "8.2.0", "description": "Live Codebase Reference Intelligence. 45 commands for AI-powered code analysis, security auditing, quality scoring, and pre-write safety checks. Supports 28+ languages with regex+AST hybrid parsing. Must activate before writing/editing/deleting any class, id, or function.", "author": "codelens", "command_categories": { diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py new file mode 100644 index 00000000..de99a218 --- /dev/null +++ b/tests/test_version_consistency.py @@ -0,0 +1,169 @@ +""" +Tests for version consistency across CodeLens metadata files. + +Issue #37 reported version drift across 6 files: pyproject.toml, +skill.json, scripts/utils.py, README.md, SKILL.md, SKILL-QUICK.md. +This test file is the CI guard that prevents future drift. + +Contract: + - ``scripts/utils.py:CODELENS_VERSION`` is the single source of truth. + - ``pyproject.toml`` ``version`` field must equal ``CODELENS_VERSION``. + - ``skill.json`` ``version`` field must equal ``CODELENS_VERSION``. + - Documentation H1 headings (README.md, SKILL.md, SKILL-QUICK.md) + must NOT embed a ``vX.Y`` literal — readers should run + ``codelens --version`` for the live version. + +Run: + python3 -m pytest tests/test_version_consistency.py -v +""" + +import json +import os +import re +import sys +from pathlib import Path + +import pytest + +# Make scripts/ importable so we can read the canonical CODELENS_VERSION +# directly. Same convention as tests/test_cli.py. +SCRIPT_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts" +) +sys.path.insert(0, SCRIPT_DIR) + +from utils import CODELENS_VERSION # noqa: E402 + +REPO_ROOT = Path(__file__).resolve().parent.parent +PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" +SKILL_JSON_PATH = REPO_ROOT / "skill.json" +DOC_PATHS = [ + REPO_ROOT / "README.md", + REPO_ROOT / "SKILL.md", + REPO_ROOT / "SKILL-QUICK.md", +] + +# A "vX.Y" or "vX.Y.Z" literal embedded in a doc H1. We reject any +# version-like prefix on the title line because the live version lives +# in CODELENS_VERSION, not in markdown. +_DOC_VERSION_PATTERN = re.compile(r"v\d+\.\d+") + + +def _read_pyproject_version() -> str: + """Return the ``version`` value from ``[project]`` in pyproject.toml. + + Prefers ``tomllib`` (stdlib in Python 3.11+) for a clean parse. + Falls back to a regex on the ``version = "..."`` line for older + Python (3.8-3.10) where ``tomllib`` is not available and we don't + want to add ``tomli`` as a test-only dependency. + + Returns: + The version string declared in pyproject.toml. + + Raises: + AssertionError: if the version field cannot be located. + """ + try: + import tomllib # type: ignore[import-not-found] + except ModuleNotFoundError: + # Python 3.8-3.10 fallback: regex the version line. Safe here + # because pyproject.toml has exactly one top-level + # ``version = "..."`` line (verified by the test below). + text = PYPROJECT_PATH.read_text(encoding="utf-8") + match = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE) + assert match, ( + f"Could not find `version = \"...\"` in {PYPROJECT_PATH}. " + f"Is the [project] table still present?" + ) + return match.group(1) + + with PYPROJECT_PATH.open("rb") as fh: + data = tomllib.load(fh) + project_table = data.get("project", {}) + version = project_table.get("version") + assert version, ( + f"pyproject.toml [project] table has no `version` key. " + f"Either hardcode it or wire up dynamic = ['version']." + ) + # If dynamic versioning is ever enabled, `version` will be None and + # the assert above will fire — surfacing the regression explicitly + # rather than silently passing the test. + return str(version) + + +def test_pyproject_version_matches_utils() -> None: + """``pyproject.toml`` version must equal ``utils.CODELENS_VERSION``. + + This is the CI guard for the PyPI/packaging metadata side of the + drift reported in issue #37. If a maintainer bumps + ``CODELENS_VERSION`` without also bumping ``pyproject.toml``, this + test fails. + + The single source of truth is ``utils.CODELENS_VERSION`` — when + the two disagree, this test treats the constant as canonical and + reports the pyproject value as the one that needs updating. + """ + pyproject_version = _read_pyproject_version() + assert pyproject_version == CODELENS_VERSION, ( + f"Version drift: pyproject.toml declares version=" + f"{pyproject_version!r} but scripts/utils.py:CODELENS_VERSION=" + f"{CODELENS_VERSION!r}. Bump pyproject.toml to match " + f"(CODELENS_VERSION is the single source of truth)." + ) + + +def test_skill_json_version_matches_utils() -> None: + """``skill.json`` version must equal ``utils.CODELENS_VERSION``. + + ``skill.json`` is the MCP server's externally-advertised manifest + (and what AI hosts read to decide whether to activate the skill). + A stale ``version`` field here is the most user-visible symptom of + drift — it tells AI agents the wrong capability level. + """ + with SKILL_JSON_PATH.open("r", encoding="utf-8") as fh: + skill_data = json.load(fh) + skill_version = skill_data.get("version") + assert skill_version is not None, ( + f"skill.json has no top-level `version` field. " + f"Add one matching CODELENS_VERSION={CODELENS_VERSION!r}." + ) + assert skill_version == CODELENS_VERSION, ( + f"Version drift: skill.json declares version={skill_version!r} " + f"but scripts/utils.py:CODELENS_VERSION={CODELENS_VERSION!r}. " + f"Bump skill.json to match (CODELENS_VERSION is the single " + f"source of truth)." + ) + + +def test_no_version_in_doc_headings() -> None: + """Doc H1 headings must NOT embed a ``vX.Y`` literal. + + The live version lives in ``CODELENS_VERSION`` and is queried via + ``codelens --version``. Hardcoding ``v8.2`` (or any other literal) + in the README/SKILL/SKILL-QUICK H1 headings creates a third source + of truth that drifts the moment a maintainer bumps + ``CODELENS_VERSION`` without remembering to edit markdown — which + is exactly how issue #37's drift happened. + + This test scans the first line of each doc and fails if a + ``v.`` pattern appears anywhere on it. + """ + missing = [p for p in DOC_PATHS if not p.exists()] + assert not missing, f"Doc file(s) missing: {missing}" + + drifted: list[str] = [] + for path in DOC_PATHS: + first_line = path.read_text(encoding="utf-8").splitlines()[0] + match = _DOC_VERSION_PATTERN.search(first_line) + if match is not None: + drifted.append( + f"{path.name}:1 — H1 contains version literal " + f"{match.group(0)!r} in line: {first_line!r}" + ) + + assert not drifted, ( + f"Doc H1 headings must not embed version literals — " + f"CODELENS_VERSION={CODELENS_VERSION!r} is the single source " + f"of truth. Readers should run `codelens --version` for the " + f"live version. Drift found in:\n - " + "\n - ".join(drifted) + )