Scope dial v2: Nearby / Region / Country / World radius on the homepage

Codex-approved evolution: the reader controls the "emotional radius" of the landing.

- Census-region "Regional" grain (geo.region_of / region_states). Scope-aware tiering
  (queries.home_tiers): closest->widest lead, confidence-gated on state + region, never
  a hard filter — blends outward so the set is always full. 'world' = the global brief.
- queries.home_brief takes a scope; /api/brief gains a scope param (nearby|region|
  country|world). Country-only / non-US homes collapse to country.
- Homepage dial replaces the 2-button toggle: adaptive stops (4 with a US state, else
  Country/World), persisted scope, "Good news closest first" framing. Concrete, soft
  section labels (Around New Jersey / Across the Northeast / Across the US / Around the
  world) so the reader sees the dial worked.

Backend 366 + frontend tests green. (Latest feed still on v1 local-first; aligning it
to the dial is the immediate follow-up.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-19 21:59:32 -04:00
parent d2a6293a13
commit 3486f3102a
5 changed files with 168 additions and 61 deletions
+6 -5
View File
@@ -2187,6 +2187,7 @@ def create_app() -> FastAPI:
prefs: str | None = Query(None),
exclude: str = Query("", description="comma-separated article ids the reader has dismissed"),
home: str | None = Query(None, max_length=8, description="local-first highlights: 'US' or 'US-NY'"),
scope: str = Query("nearby", pattern="^(nearby|region|country|world)$", description="radius dial"),
) -> BriefResponse:
# The default highlights are global (date-keyed, no session) → edge-cacheable
# so a new visitor's "Gathering the good news…" resolves from their POP, not
@@ -2203,13 +2204,13 @@ def create_app() -> FastAPI:
now = datetime.now(timezone.utc)
excl = {int(x) for x in exclude.split(",") if x.strip().lstrip("-").isdigit()}
with get_conn() as conn:
if home_country:
# The reader's home leads the landing: local good news first, blended out
# to country/world so it's always a full, sexy set. Over-fetch to survive
# dismissal/boundary filtering, then cap to limit.
if home_country and scope != "world":
# The reader's home + scope dial lead the landing: closest-first good news,
# blended outward so it's always a full set. Over-fetch to survive dismissal/
# boundary filtering, then cap to limit. scope='world' = the global brief.
meta = queries.brief(conn, brief_date=date, limit=1)
data = {"brief_date": meta["brief_date"], "title": "Close to home", "created_at": meta.get("created_at")}
pool = queries.home_brief(conn, home_country, home_state, limit=limit + 12)
pool = queries.home_brief(conn, home_country, home_state, scope=scope, limit=limit + 12)
else:
data = queries.brief(conn, brief_date=date, limit=limit)
pool = data["items"]
+28
View File
@@ -38,6 +38,34 @@ US_STATES = {
"district of columbia": "DC", "washington dc": "DC", "washington d c": "DC",
}
# US Census Bureau regions — the "Regional" grain for the scope dial. Standard,
# explainable, not arbitrary. DC sits in the South (South Atlantic) per Census.
US_REGIONS = {
"Northeast": {"CT", "ME", "MA", "NH", "RI", "VT", "NJ", "NY", "PA"},
"Midwest": {"IL", "IN", "MI", "OH", "WI", "IA", "KS", "MN", "MO", "NE", "ND", "SD"},
"South": {"DE", "FL", "GA", "MD", "NC", "SC", "VA", "DC", "WV", "AL", "KY", "MS",
"TN", "AR", "LA", "OK", "TX"},
"West": {"AZ", "CO", "ID", "MT", "NV", "NM", "UT", "WY", "AK", "CA", "HI", "OR", "WA"},
}
def region_of(state_code: str | None) -> str | None:
"""The Census region name containing a US state code, or None."""
if not state_code:
return None
sc = state_code.upper()
for name, states in US_REGIONS.items():
if sc in states:
return name
return None
def region_states(state_code: str | None) -> list[str]:
"""Sorted state codes in the same Census region as a state (incl. it)."""
name = region_of(state_code)
return sorted(US_REGIONS[name]) if name else []
# Common countries + aliases (extensible). Anything not here returns None -> we drop
# the country rather than store garbage. breadth still captures national/global, etc.
COUNTRY_TO_ISO = {
+54 -19
View File
@@ -259,24 +259,58 @@ def reindex_search(conn: sqlite3.Connection) -> int:
return conn.execute("SELECT COUNT(*) FROM article_search").fetchone()[0]
def home_brief(conn: sqlite3.Connection, home_country: str, home_state: str | None = None,
limit: int = 7, window_days: int = 3) -> list[dict]:
"""Local-first landing highlights. Leads with high/medium-confidence local good news,
then blends out to your country and the world so the set is always full (never the
sad thin-local look), and prefers already-summarized stories so the calm read stays
rich. Brief-shaped rows (incl. summary) tagged with a section, best-first within tier.
# Scope dial: the reader's "emotional radius". Each tier is a closest->widest lead
# preference, not a hard filter; 'world' is the implicit final tier. State + region
# are confidence-gated (high/medium) so a shaky location is never promoted as local.
_STATE_SQL = ("(g.confidence IN ('high','medium') AND EXISTS (SELECT 1 FROM article_places p "
"WHERE p.article_id = a.id AND p.country_code = ? AND p.state_code = ?))")
_COUNTRY_SQL = "(EXISTS (SELECT 1 FROM article_places p WHERE p.article_id = a.id AND p.country_code = ?))"
SCOPES = ("nearby", "region", "country", "world")
def _region_sql(n: int) -> str:
placeholders = ",".join("?" * n)
return ("(g.confidence IN ('high','medium') AND EXISTS (SELECT 1 FROM article_places p "
f"WHERE p.article_id = a.id AND p.country_code = ? AND p.state_code IN ({placeholders})))")
def home_tiers(home_country: str, home_state: str | None, scope: str) -> list[tuple]:
"""Ordered [(section_key, predicate_sql, params)] closest->widest for a home + scope.
Evaluated first-match (CASE WHEN / composed in order), so tiers needn't be SQL-exclusive.
'world' is implicit (everything not matched). 'region'/'nearby' need a US state; otherwise
they gracefully fall back to country (country-only / non-US homes collapse to Country/World).
"""
if home_state:
near = ("(g.confidence IN ('high','medium') AND EXISTS (SELECT 1 FROM article_places p "
"WHERE p.article_id = a.id AND p.country_code = ? AND p.state_code = ?))")
country = "EXISTS (SELECT 1 FROM article_places p WHERE p.article_id = a.id AND p.country_code = ?)"
section_case = f"CASE WHEN {near} THEN 0 WHEN {country} THEN 1 ELSE 2 END"
section_params = [home_country, home_state, home_country]
else:
near = ("(g.confidence IN ('high','medium') AND EXISTS (SELECT 1 FROM article_places p "
"WHERE p.article_id = a.id AND p.country_code = ?))")
section_case = f"CASE WHEN {near} THEN 0 ELSE 2 END" # no "country" tier without a state
section_params = [home_country]
from .geo import region_states
rs = region_states(home_state) if home_state else []
tiers: list[tuple] = []
if scope == "nearby" and home_state:
tiers.append(("state", _STATE_SQL, [home_country, home_state]))
if rs:
tiers.append(("region", _region_sql(len(rs)), [home_country, *rs]))
tiers.append(("country", _COUNTRY_SQL, [home_country]))
elif scope == "region" and home_state and rs:
tiers.append(("region", _region_sql(len(rs)), [home_country, *rs])) # includes the state
tiers.append(("country", _COUNTRY_SQL, [home_country]))
else: # country scope, country-only / non-US home, or any fallback
tiers.append(("country", _COUNTRY_SQL, [home_country]))
return tiers
def home_brief(conn: sqlite3.Connection, home_country: str, home_state: str | None = None,
scope: str = "nearby", limit: int = 7, window_days: int = 3) -> list[dict]:
"""Scope-aware local-first landing highlights. Leads with the reader's chosen radius
(state / region / country) then blends outward so the set is always full — "closest
first", never three stale local stories. Prefers already-summarized stories so the
calm read stays rich. Brief-shaped rows tagged with a concrete section key.
"""
tiers = home_tiers(home_country, home_state, scope)
whens, params = [], []
for i, (_key, pred, ps) in enumerate(tiers):
whens.append(f"WHEN {pred} THEN {i}")
params += ps
world_rank = len(tiers)
section_case = ("CASE " + " ".join(whens) + f" ELSE {world_rank} END") if whens else "0"
section_keys = [k for k, _, _ in tiers] + ["world"]
rows = conn.execute(
f"""
SELECT {_ARTICLE_COLUMNS},
@@ -294,12 +328,13 @@ def home_brief(conn: sqlite3.Connection, home_country: str, home_state: str | No
COALESCE(a.published_at, a.discovered_at) DESC
LIMIT ?
""",
section_params + [f"-{window_days} days", limit],
params + [f"-{window_days} days", limit],
).fetchall()
out = []
for r in rows:
d = dict(r)
d["__section"] = {0: "near", 1: "country", 2: "world"}.get(d.pop("section_rank", 2), "world")
rank = d.pop("section_rank", world_rank)
d["__section"] = section_keys[rank] if 0 <= rank < len(section_keys) else "world"
out.append(d)
return out