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:
+84
-1
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user