From 1df02d524c6f0bdd0fc66746fc974493e87a9891 Mon Sep 17 00:00:00 2001 From: GeneAI Date: Fri, 8 May 2026 21:08:14 -0400 Subject: [PATCH] chore(polish): migrate model alias to claude-sonnet-4-6 (deadline 2026-06-15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic deprecated `claude-sonnet-4-20250514` with EOL 2026-06-15 (V2/V3 verification probes on 2026-05-08 surfaced the deprecation warning). Bumps the polish-model alias from the dated checkpoint to the family alias `claude-sonnet-4-6`, matching attune-rag's existing `ClaudeProvider.DEFAULT_MODEL` pin and Anthropic's recommended migration path. Audit found two hard-coded model pins, both bumped: - polish.py:_POLISH_MODEL — drives every polish call (sync + batch) - doc_gen/config.py:DocGenConfig.model — drives `attune-author docs` Test fixtures pinning the old name in 3 files also bumped (test_cli_batch.py, test_maintenance_batch.py, test_polish_improvements.py). Cache invalidation is automatic: _POLISH_MODEL participates in the polish cache key, so existing entries become orphaned (remain on disk, never match the new key namespace) and the existing 30-day TTL prune reaps them organically. First regen post-upgrade is a cold-cache miss for every feature; subsequent regens behave normally. Users wanting immediate disk reclaim can run `attune-author cache clear` (optional, documented in CHANGELOG). New `tests/test_polish_smoke.py` (5 tests) pins the wire-level model name across both code paths so the next deprecation fires a clear "wire contract is stale" signal at CI time. No API spend (mocks at the SDK boundary). Verification (live, ~$0.04 total): - Synchronous regen: 10 features, 30 polish calls all 200 OK. - Batch path: submit + resume cycle for 1 stale feature, 3/3 succeeded, templates spliced correctly. - Cache invalidation: cache went 23→53 entries; 30 new under new SHA-key namespace, 23 old remain for TTL prune. - Full suite: 675 pass, 32 skipped (live + skill-export integration). Ruff clean. Spec: specs/polish-model-migration/{requirements,design,tasks}.md Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 37 ++++++++ pyproject.toml | 2 +- src/attune_author/__init__.py | 2 +- src/attune_author/doc_gen/config.py | 2 +- src/attune_author/polish.py | 2 +- tests/test_cli_batch.py | 4 +- tests/test_maintenance_batch.py | 2 +- tests/test_polish_improvements.py | 2 +- tests/test_polish_smoke.py | 128 ++++++++++++++++++++++++++++ 9 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 tests/test_polish_smoke.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e54dfc..d4af0bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,43 @@ and this project adheres to Work in progress for the next release. Add entries here as changes land, not at tag time. +## [0.11.1] - 2026-05-08 + +### Changed + +- **Polish model rename: `claude-sonnet-4-20250514` → + `claude-sonnet-4-6`.** Anthropic deprecated the dated alias + with EOL 2026-06-15; the V2/V3 verification probes on + 2026-05-08 surfaced the deprecation warning. Migrating to + the family alias matches attune-rag's existing + `ClaudeProvider.DEFAULT_MODEL` and tracks Anthropic's + recommended migration path. Affected: + - [`polish.py:_POLISH_MODEL`](src/attune_author/polish.py) + — drives every polish call (synchronous + batch). + - [`doc_gen/config.py:DocGenConfig.model`](src/attune_author/doc_gen/config.py) + — drives `attune-author docs` generation. +- **One-time cold cache after upgrade.** `_POLISH_MODEL` + participates in the polish cache key, so existing cached + entries become orphaned (they remain on disk but never + match the new key namespace). The first regen after + upgrade is a cold-cache miss for every feature; subsequent + regens behave normally. Existing 30-day TTL prune + ([polish._cache_prune](src/attune_author/polish.py)) + reaps orphaned entries automatically. + +### Added + +- **Mocked smoke test** at `tests/test_polish_smoke.py` + pinning the wire-level model name. Catches the next + model-rename failure immediately, no API spend. + +### Notes + +- Users who want to reclaim disk space immediately rather + than waiting for the TTL prune can run + `attune-author cache clear` post-upgrade. **Optional** — + the cache shrinks automatically over 30 days. + ## [0.11.0] - 2026-05-08 ### Added diff --git a/pyproject.toml b/pyproject.toml index 7d58974..2baa2af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "attune-author" -version = "0.11.0" +version = "0.11.1" description = "Documentation authoring and maintenance for the attune ecosystem — generate, maintain, and validate help content with AI assistance." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3.10" diff --git a/src/attune_author/__init__.py b/src/attune_author/__init__.py index 2bac38f..120edeb 100644 --- a/src/attune_author/__init__.py +++ b/src/attune_author/__init__.py @@ -5,7 +5,7 @@ attune-help (reader) and attune-ai (full dev workflows). """ -__version__ = "0.11.0" +__version__ = "0.11.1" from attune_author.manifest import Feature, Manifest, load_manifest from attune_author.staleness import StalenessReport, check_staleness, compute_source_hash diff --git a/src/attune_author/doc_gen/config.py b/src/attune_author/doc_gen/config.py index fad2344..153c56e 100644 --- a/src/attune_author/doc_gen/config.py +++ b/src/attune_author/doc_gen/config.py @@ -24,7 +24,7 @@ class DocGenConfig: doc_type: str = "api-reference" audience: str = "developers" - model: str = "claude-sonnet-4-20250514" + model: str = "claude-sonnet-4-6" max_outline_tokens: int = 1000 max_write_tokens: int = 8000 max_review_tokens: int = 8000 diff --git a/src/attune_author/polish.py b/src/attune_author/polish.py index 547d2bc..431891c 100644 --- a/src/attune_author/polish.py +++ b/src/attune_author/polish.py @@ -51,7 +51,7 @@ #: Anthropic model used for the polish pass. Hoisted to a #: module-level constant so it participates in the cache key — #: bumping the model invalidates cache entries automatically. -_POLISH_MODEL = "claude-sonnet-4-20250514" +_POLISH_MODEL = "claude-sonnet-4-6" #: Env var that overrides the default polish cache directory. _CACHE_DIR_ENV = "ATTUNE_AUTHOR_POLISH_CACHE" diff --git a/tests/test_cli_batch.py b/tests/test_cli_batch.py index cb21e31..d7876da 100644 --- a/tests/test_cli_batch.py +++ b/tests/test_cli_batch.py @@ -39,7 +39,7 @@ def _state(batch_id: str = "msgbatch_test") -> BatchState: batch_id=batch_id, submitted_at=datetime(2026, 5, 8, 18, 35, tzinfo=timezone.utc), expected_completion_at=datetime(2026, 5, 8, 18, 41, tzinfo=timezone.utc), - model="claude-sonnet-4-20250514", + model="claude-sonnet-4-6", requests=( BatchStateRequest("feat__auth__concept", "auth", "concept"), BatchStateRequest("feat__auth__task", "auth", "task"), @@ -217,7 +217,7 @@ class TestStatus: "batch_id": "msgbatch_status", "submitted_at": "2026-05-08T18:35:00+00:00", "expected_completion_at": "2026-05-08T18:41:00+00:00", - "model": "claude-sonnet-4-20250514", + "model": "claude-sonnet-4-6", "request_count": 3, "processing_status": "in_progress", "request_counts": { diff --git a/tests/test_maintenance_batch.py b/tests/test_maintenance_batch.py index 743e042..f846f70 100644 --- a/tests/test_maintenance_batch.py +++ b/tests/test_maintenance_batch.py @@ -58,7 +58,7 @@ def _state(submitted_at: datetime | None = None) -> BatchState: batch_id="msgbatch_test", submitted_at=submitted_at or datetime(2026, 5, 8, 18, 35, tzinfo=timezone.utc), expected_completion_at=datetime(2026, 5, 8, 18, 41, tzinfo=timezone.utc), - model="claude-sonnet-4-20250514", + model="claude-sonnet-4-6", requests=( BatchStateRequest("feat__auth__concept", "auth", "concept"), BatchStateRequest("feat__auth__task", "auth", "task"), diff --git a/tests/test_polish_improvements.py b/tests/test_polish_improvements.py index 7671047..8245ef4 100644 --- a/tests/test_polish_improvements.py +++ b/tests/test_polish_improvements.py @@ -255,7 +255,7 @@ def test_api_key_is_redacted_from_error_messages(self) -> None: fake_client, system="s", user_message="u", - model="claude-sonnet-4-20250514", + model="claude-sonnet-4-6", max_tokens=32, ) diff --git a/tests/test_polish_smoke.py b/tests/test_polish_smoke.py new file mode 100644 index 0000000..e4e757d --- /dev/null +++ b/tests/test_polish_smoke.py @@ -0,0 +1,128 @@ +"""Smoke test pinning the polish wire-level model name. + +Lives outside the broader polish test suite specifically to +catch the next model-rename failure immediately. When Anthropic +deprecates the current model alias and we need to bump +`_POLISH_MODEL` again, this test fires a clear "the wire +contract is stale" signal so the bump is mechanical. + +The test mocks at ``attune_author.doc_gen._anthropic.call_anthropic`` +so no API tokens are spent. +""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from attune_author.polish import _POLISH_MODEL, polish_template + +# The current value the codebase is pinned to. Update *both* this +# constant and `_POLISH_MODEL` together when migrating; the +# co-located assertion catches half-migrations where someone +# bumps one and forgets the other. +_EXPECTED_MODEL = "claude-sonnet-4-6" + + +def test_polish_model_constant_matches_expected() -> None: + """Sentinel: ``_POLISH_MODEL`` is the model alias we expect. + + Failure mode: someone bumped the constant in source without + bumping ``_EXPECTED_MODEL`` here. Update both together. + """ + assert _POLISH_MODEL == _EXPECTED_MODEL + + +def test_polish_template_calls_sdk_with_expected_model() -> None: + """Wire-contract test: the model passed to ``call_anthropic`` + matches ``_POLISH_MODEL`` (and thus our expected alias). + + Pinning the wire-level call shape — not just the constant — + catches refactors that bypass the constant or pin the + model elsewhere in the call site. + + Forces a cache miss via patched ``_cache_get`` so the LLM + call path always runs (otherwise on a hot polish cache the + SDK never gets called). + """ + with patch("attune_author.polish._cache_get", return_value=None): + with patch( + "attune_author.polish.call_anthropic", return_value="polished body" + ) as mock_call: + with patch("attune_author.polish.get_client") as mock_client: + mock_client.return_value = object() + result = polish_template( + content="# Sample\n\nBody.\n", + feature_name="sample", + source_summary="def f(): pass", + template_type="concept", + strict=True, + ) + + # polish_template runs _sanitize_output, which may add a trailing newline. + # Assert content rather than exact equality so the test isn't brittle. + assert result.strip() == "polished body" + mock_call.assert_called_once() + kwargs = mock_call.call_args.kwargs + assert kwargs["model"] == _EXPECTED_MODEL, ( + f"polish call went out with model={kwargs['model']!r}, " + f"expected {_EXPECTED_MODEL!r}. If the model was intentionally bumped, " + "update _EXPECTED_MODEL in tests/test_polish_smoke.py." + ) + + +def test_polish_template_uses_constant_not_hardcoded() -> None: + """Belt-and-suspenders: re-pinning ``_POLISH_MODEL`` to a + different value flows through to the SDK call. + + If someone hard-codes a model name at the call site instead + of reading the constant, this test fails because the call + will use the constant we monkey-patched but the assertion + expects the new value. + """ + sentinel = "claude-test-sentinel-9" + with patch("attune_author.polish._cache_get", return_value=None): + with patch("attune_author.polish._POLISH_MODEL", sentinel): + with patch("attune_author.polish.call_anthropic", return_value="ok") as mock_call: + with patch("attune_author.polish.get_client") as mock_client: + mock_client.return_value = object() + polish_template( + content="# X\n\nY\n", + feature_name="x", + source_summary="", + template_type="concept", + strict=True, + ) + + assert mock_call.call_args.kwargs["model"] == sentinel + + +def test_doc_gen_config_default_model_matches_expected() -> None: + """Same sentinel for the second polish-adjacent code path: + the `attune-author docs` subcommand uses `DocGenConfig.model` + as its default model. Bumps must keep it aligned with the + polish constant. + """ + from attune_author.doc_gen import DocGenConfig + + assert DocGenConfig().model == _EXPECTED_MODEL, ( + f"DocGenConfig default model is {DocGenConfig().model!r}, " + f"expected {_EXPECTED_MODEL!r}. The two model pins should track together." + ) + + +@pytest.mark.parametrize( + "deprecated_alias", + [ + # Historical: previously-pinned model that hit EOL 2026-06-15. + # Re-using this list as a regression seed if we ever migrate again. + "claude-sonnet-4-20250514", + ], +) +def test_no_deprecated_model_aliases_in_constants(deprecated_alias: str) -> None: + """Ensure deprecated aliases don't sneak back via copy-paste.""" + from attune_author.doc_gen import DocGenConfig + + assert _POLISH_MODEL != deprecated_alias + assert DocGenConfig().model != deprecated_alias