"""Nav lanes — the customizable quick-access rail above the feed. "Today" is always pinned (the daily brief). Past that, a reader pins whichever lanes they like from a pool of three kinds, all of which the feed already knows how to render as a view: * moods — curated filter presets (see moods.py); key is the bare mood key. * topics — the eight primary categories; key is the bare topic key. * tags — high-volume grouping tags (Discovery); key is "tag:". Single source of truth so the website and any future companion app agree on the pool and the default selection. The pool's tag membership is volume-gated so we never offer a lane that would land the reader on an empty feed. """ from __future__ import annotations from .moods import MOODS from .taxonomy import ALLOWED_TAGS, TOPICS # The lanes pinned first, always — never user-removable. "Highlights" is the # curated daily brief (key 'today'); "Latest" is the chronological accepted feed. PINNED = [ {"key": "today", "label": "Highlights", "description": "The day's curated good things."}, {"key": "latest", "label": "Latest", "description": "Freshest calm reads, newest first."}, ] # What a reader who has never customized sees: today's curated moods, unchanged. DEFAULT_LANES: list[str] = [m["key"] for m in MOODS if m["key"] != "today"] # A grouping tag joins the Discovery pool once it clears this many accepted # articles — except the curated few we always want offered (they fill forward). _TAG_MIN_COUNT = 60 _ALWAYS_OFFER_TAGS = ("space", "food") # Tags that duplicate a primary topic are dropped from Discovery to avoid two # pills that mean the same thing. _TAG_EXCLUDE = set(TOPICS) def _humanize(slug: str) -> str: return slug.replace("-", " ").title() def build_lane_pool(topic_counts: dict[str, int], tag_counts: dict[str, int]) -> dict: """Assemble the selectable pool, annotated with live counts. topic_counts / tag_counts map key -> number of accepted articles, so the client can show volume and we can volume-gate the Discovery tags. """ moods = [ {"key": m["key"], "label": m["label"], "description": m["description"]} for m in MOODS if m["key"] != "today" ] topics = [ {"key": k, "label": _humanize(k), "description": v, "count": int(topic_counts.get(k, 0))} for k, v in TOPICS.items() ] discovery = [ {"key": f"tag:{t}", "label": _humanize(t), "count": int(tag_counts.get(t, 0))} for t in ALLOWED_TAGS if t not in _TAG_EXCLUDE and (tag_counts.get(t, 0) >= _TAG_MIN_COUNT or t in _ALWAYS_OFFER_TAGS) ] discovery.sort(key=lambda d: (-d["count"], d["label"])) return { "pinned": PINNED, "default": DEFAULT_LANES, "groups": [ {"name": "Moods", "lanes": moods}, {"name": "Topics", "lanes": topics}, {"name": "Discovery", "lanes": discovery}, ], } def known_lane_keys(pool: dict) -> set[str]: """Every selectable key in a pool (excluding the always-pinned 'today').""" return {lane["key"] for group in pool["groups"] for lane in group["lanes"]}