# Plan — universal per-video tags ("all streams where Eva collabs", on any site, any channel) Approved by the operator 2026-09-21. Implemented as three slices (S1 core, S2 site, S3 editor) after a frozen S1.1 lands on main. ## Context The operator wants to open Anilyzer and narrow any video list to "streams where Eva collabs", including streams on channels that are not hers (Legal Mindset, Destiny, Nux Taku…), with a mechanism that is generic: any person/topic, any site, applied by the operator, by an agent, or by umtool. Today the only cross-channel curation on a site is channel **groups** (`sites//site.json` `groups`; Anilyzer's "Elfpire Eva" group holds only her own two channels) — whole channels, nothing per video. The corpus has three unrelated things called "tags" already (yt-dlp keyword `TranscriptSummary.tags`, digest topic tags, the `"tags"` search scope), which is the naming hazard this plan works around. Research on 2026-09-20 produced the first concrete tag set: 7 metadata-flagged LM collabs, ~19 LM streams discussing her, ~64 with her in chat. ## Operator decisions (2026-09-21) 1. **Assignment — all three**: by hand (video toggle + bulk "Tag selected"), by **rules** on a tag (re-evaluated at index build; operator pins/suppresses exceptions), by **import from a umtool project**. 2. **One tag per kind**: `eva-collab` (on mic), `eva-in-chat`, `eva-topic`; filter is OR across chips. 3. **Vocabulary both corpus-wide and per site, like aliases** — corpus file authoritative for definitions AND assignments; the site file is presentation ONLY (relabel/hide/order/colour). **Amended 2026-09-21 during S1**: a site-layer rule can never tag a record — a record is shared by every site carrying its channel — so rules bind only from the corpus file and the site tab offers no rule editor. 4. **Surfaces — all four**: export lists (channel + all-videos), export search, MCP filter, editor lists. 5. **Universal + flexible, three writers**: free-form tag ids for any subject, optional `group` so chips read "Eva: Collab · In chat · Discussed"; operator UI, `pnpm ops` (agents; MCP stays read-only per AGENTS.md), umtool — all through ONE server action, every assignment carrying provenance. 6. **The export lists selectable tags dynamically** (operator, 2026-09-21): a site's chip row is built from its published `/tags.json`, and compose writes only tags with a non-zero count ON THAT SITE — a global tag with no matches on a site does not appear there, nor does a hidden one. No static list anywhere. ## Naming | Surface | Name | |---|---| | UI word, files, CLI, wire | **tags**: `transcripts/tags.json`, `sites//tags.json`, published `/tags.json`, `pnpm ops tags`, `pnpm ops tag-videos` | | Record field on shards | **`curatedTags?: string[]`** (never `tags` — `common/lib/transcripts.ts:21` is yt-dlp keywords) | | Modules / types | `common/lib/curatedTags.ts`, `common/lib/curatedTagsStore.ts`, `common/controller/curatedTagsIndex.ts`; `CuratedTagDef`, `CuratedTagRule`, `CuratedTagAssignment`, `CuratedTagsConfig`, `PublishedTags` | | Existing keyword scope | UI label "Tags" → **"Keywords"** at `common/components/QueryLeafView.tsx:46` (`SCOPE_LABELS`) and `:156-159` (placeholder); code token `"tags"`, URL and MCP `scopes` enum unchanged | Tag ids: `^[a-z0-9][a-z0-9._-]*$`. Nothing in code names Eva; she is seed data (§ Seed). ## Data model **`transcripts/tags.json`** (authoritative; `writeJsonAtomic`; `sanitizeTagsConfig` on read): ```json { "version": 1, "tags": [ { "id": "eva-collab", "label": "Collab", "group": "eva", "groupLabel": "Eva", "description": "Eva is on mic", "color": "#b48ead", "order": 1, "hidden": false, "rules": [ { "id": "meta", "kind": "metadata", "pattern": "elfpire|elf ?pire ?eva", "channels": [], "dateFrom": null, "dateTo": null, "enabled": true } ] } ], "assignments": { "legal-mindset/XZqL6k9IHGA": { "manual": ["eva-collab"], "suppressed": ["eva-topic"], "sources": { "eva-collab": { "source": "umtool:elfpire-eva", "setAt": "2026-09-21T18:04:11Z" }, "eva-topic": { "source": "operator", "setAt": "2026-09-21T18:05:02Z" } } } } } ``` - `source` ∈ `operator | agent: