Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/attune_author/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/attune_author/doc_gen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/attune_author/polish.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cli_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_maintenance_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 1 addition & 1 deletion tests/test_polish_improvements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
128 changes: 128 additions & 0 deletions tests/test_polish_smoke.py
Original file line number Diff line number Diff line change
@@ -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
Loading