diff --git a/autoassistant/audit_skill_apis.py b/autoassistant/audit_skill_apis.py index 2b070dc..640367e 100644 --- a/autoassistant/audit_skill_apis.py +++ b/autoassistant/audit_skill_apis.py @@ -806,11 +806,20 @@ def write_baseline(root: Path, *, allow_dev_stack: bool = False) -> Path: def check_version(root: Path) -> int: - """Compare the installed stack against the committed baseline. - - Returns 0 when versions and API hashes match, 1 on any drift (or a missing - baseline). Prints a short human-readable summary. Intentionally cheap — no - Markdown scan — so it is safe to run at session start. + """Compare the installed stack's public API surface against the committed baseline. + + Returns 0 when the public-API-surface hashes match, 1 on any hash drift (or a + missing baseline). Prints a short human-readable summary. Intentionally cheap + — no Markdown scan — so it is safe to run at session start. + + The per-module ``__version__`` equality this check used to *gate* on was + dropped (PyAutoMind build-chain #155 Phase 4 task 3). Since PyAutoConf#119 / + PyAutoBuild#121 a release no longer commits the ``__version__`` stamp back to + the library ``main``, so a source checkout reports a frozen stamp while the + baseline is wheel-derived — a structurally-permanent version mismatch, + independent of release cadence, that the API-surface hash already proves is + spurious (the public surface is byte-identical). Versions are still shown for + context; only the API-surface hash gates. """ path = root / BASELINE_REL_PATH if not path.exists(): @@ -823,7 +832,10 @@ def check_version(root: Path) -> int: baseline = json.loads(path.read_text(encoding="utf-8")) current = compute_baseline() - version_drift = [ + # Informational only — a version-stamp difference no longer gates (frozen + # source stamps vs a wheel-derived baseline false-positive; the API-surface + # hash is the real signal). + version_changes = [ (m, baseline["versions"].get(m, "(absent)"), current["versions"][m]) for m in VERSIONED_MODULES if baseline["versions"].get(m) != current["versions"][m] @@ -835,10 +847,17 @@ def check_version(root: Path) -> int: != current["api_surface"][m]["hash"] ] - if not version_drift and not hash_drift: + if not hash_drift: + note = "" + if version_changes: + note = ( + " (version stamp differs but public API surface is identical: " + + ", ".join(f"{m} {old}->{new}" for m, old, new in version_changes) + + ")" + ) print( - f"[drift] clean — installed stack matches baseline " - f"(autofit {current['versions']['autofit']}, generated {baseline.get('generated')})." + f"[drift] clean — installed public API surface matches baseline " + f"(autofit {current['versions']['autofit']}, generated {baseline.get('generated')}){note}." ) return 0 @@ -847,16 +866,12 @@ def check_version(root: Path) -> int: f"(baseline generated {baseline.get('generated')}):", file=sys.stderr, ) - for m, old, new in version_drift: - print(f" - {m}: {old} -> {new}", file=sys.stderr) - if hash_drift: - print( - f" - public API surface changed: {', '.join(hash_drift)}", file=sys.stderr - ) + print(f" - public API surface changed: {', '.join(hash_drift)}", file=sys.stderr) + for m, old, new in version_changes: + print(f" - {m} version: {old} -> {new} (informational)", file=sys.stderr) print( - " The skills/wiki were validated against the baseline. Upgrade/downgrade to the " - "pinned version (`pip install -U autofit`), or run `--scope all` to audit drift " - "and `--write-baseline` to re-pin once fixed.", + " The skills/wiki were validated against the baseline. Run `--scope all` to audit " + "drift and `--write-baseline` to re-pin once fixed.", file=sys.stderr, ) return 1 diff --git a/autoassistant/tests/test_check_version.py b/autoassistant/tests/test_check_version.py new file mode 100644 index 0000000..28fc78d --- /dev/null +++ b/autoassistant/tests/test_check_version.py @@ -0,0 +1,57 @@ +"""test_check_version.py — --check-version gates on the API-surface hash only. + +The per-module ``__version__`` equality was dropped (PyAutoMind build-chain #155 +Phase 4 task 3): releases no longer commit the stamp back to library ``main``, so +a source checkout's frozen stamp vs a wheel-derived baseline is a permanent false +positive that the API-surface hash already proves spurious. These tests +monkeypatch ``compute_baseline`` so they need no installed stack. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from autoassistant import audit_skill_apis as a + + +def _bl(versions, hashes): + return { + "generated": "2026-07-09", + "versions": versions, + "api_surface": {m: {"hash": h, "n_symbols": 1} for m, h in hashes.items()}, + } + + +def _write_baseline(root, versions, hashes): + path = root / a.BASELINE_REL_PATH + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(_bl(versions, hashes)), encoding="utf-8") + + +def test_version_differs_but_api_surface_matches_is_clean(tmp_path, monkeypatch, capsys): + base_versions = {m: "2026.7.9.1" for m in a.VERSIONED_MODULES} + hashes = {m: "deadbeef" for m in a.BASELINE_MODULES} + _write_baseline(tmp_path, base_versions, hashes) + cur_versions = {m: "2026.7.15.1" for m in a.VERSIONED_MODULES} + monkeypatch.setattr(a, "compute_baseline", lambda: _bl(cur_versions, hashes)) + assert a.check_version(tmp_path) == 0 # was 1 before task 3 + out = capsys.readouterr().out + assert "clean" in out and "public API surface is identical" in out + + +def test_api_surface_hash_drift_is_flagged(tmp_path, monkeypatch, capsys): + versions = {m: "2026.7.9.1" for m in a.VERSIONED_MODULES} + base_hashes = {m: "deadbeef" for m in a.BASELINE_MODULES} + _write_baseline(tmp_path, versions, base_hashes) + moved = a.BASELINE_MODULES[-1] + cur_hashes = dict(base_hashes) + cur_hashes[moved] = "cafef00d" + monkeypatch.setattr(a, "compute_baseline", lambda: _bl(versions, cur_hashes)) + assert a.check_version(tmp_path) == 1 + err = capsys.readouterr().err + assert "public API surface changed" in err and moved in err + + +def test_missing_baseline_is_drift(tmp_path): + assert a.check_version(tmp_path) == 1 \ No newline at end of file diff --git a/skills/af_audit_skill_apis.md b/skills/af_audit_skill_apis.md index 9f7179c..8d534cc 100644 --- a/skills/af_audit_skill_apis.md +++ b/skills/af_audit_skill_apis.md @@ -79,9 +79,13 @@ loops. ### 3. Version baseline + drift-check -This workspace is pinned to an autofit version via `wiki/core/api_audit_baseline.json` -(per-module `__version__` + a hash of each module's public `dir()`). Two commands manage -it: +This workspace is pinned to an autofit API surface via `wiki/core/api_audit_baseline.json` +(a hash of each module's public `dir()`, plus per-module `__version__` recorded for +context). `--check-version` **gates on the API-surface hash only** — the version stamp is +shown but no longer fails the check, because since PyAutoConf#119 / PyAutoBuild#121 a +release does not commit the stamp back to library `main`, so a source checkout's frozen +stamp vs the wheel-derived baseline is a structurally-permanent false positive that the +hash already proves spurious. Two commands manage it: ```bash # Cheap drift-check — compares the installed stack to the committed baseline. @@ -99,7 +103,8 @@ baseline from a clean venv holding the released wheel (`env -u PYTHONPATH /bin/python autoassistant/audit_skill_apis.py --write-baseline`); `--allow-dev-stack` overrides deliberately. -The workflow is: `--check-version` flags that the installed autofit moved → run the full +The workflow is: `--check-version` flags that the installed autofit **public API surface** +moved → run the full `--scope all` audit to find what broke → fix the references (per the Branch section below) → `--write-baseline` to re-pin once the audit is clean. **Only re-pin after fixing**, never to silence a red drift-check on a stack you haven't audited. Because a wiki refresh