Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker

The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.

SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
  (a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
  rep (URL stability) -> quality score, so an accepted page never retires to a
  rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
  falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
  policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).

Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
  picker (bundled data, no CDN) for the blurb editor.

Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.

Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-18 11:32:27 -04:00
parent 2dbe73430c
commit 89c0fbe1f6
66 changed files with 6138 additions and 109 deletions
+255
View File
@@ -0,0 +1,255 @@
"""Bloom — the daily word wheel. Locks the design/acceptance split:
• DESIGN (deterministic, stored): wheel + tiers + pangram + Full-Bloom target,
from the COMMON list. The PERMANENT guardrail — Flourishing reachable with
common words — still holds.
• ACCEPTANCE (broad + dynamic): every valid word buildable from the wheel,
computed live as broad dict {allow} {block}; runtime admin overrides +
player reports drive curation with no deploy.
"""
import os
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from goodnews import bloom, games
from goodnews.db import connect, init_db
DATES = [f"2026-06-{d:02d}" for d in range(10, 25)] # 15 sample days
@pytest.fixture(scope="module")
def designs():
return {d: bloom.build_puzzle(d) for d in DATES}
@pytest.fixture
def conn(tmp_path):
c = connect(str(tmp_path / "t.db"))
init_db(c)
c.execute("INSERT INTO users (email) VALUES ('a@b.c')")
c.commit()
return c
def _letters(p):
return frozenset(p["center"]) | frozenset(p["outer"])
def _commons_for(p):
"""COMMON words for a center-mode wheel (the designed puzzle)."""
L = _letters(p)
return [w for w in bloom._COMMON if p["center"] in w and frozenset(w) <= L]
def _assert_no_answer_leak(resp):
assert "words" not in resp
assert resp["accepted"] and all(
isinstance(h, str) and len(h) == 64 and set(h) <= set("0123456789abcdef")
for h in resp["accepted"])
# --- DESIGN (deterministic, common-based) --------------------------------------
def test_build_is_deterministic():
assert bloom.build_puzzle("2026-06-15") == bloom.build_puzzle("2026-06-15")
@pytest.mark.parametrize("date", DATES)
def test_design_shape(designs, date):
p = designs[date]
L = _letters(p)
assert len(L) == 7 and "s" not in L
assert p["center"] in L and len(p["outer"]) == 6
assert bloom.MIN_COMMON_WORDS <= len(_commons_for(p)) <= bloom.MAX_COMMON_WORDS
assert frozenset(p["pangram"]) == L # display pangram uses all 7
assert p["pangram"] in bloom._COMMON and p["pangram"] not in bloom._AVOID
@pytest.mark.parametrize("date", DATES)
def test_PERMANENT_top_tier_reachable_with_common_words(designs, date):
"""Flourishing reachable from COMMON words alone — never obscure-word hunting."""
p = designs[date]
flourishing = next(t["score"] for t in p["tiers"] if t["name"] == "Flourishing")
assert bloom.score_words(p, _commons_for(p)) >= flourishing
def test_tiers_are_8_30_70_of_common_and_max_is_common_total():
p = bloom.build_puzzle("2026-06-15")
assert [t["name"] for t in p["tiers"]] == ["Sprouting", "Budding", "Blooming", "Flourishing"]
common_total = bloom.score_words(p, _commons_for(p))
assert p["max_score"] == common_total # Full Bloom = the designed puzzle
flour = next(t["score"] for t in p["tiers"] if t["name"] == "Flourishing")
assert flour == int(0.70 * common_total) and flour <= p["max_score"]
# --- ACCEPTANCE (broad + dynamic) ----------------------------------------------
def test_accept_is_broad_and_obeys_center_rule(conn):
p = bloom.build_puzzle("2026-06-15")
acc = bloom.accepted_words(conn, p["center"], p["outer"], require_center=True)
L = _letters(p)
for w in acc:
assert len(w) >= 4 and "s" not in w and frozenset(w) <= L and p["center"] in w
# broad accept is a SUPERSET of the common puzzle (bonus words beyond design)
assert set(_commons_for(p)) <= set(acc)
assert len(acc) > len(_commons_for(p))
def test_arraign_class_words_auto_accepted():
# broad dict includes real-but-rare words without any include-list
for w in ("arraign", "feign", "crwth"):
assert w in set(bloom.ACCEPT)
def test_overrides_block_and_allow(conn):
p = bloom.build_puzzle("2026-06-15")
acc0 = set(bloom.accepted_words(conn, p["center"], p["outer"], True))
victim = sorted(acc0)[0]
bloom.set_override(conn, victim, "block", by="t")
assert victim not in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
# allow a made-up letter-combo that fits the wheel + center
fake = (p["center"] + "".join(p["outer"][:3]))[:5]
if "s" not in fake and len(fake) >= 4:
bloom.set_override(conn, fake, "allow", by="t")
assert fake in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
bloom.clear_override(conn, victim)
assert victim in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
def test_allow_override_rejects_inert_hard_rule_words(conn):
# an allow that could never count (too short / has 's') is rejected, not stored
assert bloom.set_override(conn, "cat", "allow") is False # < 4 letters
assert bloom.set_override(conn, "roses", "allow") is False # contains 's'
assert bloom.set_override(conn, "bloom", "allow") is True # valid → stored
allow, _ = bloom.overrides(conn)
assert allow == {"bloom"}
# block stays permissive (can block anything)
assert bloom.set_override(conn, "roses", "block") is True
def test_wild_accepts_words_without_center(conn):
p = bloom.build_free("seed-w", "wild")
acc = bloom.accepted_words(conn, p["center"], p["outer"], require_center=False)
assert any(p["center"] not in w for w in acc) # Wild's defining trait
assert all(frozenset(w) <= _letters(p) for w in acc)
# --- responses + storage -------------------------------------------------------
def test_generate_is_idempotent_and_stored(conn):
a = bloom.generate_bloom_puzzle(conn, "2026-06-15")
assert a == bloom.generate_bloom_puzzle(conn, "2026-06-15") == bloom.stored_payload(conn, "2026-06-15")
assert "words" not in a # design payload holds no answers
def test_response_no_leak_and_hash_roundtrip(conn):
r = bloom.bloom_response(conn, "2026-06-15")
_assert_no_answer_leak(r)
p = bloom.stored_payload(conn, "2026-06-15")
real = bloom.accepted_words(conn, p["center"], p["outer"], True)[0]
assert bloom.word_hash("2026-06-15", real) in set(r["accepted"])
assert bloom.word_hash("2026-06-15", "zzzzq") not in set(r["accepted"])
assert r["max_score"] == p["max_score"]
def test_free_endpoint_resumes_and_leaks_nothing(api_app):
tc = TestClient(api_app)
r1 = tc.get("/api/puzzle/bloom/free?format=wild").json()
seed = r1["seed"]
assert r1["mode"] == "free" and r1["format"] == "wild" and seed
r2 = tc.get(f"/api/puzzle/bloom/free?format=wild&seed={seed}").json()
assert r2["center"] == r1["center"] and r2["outer"] == r1["outer"]
_assert_no_answer_leak(r1)
# --- server-side state ---------------------------------------------------------
def test_sanitize_drops_junk_recomputes_score_and_full(conn):
p = bloom.generate_bloom_puzzle(conn, "2026-06-15")
acc = bloom.accepted_words(conn, p["center"], p["outer"], True)
good = acc[:3]
clean = games.sanitize_game_state(conn, "bloom", "", "2026-06-15",
{"found": good + ["zzzz", "ab", good[0], 9], "score": 9999})
assert sorted(clean["found"]) == sorted(set(good))
assert clean["score"] == bloom.score_words(p, good)
assert "full" not in clean
# finding the whole common puzzle ⇒ Full Bloom (score ≥ max_score)
full = games.sanitize_game_state(conn, "bloom", "", "2026-06-15", {"found": _commons_for(p)})
assert full.get("full") is True
def test_merge_unions_found():
m = games.merge_game_state("bloom", {"found": ["able", "bake"]}, {"found": ["bake", "tale"]})
assert sorted(m["found"]) == ["able", "bake", "tale"]
def test_block_override_takes_effect_without_regen(conn):
# the live response reflects an override with no puzzle regeneration
p = bloom.generate_bloom_puzzle(conn, "2026-06-15")
victim = bloom.accepted_words(conn, p["center"], p["outer"], True)[0]
before = set(bloom.bloom_response(conn, "2026-06-15")["accepted"])
bloom.set_override(conn, victim, "block", by="t")
after = set(bloom.bloom_response(conn, "2026-06-15")["accepted"])
assert bloom.word_hash("2026-06-15", victim) in before
assert bloom.word_hash("2026-06-15", victim) not in after
# --- reports → admin queue → overrides -----------------------------------------
def test_report_then_approve_creates_allow_override(conn):
assert bloom.add_report(conn, "arraign", "2026-06-15", "daily", "center", "aceglnr", "not in the word list")
assert bloom.add_report(conn, "arraign", "2026-06-15", "daily", "center", "aceglnr", "x") # dedup pending
pending = bloom.list_reports(conn, "pending")
assert len(pending) == 1 and pending[0]["word"] == "arraign"
assert bloom.resolve_report(conn, pending[0]["id"], "approve", by="admin")
allow, _ = bloom.overrides(conn)
assert "arraign" in allow
assert not bloom.list_reports(conn, "pending")
assert bloom.list_reports(conn, "approved")
def test_report_block_creates_block_override(conn):
bloom.add_report(conn, "uglyword", None, "free", "wild", "abcdefg", "x")
rid = bloom.list_reports(conn, "pending")[0]["id"]
bloom.resolve_report(conn, rid, "block", by="admin")
_, block = bloom.overrides(conn)
assert "uglyword" in block
# --- API: public report + admin endpoints --------------------------------------
@pytest.fixture
def api_app(tmp_path, monkeypatch):
db = tmp_path / "t.sqlite3"
monkeypatch.setenv("GOODNEWS_DB", str(db))
monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver")
monkeypatch.setenv("GOODNEWS_ADMIN_EMAILS", "admin@b.com")
import importlib
import goodnews.api as api
importlib.reload(api)
c = connect(str(db)); init_db(c); c.commit(); c.close()
return api.create_app()
def _admin(app):
tc = TestClient(app)
sent = {}
import goodnews.email_send as es
orig = es.send_magic_link
es.send_magic_link = lambda to, link: sent.update(link=link)
try:
tc.post("/api/auth/email/start", json={"email": "admin@b.com"})
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
finally:
es.send_magic_link = orig
return tc
def test_public_report_then_admin_queue_flow(api_app):
pub = TestClient(api_app)
assert pub.post("/api/bloom/report", json={"word": "arraign", "date": "2026-06-15",
"mode": "daily", "format": "center", "letters": "aceglnr",
"reason": "not in the word list"}).json()["ok"]
# admin-only queue
assert TestClient(api_app).get("/api/admin/bloom/reports").status_code == 401
tc = _admin(api_app)
q = tc.get("/api/admin/bloom/reports").json()
assert len(q["reports"]) == 1
rid = q["reports"][0]["id"]
assert tc.post(f"/api/admin/bloom/reports/{rid}", json={"action": "approve"}).json()["ok"]
ovr = tc.get("/api/admin/bloom/reports").json()["overrides"]
assert any(o["word"] == "arraign" and o["action"] == "allow" for o in ovr)