# TV Anarchy — Objectives (living backlog)

Running list of agreed objectives. Status: ✅ done (built+tested+staged) · 🟡 in
progress · ⬜ planned · 💭 designed (fleet package, future). Full specs in
`~/.claude/plans/woolly-tumbling-pelican.md`; fleet design in `.project/history/`.

## ✅ Done — built, tested, staged (green)
- **Consolidate** governor/mcp/recommender/search into the single repo; repoint via
  `RepoPaths`; in-repo `mcp→search` verified returning live magnets. (Part A)
- **Hosts → Devices** rename + 5 device types — cellphone · laptop · storage · seed ·
  **Broadcast Station** — each with an overridable `DeviceServices` set (stream ·
  offlineCache · ttlSeed · custody · publicSwarmFace · f2fRelay · meshAnchor),
  mirroring the fleet duties; legacy `hosts.json` infers type from kind. (Part B1)
- **Offline cache** — pull next-Y-episodes-of-recent-Z-shows to local disk
  (continue-watching / recently-added rules), rsync from black, capped. (Part B2)
- **Media-control forwarding** — Mac media keys / Control Center / lock screen /
  AirPods drive the active device + Now Playing; `forwardMediaKeys` setting. (Part B3)
- **Unified playlist** — playing a series episode queues the rest of the show from
  there (resume the first, rest from 0). **Continue Watching + Home rails now route
  through it too** (was the "S3 didn't queue after S2 Daria" bug — those paths
  single-launched one episode and queued nothing).
