89c0fbe1f6
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>
83 lines
3.4 KiB
Python
83 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Prototype Bloom (Center Circle) generator — prints real sample wheels so we
|
|
can feel the quality before building any UI. The validated logic here becomes
|
|
goodnews/bloom.py."""
|
|
import hashlib
|
|
import json
|
|
import random
|
|
from pathlib import Path
|
|
|
|
_DATA = Path(__file__).resolve().parents[1] / "goodnews" / "data"
|
|
d = json.loads((_DATA / "bloom_words.json").read_text())
|
|
ACCEPT = d["accept"]
|
|
COMMON = set(d["common"])
|
|
ACCEPT_LS = [(w, frozenset(w)) for w in ACCEPT]
|
|
# Off-brand words we never CELEBRATE as the day's pangram (accept-list unaffected).
|
|
AVOID = set(json.loads((_DATA / "bloom_avoid.json").read_text()))
|
|
|
|
# Candidate wheels = letter-sets of COMMON 7-distinct-letter words (so the day's
|
|
# pangram is always a recognizable word). No 'S' already guaranteed by the list.
|
|
PANGRAM_SETS: dict[frozenset, list[str]] = {}
|
|
for w in COMMON:
|
|
s = frozenset(w)
|
|
if len(s) == 7:
|
|
PANGRAM_SETS.setdefault(s, []).append(w)
|
|
|
|
MIN_WORDS, MAX_WORDS, MIN_COMMON, TOP_TIER = 24, 60, 14, 0.70
|
|
|
|
|
|
def score(w: str) -> int:
|
|
return 1 if len(w) == 4 else len(w)
|
|
|
|
|
|
def build(letters: frozenset, center: str):
|
|
words = [w for w, s in ACCEPT_LS if center in w and s <= letters]
|
|
pangrams = [w for w in words if frozenset(w) == letters]
|
|
commons = [w for w in words if w in COMMON]
|
|
max_score = sum(score(w) for w in words) + 7 * len(pangrams)
|
|
common_score = sum(score(w) for w in commons) + 7 * len([w for w in pangrams if w in COMMON])
|
|
return words, pangrams, commons, max_score, common_score
|
|
|
|
|
|
def valid(letters, center):
|
|
words, pangrams, commons, max_score, common_score = build(letters, center)
|
|
if not (MIN_WORDS <= len(words) <= MAX_WORDS):
|
|
return None
|
|
# The DISPLAY pangram must be calm + recognizable: common, not on the avoid
|
|
# list. (Off-brand pangrams like LUCIFER/VOMITING are still accepted if typed,
|
|
# just never the day's celebrated word.)
|
|
display = [p for p in pangrams if p in COMMON and p not in AVOID]
|
|
if not display or len(commons) < MIN_COMMON:
|
|
return None
|
|
if common_score < TOP_TIER * max_score: # top tier reachable from common vocab
|
|
return None
|
|
return words, sorted(display, key=len), commons, max_score, common_score
|
|
|
|
|
|
def generate(date: str):
|
|
rng = random.Random(int(hashlib.sha256(f"bloom:{date}".encode()).hexdigest(), 16))
|
|
sets = list(PANGRAM_SETS)
|
|
rng.shuffle(sets)
|
|
for s in sets:
|
|
centers = sorted(s)
|
|
rng.shuffle(centers)
|
|
for c in centers:
|
|
res = valid(s, c)
|
|
if res:
|
|
return s, c, res
|
|
return None
|
|
|
|
|
|
print(f"loaded accept={len(ACCEPT)} common={len(COMMON)} | candidate wheels={len(PANGRAM_SETS)}\n")
|
|
for date in ("2026-06-15", "2026-06-16", "2026-06-17", "2026-06-18", "2026-06-19"):
|
|
s, c, (words, pangrams, commons, max_score, common_score) = generate(date)
|
|
outer = sorted(s - {c})
|
|
tiers = {"Sprouting": 0, "Budding": int(0.08 * max_score),
|
|
"Blooming": int(0.30 * max_score), "Flourishing": int(0.70 * max_score)}
|
|
longest = sorted(words, key=len, reverse=True)[:3]
|
|
sample = sorted(random.Random(1).sample(words, min(16, len(words))))
|
|
print(f"── {date} ── center [{c.upper()}] outer {[x.upper() for x in outer]}")
|
|
print(f" words={len(words)} (common={len(commons)}) pangram(s)={[p.upper() for p in pangrams]}")
|
|
print(f" max_score={max_score} tiers={tiers}")
|
|
print(f" longest={longest} sample={sample}\n")
|