Initial commit
This commit is contained in:
commit
5f2db08b43
100 files changed
+16985
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"""Build a relevance context from a user's preferences (spec §5, DESIGN §4/§10).
|
||||
|
||||
The :class:`RelevanceContext` is the pure input to :func:`ranking.score_bill`.
|
||||
It is built from a user's stored preferences plus (optionally) the correlation
|
||||
lane's committee<->member join, so the "committee of a watched legislator"
|
||||
float path fires *only* when a real roster-backed committee exists for the
|
||||
bill (never fabricated).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from .ranking import Weights, DEFAULT_WEIGHTS
|
||||
|
||||
|
||||
def _slug(name):
|
||||
if not name:
|
||||
return ""
|
||||
return re.sub(r"[^a-z0-9]+", "-", name.strip().lower()).strip("-") or "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RelevanceContext:
|
||||
"""Pure input to the ranking for one logged-in user.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
watched_authors: lower-cased slugs of the primary author of each watched
|
||||
legislator (``author ∩ watched_legislators``).
|
||||
watched_committees: slugs of committees served by a watched legislator
|
||||
(committee-of-watched-legislator path). Built by :func:`from_prefs`
|
||||
when a committee-roster lookup is supplied; otherwise empty.
|
||||
opted_in_tags: lower-cased tag strings (``tags ∩ opted_in_tags``).
|
||||
home_chamber: optional chamber hint for the +1 score term.
|
||||
weights: scoring weights (tunable — Open Decision D3).
|
||||
"""
|
||||
|
||||
watched_authors: Set[str] = field(default_factory=set)
|
||||
watched_committees: Set[str] = field(default_factory=set)
|
||||
opted_in_tags: Set[str] = field(default_factory=set)
|
||||
home_chamber: Optional[str] = None
|
||||
weights: Weights = field(default_factory=lambda: DEFAULT_WEIGHTS)
|
||||
|
||||
def score(self, bill: Dict) -> Dict:
|
||||
"""Score one bill against this context."""
|
||||
from .ranking import score_bill
|
||||
return score_bill(
|
||||
bill,
|
||||
self.watched_authors,
|
||||
self.watched_committees,
|
||||
self.opted_in_tags,
|
||||
home_chamber=self.home_chamber,
|
||||
weights=self.weights,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_personalised(self) -> bool:
|
||||
"""True when the user has set a watch or opt-in (DESIGN §6.3 gate)."""
|
||||
return bool(self.watched_authors or self.watched_committees
|
||||
or self.opted_in_tags)
|
||||
|
||||
|
||||
def from_prefs(prefs,
|
||||
committee_roster=None,
|
||||
weights=None) -> RelevanceContext:
|
||||
"""Build a :class:`RelevanceContext` from a stored preferences blob.
|
||||
|
||||
``prefs`` matches API_CONTRACT §3.3::
|
||||
|
||||
{ "watched_legislators": ["L-asm-14", ...],
|
||||
"opted_in_tags": ["waste", "health"],
|
||||
"home_chamber": "ASSEMBLY",
|
||||
"districts": [...] }
|
||||
|
||||
``committee_roster`` optionally maps a ``legislator_id`` -> list of
|
||||
committee slugs that legislator chairs/serves in, so the
|
||||
committee-of-watched path can be derived. It is looked up against the
|
||||
correlation lane by the caller (:mod:`scraper.dashboard.data`); when
|
||||
absent the committee path is empty (honest degradation — the correlation
|
||||
lane found no roster, and we never fabricate membership).
|
||||
"""
|
||||
prefs = prefs or {}
|
||||
ctx = RelevanceContext(weights=weights or DEFAULT_WEIGHTS)
|
||||
|
||||
# Key the author-matching set on both the id-as-given and its slug so a
|
||||
# bill whose `author` column is the *name* still matches a name-based watch.
|
||||
for lid in prefs.get("watched_legislators") or []:
|
||||
s = _slug(lid)
|
||||
if s and s != "unknown":
|
||||
ctx.watched_authors.add(s)
|
||||
if isinstance(lid, str) and lid.strip():
|
||||
ctx.watched_authors.add(lid.strip().lower())
|
||||
|
||||
for tag in prefs.get("opted_in_tags") or []:
|
||||
t = str(tag).strip().lower()
|
||||
if t:
|
||||
ctx.opted_in_tags.add(t)
|
||||
|
||||
hc = prefs.get("home_chamber")
|
||||
if hc:
|
||||
ctx.home_chamber = str(hc).lower()
|
||||
|
||||
# Resolve committee-of-watched legs from the roster map (never fabricated).
|
||||
if committee_roster:
|
||||
for lid in prefs.get("watched_legislators") or []:
|
||||
committees = committee_roster.get(lid)
|
||||
if committees:
|
||||
for c in committees:
|
||||
cs = _slug(c)
|
||||
if cs and cs != "unknown":
|
||||
ctx.watched_committees.add(cs)
|
||||
return ctx
|
||||
Reference in new issue
Block a user