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
+84 -1
View File
@@ -243,6 +243,11 @@ def poll_source(conn: sqlite3.Connection, source: sqlite3.Row) -> dict:
}
# Deep-preview accessibility sample bounds (module-level so tests can shrink them).
_ACCESS_FETCH_TIMEOUT = 6 # per-article socket timeout (seconds)
_ACCESS_DEADLINE_S = 12.0 # hard wall-clock cap for the whole access phase
def preview_feed(url: str, sample: int = 25, pr_risk_default: int = 3, client=None, fetcher=None) -> dict:
"""Fetch and score a sample of a feed WITHOUT persisting anything.
@@ -302,12 +307,85 @@ def preview_feed(url: str, sample: int = 25, pr_risk_default: int = 3, client=No
cortisol=ns["cortisol_score"],
ragebait=ns["ragebait_score"],
pr_risk=ns["pr_risk_score"],
reason_code=ns["reason_code"],
language=ns.get("language", ""),
)
except Exception:
pass # one bad item shouldn't sink the whole preview
total = len(rows)
accepted = sum(1 for r in rows if r["accepted"])
# Non-English items are HELD (English-only feed for now), not calm-filter
# rejections — surface the count and judge acceptance over English items only, so
# a multilingual wire (e.g. PR Newswire) isn't unfairly penalized in the preview.
non_english = sum(1 for r in rows if r.get("reason_code") == "non_english")
judged = total - non_english
# Accessibility sample — deep preview only (it already means "spend ~a minute to
# really know"). Layered per Codex: the instant DOMAIN rule + a small sampled
# article fetch, so a paywall verdict rests on evidence, not domain alone (NYT
# Learning proved domain rules false-positive).
from .paywall import check_article_access, is_paywalled
domain_paywalled = is_paywalled(url)
access = None
access_verdict = None
if classified and rows:
from concurrent.futures import ThreadPoolExecutor, as_completed
# prefer the URLs the model would actually surface, then fill from the rest
ordered = [r["url"] for r in rows if r["accepted"] and r["url"]] + \
[r["url"] for r in rows if not r["accepted"] and r["url"]]
seen, sample_urls = set(), []
for u in ordered:
if u not in seen:
seen.add(u)
sample_urls.append(u)
if len(sample_urls) >= 6:
break
results = []
if sample_urls:
af = fetcher or fetch_feed
ex = ThreadPoolExecutor(max_workers=min(6, len(sample_urls)))
futs = {ex.submit(check_article_access, u, af, _ACCESS_FETCH_TIMEOUT): u for u in sample_urls}
done = {}
try:
# Hard wall-clock cap: the access step can NEVER stall the whole
# preview. Fetches run in parallel; whatever hasn't finished by the
# deadline is left 'unknown' (unverified — never counts as walled).
# shutdown(wait=False, cancel_futures=True) below means we don't block
# on stragglers (no `with ... as ex` join), so wall-clock == the cap.
for fut in as_completed(futs, timeout=_ACCESS_DEADLINE_S):
done[futs[fut]] = fut.result()
except Exception: # noqa: BLE001 — overall deadline hit; use what finished
pass
ex.shutdown(wait=False, cancel_futures=True)
results = [(u, done.get(u, "unknown")) for u in sample_urls]
counts = Counter(a for _, a in results)
readable, paywalled = counts.get("readable", 0), counts.get("paywalled", 0)
assessable = readable + paywalled
inacc = (paywalled / assessable) if assessable else None
# `blocked` is deliberately NOT counted as inaccessible: a bot-block isn't a
# reader paywall (it may open fine in a browser), so it can never push a
# source to reject-ready — only readable-vs-paywalled evidence does. Need a
# few clearly-assessable samples before judging confidently.
ENOUGH = 3
if assessable < ENOUGH:
access_verdict = "review" # mostly blocked/unknown — can't confirm; click examples
elif domain_paywalled and inacc >= 0.7:
access_verdict = "reject-ready" # domain rule AND sample agree it's walled
elif domain_paywalled:
access_verdict = "review" # domain says walled but the sample isn't — likely a false positive, look
elif inacc >= 0.7:
access_verdict = "review" # not on the list but mostly walled — candidate for the rule
elif inacc <= 0.3:
access_verdict = "fine"
else:
access_verdict = "review" # mixed
access = {
"checked": len(results),
"readable": readable, "paywalled": paywalled,
"blocked": counts.get("blocked", 0), "unknown": counts.get("unknown", 0),
"examples": [{"url": u, "access": a} for u, a in results][:5],
}
def _avg(key: str) -> float:
return round(sum(r[key] for r in rows) / total, 1) if total else 0.0
@@ -329,12 +407,17 @@ def preview_feed(url: str, sample: int = 25, pr_risk_default: int = 3, client=No
"sampled": total,
"classified": classified,
"accepted": accepted,
"acceptance_rate": round(accepted / total, 2) if total else 0.0,
"non_english": non_english, # held for language (English-only feed for now)
# None (not 0%) when there are no English items to judge — "all held", not "all rejected".
"acceptance_rate": round(accepted / judged, 2) if judged else None,
"avg_cortisol": _avg("cortisol"),
"avg_ragebait": _avg("ragebait"),
"avg_pr_risk": _avg("pr_risk"),
"newest_published": newest,
"recent_7d": recent_7d,
"paywall_rule": domain_paywalled, # instant domain hint
"access": access, # sampled readable/paywalled/blocked/unknown (deep only)
"access_verdict": access_verdict, # fine | review | reject-ready
"topic_mix": dict(Counter(r["topic"] for r in rows if r["topic"])),
"flavor_mix": dict(Counter(r["flavor"] for r in rows if r["flavor"])),
"examples_accepted": [r["title"] for r in rows if r["accepted"]][:5],