- **Season 0 = specials/movies**, ordered LAST (Daria's two movies after the run);
  shown as "Specials & Movies"; the unified queue puts them at the end.
- **Continue Watching rethink** — was severely broken (one row per *watched episode*,
  never advanced, junk `:Zone.Identifier` rows). Now **per-show**, pointing at the
  **next** episode (library-ordered → crosses season boundaries, specials last),
  most-recent **sustained** play wins (fly-by filter, mirrors the governor), finished
  shows drop off, junk filtered.
- **Watch-state core + UI** — `WatchState` (unwatched/inProgress/watched) +
  `nextUnwatched` + `isWatched`; **watched badges** on the Library grid + the
  **thumbnail-tap = "watch next"** / **title-tap = open detail** split (movie plays,
  series plays its next unwatched episode and queues the rest). *(Still pending was:
  rewatch button = reset watch state; searchable top tags; indicators on the other
  list formats — Home rails, detail.)* ✅ 2026-06-21.

## ⬜ Planned — specified, not yet built
- **Download manager (Part E):** `TransferHealth` ✅ → smart sort (attention + ETA) ✅
  (`downloadingSorted` + "needs attention" count + health pills) → ✅ system+in-app
  surfacing (ready-to-watch / stuck) → ✅ **per-torrent debug detail +
  reannounce/verify/pause actions** (mcp `tx-detail`/`tx-reannounce`/`tx-verify`/
  `tx-start`/`tx-stop`; `TorrentDetail`/`TrackerStat` models; `DownloadsController`
  detail+act; "Inspect / fix…" sheet showing error/peers/per-tracker announce +
  Reannounce/Verify/Pause-Resume — verified live against black) → ✅ dead-torrent
  re-search swap ("Find a better release": search title → rank by seeders → swap) 2026-06-21.
- ✅ **Settings UI** — media keys, offline (Y/Z/source), combine + local-LLM, hover
  previews, adult, all in the (renamed) **Settings** pane, one-persist on change.
- ✅ **Sidebar subnav** — Library → category links, auto-collapsed (shown only while
  Library is the active nav).
- **Search results as collections (Part E):** per-title summary cards (not flat rows),
  anchored to library/TMDB; collection page with **resolution selector** —
  movie = pick resolution → auto-pick best-seeded (one tap); series = **resolution ×
  season matrix** showing owned/available/downloading.
  ✅ Wired 2026-06-21 (basic grouping + best-add cards + alt menu in SearchView; full matrix/res selector UI scaffolded).
- **Search robustness (Part E):** launch-time warmup, per-source partial results,
  dependency health row, retry-after-warmup, prune dead sources.
  ✅ Core landed 2026-06-21 (D1 probes+health in Deps/Setup + better errors; D2 launch warmup wired + cli; D5 prune EXCLUDE; D3/D4 partial via existing python union + budgets; spec history followed; see SearchController + mcp/search.ts + Setup).
- **Settings hub (Part F):** summary/home page + dedicated pages; everything
  toggleable/tuneable via `AppSettings`; **device editing lives in Settings**
  (Devices view deep-links to it).
- **Auto-preview pre-warm (Part F):** generate hover previews ahead of time for
  likely-previewed content — rules: recently-added · visible-on-screen ·
  continue-watching · recommended; capped per pass, low-priority.
- **Watch-state + card interaction (new):**
  - **Watched indicators** in every list format (grid/rows/collection) — unwatched /
    in-progress / watched.
  - **Card tap split:** tapping the **thumbnail** = "**watch next**" (play the next
    unwatched episode straight away); tapping the **text/title** area = open the
    detail page (breadcrumb). Two targets on one card.
  - **Rewatch button** per collection/item — resets watch state so "watch next"
    starts over.
  - **Searchable top tags** — surface top tags/genres as chips that search on tap.
- **Dynamic show combination (new):** merge the split/duplicate library entries of
  one show into a single combined show/collection. Today `mergeSeriesByName` only
  merges *exact normalized name + same category + disjoint seasons*, so e.g.
  **Dandadan** stays as 3–4 entries: spacing variants ("DAN DA DAN" vs "DANDADAN"),
  cross-category (anime vs cartoons), and duplicate S1 releases (REPACK/Remux/BDRip)
  whose overlapping seasons make the merge treat them as different shows. **Rethink:
  metadata-anchored grouping** — resolve each entry to a canonical TMDB/enrich work
  (title+year+id); same work → combine + dedup episodes per season×episode (alt
  releases reachable via the quality switcher); different work → stay separate (so an
  anime + its live-action remake don't fuse). Same anchor model as search collections;
  feeds the shared catalog's "media definitions."
  - ✅ **Cheap stage + seam built:** `ShowGrouping.candidateClusters` blocks variants
    by a canonical key (alphanumeric — "DAN DA DAN"/"DANDADAN"/"Dan.Da.Dan" → one
    cluster); `ambiguousClusters` is the cheap→reasoner hand-off; the `ShowGrouper`
    seam (provider-agnostic: local LLM / TMDB) makes the final same-work call. Two-
    stage per the design: **easy algorithm for the bulk + LLM light reasoning on the
    ambiguous tail** (the same role the unfilled `TitleRefiner` MLX seam was built for).
  - ✅ **Local-LLM reasoner BUILT + wired (no TMDB):** installed **MLX**
    (`mlx-lm` + `Qwen2.5-1.5B-Instruct-4bit`) into the recommender uv env — plum now
    has a working local LLM. `recommender/media_rec/grouper.py` reasons over a
    candidate cluster (correctly groups Dandadan's cross-category dupes); Swift
    `LocalLLMGrouper` shells into it like `enrich`; `ShowGroupCache`/
    `CachedGroupDecider` persist decisions (once per cluster);
    `ShowGrouping.combine`+`merge` apply them (dedup episodes per season×episode, a
    year-gap guard against a small model over-merging a remake);
    `LibraryController.combineSplitShows()` runs it off-main after load/scan, gated by
    the `combineSplitShows` setting.
  - ✅ **Shipped default is now deterministic (zero MB), LLM optional.** The measured
    1.5B model is 839 MB — too big to ship (no git-lfs; repo .git is 2.5 MB). The
    same-work signal is *structural*: `DeterministicGrouper` combines same-canonical-
    key entries with close release years (dupes + split seasons) and excludes
    year-distant remakes — no model. It's the default in `combineSplitShows`; the MLX
    `LocalLLMGrouper` is gated behind `useLLMGrouper` (off) for the rare same-year
    ambiguous tail. *Future tiny-ship option:* a Model2Vec static embedding (~30 MB,
    CPU-instant) for fuzzy title recall ("Frieren" ≈ "Sousou no Frieren") — beats a
    generative model for this task and is genuinely shippable.
- **Sidebar subnav (Part G):** Library → category links, Settings → its pages, with
  auto-collapse per nav.
- **Cleanup (Part C, LAST):** remove the stale worktrees + delete the external repos
  (plum-control-mcp, portable-net-tv, media-recommender, torrent-search-mcp) once the
  port is verified. Nothing irreversible until then.

## 💭 Designed — fleet PACKAGE (future, not app core)
> Keep the mesh *engine* out of `TVAnarchyCore`; the app consumes its outputs. Lives
> in `fleet/` (package).
- **Networking — two planes** (spec: `.project/history/20260608_fleet-manager-mesh-design.md`
  → "Networking — two independent planes"). Separate interfaces, never conflated:
  - **Plane 1 — Fleet WireGuard (internal mesh):** auto-provisioned (package mints keys/
    subnet/peers, thin per-OS actuator raises the interface — Linux `wg-quick`+systemd;
    macOS `wg-quick` under sudo over a utun; iOS manual/out-of-path), **tv-anarchy-only**
    (`AllowedIPs`=fleet subnet, `Table=off`, no default route — a scoped mesh, NOT a VPN),
    **never-conflict via a runtime route/interface/port collision-probe** + dedicated
    `tva0` name. Promoted to an **early foundational stage** because one stable fleet IP
    per device (home-or-away) kills today's LAN-vs-VPN juggling ([[black-endpoint-lan-vpn]];
    the transmission LAN→VPN fallback is the interim). Single-fleet trust = trivial (own
    all devices, no M-of-N). *Open Q: is `10.9.0.4` general infra (additive) or just the
    tv-anarchy overlay (migration)?*
  - **Plane 2 — Commercial VPN via uploaded `.ovpn` (public-swarm exit):** the
    `public_swarm_face` egress that keeps the home IP dark.
    - ✅ **Import + manage UI BUILT** (Settings → "VPN (public-swarm exit)"): import
      individual `.ovpn` OR a provider **zip** (`ditto` unpack → harvest every `.ovpn`
      + its sidecar certs → group by zip name in `~/.config/tv-anarchy/vpn/`),
      `OVPNParser` surfaces remote/proto/needs-login/inline-vs-external-certs,
      per-provider login stored in the **Keychain** (`VPNCredentialStore`, never on
      disk), list/group/delete. `VPNController` + `VPNConfigStore`; parser/scan/zip
      tested (`OVPNParserTests`, 8). Management only — no actuation yet.
    - ⬜ **Select active config per `public_swarm_face` device** + **actuation** =
      OpenVPN on the always-on Linux node, **split-routed** (only public-swarm torrent
      egress through `tun`, not a full-device VPN). Coexists with plane-1 WG on a
      separate interface/route scope. (Needs the fleet duty engine.)
- **Mesh registry + duty assignment** — `peers_for(infohash)` / `custodians_of(title)`,
  auto-built from activity (watch = auto-seed, completion = custody).
- **Shared catalog / "public data drive"** — registry lists + media/collection
  definitions (e.g. "The Matrix collection") + optional cover art; **written only by
  mesh anchors (Broadcast Stations), read by all, distributed AS a torrent**; a
  signed, **versioned** catalog pointer.
- **Anchor trust** — M-of-N existing anchors co-sign a new writer's key (vouching);
  reuse WireGuard peer identity; signed revocation list; founder-key bootstrap.
- **Custody floor + reaper** — ≥N copies per wanted title; re-pin before the last
  copy vanishes; reaper resurrects dead titles from the mesh first, public re-search
  fallback. (System-level "improve health".)
- **Cover-art sharing** across the mesh (a payload of the shared catalog).
- **Friend subscription + mutual promotion** — A invites B, B accepts → a bidirectional,
  **signed, revocable** `Friendship` (on the WireGuard-key identity + Discord) carrying
  a per-pair share/promote policy: serve each other custody/streaming (bandwidth tier 2),
  union each other into `peers_for`, relay each other's F2F requests, and boost each
  other's content in discovery/recs. Either side revokes.
- **Anonymized adult sharing** — help friends watch porn you hold *without it being
  attributable to you*: serve it **content-addressed (by `ContentID`/infohash, not "my
  files")**, **via F2F relay** (source identity stripped hop-by-hop), with a **raised
  k-anonymity threshold** (never surfaced below K distinct holders). The friend gets the
  bytes by hash; the mesh never reveals *who* has it. A per-`porn` sharing mode on top
  of the data boundaries.
- ✅ **Content hashing (`ContentID`) — BUILT.** Canonical, library-agnostic episode id
  (`canonicalKey(work)/sNNeNN`, quality opt-in) so the same episode across libraries
  (different rips/folders) normalizes to one id; + a SHA-256 short digest that leaks no
  title. The keystone for dedup, `peers_for(id)`, k-anonymity counting, and the
  anonymized content-addressed serving above. (Byte-exact swarm identity stays the
  torrent infohash at the transmission layer.)
- **Data boundaries + bandwidth tiers** ✅ pure models built (`DataDomain`:
  tvanarchy/user/publicMedia + shareability; `BandwidthPolicy`: you > friends-when-idle
  > public, Travel-Mode = all-to-you). ⬜ Actuation: mcp upload-control verbs
  (`transmission-remote -u/-U`, per-torrent), laptop magnet-add for swarm augmentation,
  governor enforcement.

## 💭 For later — recommendation engine
- **"Collection ideas like this"** — per-collection "more like this" recommendations
  (recommender / media-recommender). Surfaced from a collection item.

## 💭 2026-06-23 Council Expansions (from session 019ef387... planning)
- Net "signal" part (light recent-watch aggregates, k-anon, local MVP in Phase 1.5) for Library badges + Download hints.
- Consensual anonymous adult sharing ("porn relay"): raised-K contentKey-only part, F2F relay stripping, never surfaced below threshold (aligns operator trust-and-safety domain).
- Install health dashboard (per-device last-seen/quota/net-contrib) + "synced" skip badge in Player when marker from merged Net.
- Net peers-hints boost "Find better" re-search in Downloads detail.
- Exhaustive "fleet" → "install" terminology sweep as explicit pre-Phase-1 gate (product term; internal fleet.json/WG stay).
- All grounded in current shipped (library titles+fix, Winamp, mesh join), respect contentKey-only / k-anon / adult redlines / one-shell / pillar DRY. Full synthesis + handoff in `.project/history/20260623_v2-council-expansions-planning.md`.

## Standing directives
- Keep this list current as objectives are added/finished.
- Keep building while waiting for responses; don't stall on questions.
- Separate core app code from ideas that should be packages.
- Thoroughly test each feature; keep the tree green at every boundary.
