From 3051f6c04ee004b4f736bffb397c600d54515c51 Mon Sep 17 00:00:00 2001 From: GeneAI Date: Fri, 10 Jul 2026 00:59:20 -0400 Subject: [PATCH 1/2] =?UTF-8?q?fix(polish-summaries):=20Batch=20API=20cust?= =?UTF-8?q?om=5Fid=20charset=20=E2=80=94=20hash=20ids,=20path=20in=20featu?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First live run 400'd: custom_id must match ^[a-zA-Z0-9_-]{1,64}$ and 'sum__cli/concept.md' doesn't. custom_id becomes sum_; the readable rel path rides in the request/result 'feature' field and apply_results maps through that instead of parsing the id. Regression test pins the charset. Co-Authored-By: Claude Fable 5 --- src/attune_author/summaries_polish.py | 18 ++++++++++++---- tests/test_summaries_polish.py | 31 ++++++++++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/attune_author/summaries_polish.py b/src/attune_author/summaries_polish.py index 8d169be..50fffa9 100644 --- a/src/attune_author/summaries_polish.py +++ b/src/attune_author/summaries_polish.py @@ -131,8 +131,16 @@ def polish_model() -> str: def custom_id_for(rel_path: str) -> str: - """Stable Anthropic ``custom_id`` for one template path.""" - return f"sum__{rel_path}" + """Stable Anthropic ``custom_id`` for one template path. + + The Batch API requires ``^[a-zA-Z0-9_-]{1,64}$`` — template paths + (slashes, dots) violate it, so the id is a path hash. The readable + path rides in the request's ``feature`` field, which ``poll_batch`` + copies onto each result; ``apply_results`` maps results back + through THAT, never by parsing the id. (Learned from a live 400 on + the first attune-ai run, 2026-07-10.) + """ + return f"sum_{_sha256(rel_path)[:24]}" def build_requests(help_dir: Path, model: str, *, force: bool = False) -> BuildReport: @@ -172,8 +180,10 @@ def build_requests(help_dir: Path, model: str, *, force: bool = False) -> BuildR ) requests.append( BatchPolishRequest( + # feature carries the FULL relative path — apply_results + # maps batch results back through it (see custom_id_for). custom_id=custom_id_for(rel_path), - feature=str(Path(rel_path).parent), + feature=rel_path, depth=Path(rel_path).stem, system=_SYSTEM, user_message=user_message, @@ -232,7 +242,7 @@ def apply_results( stamp = (_now or (lambda: datetime.now(timezone.utc)))().isoformat() counters = {"applied": 0, "truncated": 0, "errored": 0, "empty": 0} for result in results: - rel_path = result.custom_id.removeprefix("sum__") + rel_path = result.feature # full relative path — see custom_id_for if result.error is not None or result.text is None: counters["errored"] += 1 continue diff --git a/tests/test_summaries_polish.py b/tests/test_summaries_polish.py index 6f9b2cf..d405471 100644 --- a/tests/test_summaries_polish.py +++ b/tests/test_summaries_polish.py @@ -40,7 +40,8 @@ def test_builds_one_request_per_entry(self, tmp_path): report = sp.build_requests(help_dir, "claude-sonnet-5") assert len(report.requests) == 2 req = report.requests[0] - assert req.custom_id == "sum__cli/concept.md" + assert req.custom_id == sp.custom_id_for("cli/concept.md") + assert req.feature == "cli/concept.md" # apply maps results via this assert req.model == "claude-sonnet-5" assert "Current (mechanical) summary: seed" in req.user_message assert "Body prose here." in req.user_message @@ -78,7 +79,7 @@ def test_orphaned_sidecar_key_ignored(self, tmp_path): sidecar["gone/nope.md"] = "orphan" (help_dir / "summaries.json").write_text(json.dumps(sidecar)) report = sp.build_requests(help_dir, "claude-sonnet-5") - assert [r.custom_id for r in report.requests] == ["sum__cli/concept.md"] + assert [r.feature for r in report.requests] == ["cli/concept.md"] class TestModelAndCost: @@ -126,8 +127,8 @@ def test_strips_wrapping_quotes(self): class TestApplyResults: def _result(self, rel: str, text: str | None, error: str | None = None) -> BatchPolishResult: return BatchPolishResult( - custom_id=f"sum__{rel}", - feature=str(Path(rel).parent), + custom_id=sp.custom_id_for(rel), + feature=rel, depth=Path(rel).stem, text=text, error=error, @@ -286,8 +287,8 @@ def test_source_hash_falls_back_to_body_sha(self, tmp_path): help_dir, [ BatchPolishResult( - custom_id="sum__cli/concept.md", - feature="cli", + custom_id=sp.custom_id_for("cli/concept.md"), + feature="cli/concept.md", depth="concept", text="Polished.", error=None, @@ -306,8 +307,8 @@ def test_apply_truncates_overlong_result_and_flags_meta(self, tmp_path): help_dir, [ BatchPolishResult( - custom_id="sum__cli/concept.md", - feature="cli", + custom_id=sp.custom_id_for("cli/concept.md"), + feature="cli/concept.md", depth="concept", text=long_text, error=None, @@ -358,3 +359,17 @@ def fake_poll(batch_id, expected, **kwargs): assert sidecar["cli/concept.md"] == "Resumed polish." assert not (help_dir / ".summaries-batch-state.json").exists() assert "applied 1" in capsys.readouterr().out + + +class TestCustomIdContract: + def test_matches_batch_api_charset(self): + """The Batch API enforces ^[a-zA-Z0-9_-]{1,64}$ — a raw template + path (slashes, dots) 400s the whole submit. Hit live 2026-07-10.""" + import re + + for rel in ("cli/concept.md", "elicitation-forms/troubleshooting.md"): + cid = sp.custom_id_for(rel) + assert re.fullmatch(r"[a-zA-Z0-9_-]{1,64}", cid), cid + + def test_distinct_paths_get_distinct_ids(self): + assert sp.custom_id_for("a/b.md") != sp.custom_id_for("a/c.md") From ab1fc540f881189580e111e07b586f9736ef4239 Mon Sep 17 00:00:00 2001 From: GeneAI Date: Fri, 10 Jul 2026 01:01:38 -0400 Subject: [PATCH 2/2] style(tests): pinned-black formatting for test_anthropic_fable.py Co-Authored-By: Claude Fable 5 --- tests/test_anthropic_fable.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_anthropic_fable.py b/tests/test_anthropic_fable.py index c7fd6df..7930dad 100644 --- a/tests/test_anthropic_fable.py +++ b/tests/test_anthropic_fable.py @@ -137,9 +137,7 @@ def test_fable_response_leading_thinking_block_skipped() -> None: is the answer. Hit live 2026-07-10 via the commit regen hook.""" thinking = SimpleNamespace(thinking="reasoning...", type="thinking") text_block = SimpleNamespace(text="the answer", type="text") - response = SimpleNamespace( - content=[thinking, text_block], stop_reason="end_turn", usage=None - ) + response = SimpleNamespace(content=[thinking, text_block], stop_reason="end_turn", usage=None) client = MagicMock() client.beta.messages.create.return_value = response assert (