Files
thejayman77 89c0fbe1f6 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>
2026-06-18 11:32:27 -04:00

318 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bloom — the daily word wheel (Center Circle / Wild Bloom).
DESIGN and ACCEPTANCE are decoupled:
• DESIGN (wheel selection, tiers, pangram, the Full-Bloom target) uses the small
COMMON list only — deterministic, stored in daily_puzzles, and unaffected by
curation. Tiers are scored on COMMON so "Flourishing" is always reachable with
everyday vocabulary, and "Full Bloom" = finding the whole *designed* puzzle
(the broad bonus words are extra credit beyond it, never required).
• ACCEPTANCE is BROAD and DYNAMIC — every valid dictionary word buildable from
the wheel, computed at RESPONSE TIME as: broad dict {allow} {block}, where
allow/block are runtime admin overrides (bloom_word_overrides). So a missed
word can be allowed (or a junk word blocked) with NO deploy or regeneration.
Accept words never sit in the network response: clients validate against salted
hashes and compute their own score/tier/pangram from the 7 letters.
"""
from __future__ import annotations
import hashlib
import json
import random
import sqlite3
from itertools import combinations
from pathlib import Path
_DATA = Path(__file__).parent / "data"
_W = json.loads((_DATA / "bloom_words.json").read_text())
ACCEPT: list[str] = _W["accept"] # broad: all valid dictionary words
_COMMON: set[str] = set(_W["common"]) # tight: design / tiers / pangrams only
_COMMON_LS: list[tuple[str, frozenset]] = [(w, frozenset(w)) for w in _COMMON]
_AVOID: set[str] = set(json.loads((_DATA / "bloom_avoid.json").read_text()))
# Broad accept words bucketed by distinct-letter set, so the accepted set for a
# 7-letter wheel is gathered by unioning its ≤127 letter-subsets (fast) — no scan
# of the whole ~68k list per request.
_BY_SET: dict[frozenset, list[str]] = {}
for _w in ACCEPT:
_BY_SET.setdefault(frozenset(_w), []).append(_w)
# Candidate wheels = letter-sets of 7-distinct-letter COMMON words (every wheel
# has ≥1 recognizable pangram). Sorted for deterministic order.
_PANGRAM_SETS: dict[frozenset, list[str]] = {}
for _w in _COMMON:
_s = frozenset(_w)
if len(_s) == 7:
_PANGRAM_SETS.setdefault(_s, []).append(_w)
_CANDIDATES: list[frozenset] = sorted(_PANGRAM_SETS, key=lambda s: "".join(sorted(s)))
MIN_COMMON_WORDS, MAX_COMMON_WORDS = 14, 45
PANGRAM_BONUS = 7
# 8 / 30 / 70 — Flourishing at 70% keeps Bloom from becoming a completionist
# grind. Do NOT raise Flourishing above 0.70 (Codex).
TIER_PCTS: tuple[tuple[str, float], ...] = (
("Sprouting", 0.0), ("Budding", 0.08), ("Blooming", 0.30), ("Flourishing", 0.70),
)
TOP_TIER_PCT = 0.70
def score_word(word: str) -> int:
"""4-letter word = 1 point; longer = its length. Pangram bonus added on top."""
return 1 if len(word) == 4 else len(word)
def score_words(payload: dict, words) -> int:
"""Score found words for a wheel (pangram = uses all 7 letters). Used for the
player's running score AND the Full-Bloom check (vs the design's max_score)."""
letters = frozenset(payload["center"]) | frozenset(payload["outer"])
total = 0
for w in words:
total += score_word(w)
if frozenset(w) == letters:
total += PANGRAM_BONUS
return total
# --- DESIGN: common-only, deterministic, stored --------------------------------
def tiers_for(common_max: int) -> list[dict]:
return [{"name": n, "score": int(p * common_max)} for n, p in TIER_PCTS]
def _design(letters: frozenset, center: str):
"""Center-mode design from the COMMON list only."""
commons = [w for (w, s) in _COMMON_LS if center in w and s <= letters]
pangrams = [w for w in commons if frozenset(w) == letters]
common_max = sum(score_word(w) for w in commons) + PANGRAM_BONUS * len(pangrams)
display = sorted((p for p in pangrams if p not in _AVOID), key=lambda p: (len(p), p))
return commons, display, common_max
def _design_wild(letters: frozenset):
"""Wild design (no required center) from the COMMON list only."""
commons = [w for (w, s) in _COMMON_LS if s <= letters]
pangrams = [w for w in commons if frozenset(w) == letters]
common_max = sum(score_word(w) for w in commons) + PANGRAM_BONUS * len(pangrams)
display = sorted((p for p in pangrams if p not in _AVOID), key=lambda p: (len(p), p))
vowels = [c for c in sorted(letters) if c in "aeiou"]
return commons, display, common_max, (vowels[0] if vowels else sorted(letters)[0])
def _payload(letters: frozenset, center: str, display, common_max: int) -> dict:
return {
"center": center,
"outer": sorted(letters - {center}),
"pangram": display[0],
"tiers": tiers_for(common_max),
# Full Bloom = finding the whole designed (common) puzzle; broad bonus
# words push score past this but are never required.
"max_score": common_max,
}
def _generate(seed_str: str, fmt: str) -> dict:
"""Deterministically pick a wheel design for a seed + format."""
rng = random.Random(int(hashlib.sha256(seed_str.encode()).hexdigest(), 16))
order = _CANDIDATES[:]
rng.shuffle(order)
for letters in order:
if fmt == "wild":
commons, display, cmax, center = _design_wild(letters)
if len(commons) >= MIN_COMMON_WORDS and display:
return _payload(letters, center, display, cmax)
else:
centers = sorted(letters)
rng.shuffle(centers)
for center in centers:
commons, display, cmax = _design(letters, center)
if MIN_COMMON_WORDS <= len(commons) <= MAX_COMMON_WORDS and display:
return _payload(letters, center, display, cmax)
raise RuntimeError("bloom: no valid wheel found") # impossible with the vendored dict
def build_puzzle(date: str) -> dict:
"""The day's shared Center Circle wheel design (deterministic by date)."""
return {"date": date, **_generate(f"bloom:{date}", "center")}
def build_free(seed: str, fmt: str = "center") -> dict:
"""A free-play wheel design (deterministic by seed) — Center Circle or Wild."""
fmt = "wild" if fmt == "wild" else "center"
return {"seed": seed, "format": fmt, **_generate(f"free:{fmt}:{seed}", fmt)}
# --- ACCEPTANCE: broad + runtime overrides, computed at response time ----------
def overrides(conn: sqlite3.Connection) -> tuple[set, set]:
allow, block = set(), set()
for r in conn.execute("SELECT word, action FROM bloom_word_overrides"):
(allow if r["action"] == "allow" else block).add(r["word"])
return allow, block
def _broad_words_for(letters: frozenset) -> list[str]:
"""Every broad-dictionary word buildable from `letters` (distinct-set ⊆ letters)."""
ls = sorted(letters)
out = []
for r in range(1, len(ls) + 1):
for combo in combinations(ls, r):
out.extend(_BY_SET.get(frozenset(combo), ()))
return out
def accepted_words(conn: sqlite3.Connection, center: str, outer, require_center: bool) -> list[str]:
"""The wheel's accepted set RIGHT NOW: broad words buildable from the letters
(optionally requiring the center), plus allow-overrides, minus block-overrides."""
letters = frozenset(outer) | {center}
allow, block = overrides(conn)
seen, out = set(), []
for w in _broad_words_for(letters):
if w in seen or w in block:
continue
if require_center and center not in w:
continue
seen.add(w)
out.append(w)
for w in allow: # allow words that may not be in the broad dict
if w in seen or w in block or len(w) < 4 or "s" in w:
continue
if not (frozenset(w) <= letters) or (require_center and center not in w):
continue
seen.add(w)
out.append(w)
return sorted(out)
# --- daily_puzzles storage -----------------------------------------------------
def generate_bloom_puzzle(conn: sqlite3.Connection, date: str) -> dict:
"""Ensure the day's Bloom DESIGN exists in daily_puzzles. Idempotent, pure code."""
existing = conn.execute(
"SELECT payload_json FROM daily_puzzles WHERE puzzle_date=? AND game='bloom' AND variant=''", (date,)
).fetchone()
if existing:
return json.loads(existing["payload_json"])
payload = build_puzzle(date)
conn.execute(
"INSERT OR IGNORE INTO daily_puzzles (puzzle_date, game, variant, payload_json) VALUES (?, 'bloom', '', ?)",
(date, json.dumps(payload)),
)
conn.commit()
row = conn.execute(
"SELECT payload_json FROM daily_puzzles WHERE puzzle_date=? AND game='bloom' AND variant=''", (date,)
).fetchone()
return json.loads(row["payload_json"])
def stored_payload(conn: sqlite3.Connection, date: str) -> dict | None:
"""The day's design IF it already exists — never generates (used by the state
sanitizer, which must not trigger generation)."""
row = conn.execute(
"SELECT payload_json FROM daily_puzzles WHERE puzzle_date=? AND game='bloom' AND variant=''", (date,)
).fetchone()
return json.loads(row["payload_json"]) if row else None
def word_hash(salt: str, word: str) -> str:
return hashlib.sha256(f"{salt}:{word}".encode()).hexdigest()
def _response(salt: str, p: dict, words: list[str], extra: dict) -> dict:
return {
"game": "bloom",
"center": p["center"],
"outer": p["outer"],
"accepted": [word_hash(salt, w) for w in words], # NO plaintext words leak
"max_score": p["max_score"], # Full Bloom = designed puzzle
"tiers": p["tiers"],
**extra,
}
def bloom_response(conn: sqlite3.Connection, date: str) -> dict:
"""Daily Center Circle — accepted set computed live (broad + overrides)."""
p = generate_bloom_puzzle(conn, date)
words = accepted_words(conn, p["center"], p["outer"], require_center=True)
return _response(date, p, words, {"date": date})
def bloom_free_response(conn: sqlite3.Connection, seed: str, fmt: str) -> dict:
"""Free-play wheel keyed by `seed` (resumable). Accepted set computed live."""
p = build_free(seed, fmt)
words = accepted_words(conn, p["center"], p["outer"], require_center=p["format"] != "wild")
return _response(seed, p, words, {"mode": "free", "format": p["format"], "seed": p["seed"]})
# --- runtime curation: overrides + player reports ------------------------------
def set_override(conn: sqlite3.Connection, word: str, action: str, reason: str | None = None,
by: str | None = None) -> bool:
word = (word or "").strip().lower()
if not (word.isalpha() and action in ("allow", "block")):
return False
# An ALLOW that violates Bloom's hard rules (≥4 letters, no 'S') could never
# count — reject it rather than store an inert override. BLOCK stays permissive.
if action == "allow" and (len(word) < 4 or "s" in word):
return False
conn.execute(
"INSERT INTO bloom_word_overrides (word, action, reason, created_by) VALUES (?,?,?,?) "
"ON CONFLICT(word) DO UPDATE SET action=excluded.action, reason=excluded.reason, "
"created_by=excluded.created_by, created_at=CURRENT_TIMESTAMP",
(word, action, reason, by),
)
conn.commit()
return True
def clear_override(conn: sqlite3.Connection, word: str) -> None:
conn.execute("DELETE FROM bloom_word_overrides WHERE word=?", ((word or "").strip().lower(),))
conn.commit()
def list_overrides(conn: sqlite3.Connection) -> list[dict]:
return [dict(r) for r in conn.execute(
"SELECT word, action, reason, created_by, created_at FROM bloom_word_overrides ORDER BY created_at DESC")]
def add_report(conn: sqlite3.Connection, word: str, puzzle_date, mode, fmt, letters, reason) -> bool:
word = (word or "").strip().lower()
if not (word.isalpha() and 4 <= len(word) <= 24):
return False
# Don't pile up duplicate pending reports for the same word.
dup = conn.execute(
"SELECT 1 FROM bloom_word_reports WHERE word=? AND status='pending'", (word,)).fetchone()
if dup:
return True
conn.execute(
"INSERT INTO bloom_word_reports (word, puzzle_date, mode, format, letters, reason) "
"VALUES (?,?,?,?,?,?)",
(word, str(puzzle_date or "")[:16], str(mode or "")[:8], str(fmt or "")[:8],
str(letters or "")[:16], str(reason or "")[:60]),
)
conn.commit()
return True
def list_reports(conn: sqlite3.Connection, status: str = "pending", limit: int = 100) -> list[dict]:
return [dict(r) for r in conn.execute(
"SELECT id, word, puzzle_date, mode, format, letters, reason, status, created_at "
"FROM bloom_word_reports WHERE status=? ORDER BY created_at DESC LIMIT ?", (status, limit))]
def resolve_report(conn: sqlite3.Connection, report_id: int, action: str, by: str | None = None) -> bool:
"""action: 'approve' (→ allow override) | 'block' (→ block override) | 'dismiss'."""
status = {"approve": "approved", "block": "blocked", "dismiss": "dismissed"}.get(action)
row = conn.execute("SELECT word FROM bloom_word_reports WHERE id=?", (report_id,)).fetchone()
if not row or not status:
return False
if action == "approve":
if not set_override(conn, row["word"], "allow", reason="report", by=by):
return False # can't allow (hard rule) — leave pending; dismiss instead
elif action == "block":
set_override(conn, row["word"], "block", reason="report", by=by)
conn.execute("UPDATE bloom_word_reports SET status=? WHERE id=?", (status, report_id))
conn.commit()
return True