Why-it-belongs: top-up requires all three fields (idempotency fix)

Per Codex: generate_summary treated why_belongs alone as a complete explanation,
but get_explanation requires all three — so a partial older row (e.g. only
why_belongs) would never top up and the page would fall back forever. Now the
fully-cached check requires summary + what_happened + why_matters + why_belongs.
Test covers the partial-row top-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 20:10:27 -04:00
parent 337dc3f901
commit 008364e922
2 changed files with 30 additions and 3 deletions
+6 -3
View File
@@ -136,10 +136,13 @@ def generate_summary(conn: sqlite3.Connection, article_id: int, client: LocalMod
explanation is topped up on the next call (so existing pages gain the section). explanation is topped up on the next call (so existing pages gain the section).
""" """
existing = conn.execute( existing = conn.execute(
"SELECT summary, why_belongs FROM article_summaries WHERE article_id = ?", (article_id,) "SELECT summary, what_happened, why_matters, why_belongs FROM article_summaries WHERE article_id = ?",
(article_id,),
).fetchone() ).fetchone()
if existing and existing["summary"] and existing["why_belongs"]: # Fully cached only when the explanation is COMPLETE (all three) — matches
return existing["summary"] # summary + a complete explanation already cached # get_explanation(), so a partial older row gets topped up on the next call.
if existing and existing["summary"] and existing["what_happened"] and existing["why_matters"] and existing["why_belongs"]:
return existing["summary"]
row = conn.execute( row = conn.execute(
"SELECT a.title, a.description, a.canonical_url, a.duplicate_of, s.accepted " "SELECT a.title, a.description, a.canonical_url, a.duplicate_of, s.accepted "
"FROM articles a LEFT JOIN article_scores s ON s.article_id = a.id WHERE a.id = ?", "FROM articles a LEFT JOIN article_scores s ON s.article_id = a.id WHERE a.id = ?",
+24
View File
@@ -60,3 +60,27 @@ def test_share_renders_structured_or_fallback():
# no explanation → the single "Why its here" reason line is the calm fallback # no explanation → the single "Why its here" reason line is the calm fallback
fb = share.render_share_page(art, "http://b", summary="sum", explanation=None) fb = share.render_share_page(art, "http://b", summary="sum", explanation=None)
assert "Why its here" in fb and "calm reason" in fb assert "Why its here" in fb and "calm reason" in fb
def test_generate_tops_up_partial_explanation(tmp_path, monkeypatch):
c = connect(str(tmp_path / "p.db")); init_db(c); _seed_article(c)
# an older row: summary + why_belongs only (what_happened/why_matters missing)
c.execute("INSERT INTO article_summaries (article_id,summary,why_belongs) VALUES (1,'old summary','b')")
c.commit()
assert summarize.get_explanation(c, 1) is None # incomplete → falls back
calls = {"explain": 0}
def fake_explain(client, t, s, b):
calls["explain"] += 1
return {"what_happened": "w", "why_matters": "m", "why_belongs": "b2"}
monkeypatch.setattr(summarize, "explain_article", fake_explain)
monkeypatch.setattr(summarize, "_fetch_text", lambda url: "body")
class FakeClient:
model = "test"
def chat_text(self, messages): return "should not be used for summary (kept)"
out = summarize.generate_summary(c, 1, client=FakeClient())
assert out == "old summary" # existing summary reused, not regenerated
assert calls["explain"] == 1 # explanation WAS topped up
assert summarize.get_explanation(c, 1) == {"what_happened": "w", "why_matters": "m", "why_belongs": "b2"}