// Server-side persistence for curated per-video tags. Two source files: // - global: paths.globalTagsFile (/tags.json) // - per-site: sites//tags.json (siteTagsFile) // The global file is AUTHORITATIVE for both the vocabulary and the assignments // (an assignment is a fact about a video, not a presentation choice). The // per-site file is PRESENTATION — relabel/recolour/reorder/hide — plus // site-only tags. See common/lib/curatedTags.ts for the pure model, the // coercion and mergeTagDefs. // // **A site-layer RULE never tags a record, so a site layer carries none.** // Rules are evaluated once at index time over records SHARED by every site that // carries the channel — there is no per-site record for a per-site hit to live // in, so a rule written at the site layer could only ever LOOK like it worked. // The site coercion therefore DROPS rules outright: on READ, so a hand-edited // sites//tags.json cannot smuggle one in, and on WRITE, so neither can a // caller — and mergeTagDefs has no rule branch left to append through. Rules // bind from transcripts/tags.json alone; promote one there to make it fire. // This is why the editor offers no site-side rule editor. // // Unlike aliasesStore there are NO seeded defaults: a fresh install has no // tags, and an absent global file reads as an empty config. // // ─── The four ops of applyTagAssignments ─── // // A tag lands on a video two ways: a RULE matched (re-evaluated at every index // build, never persisted) or the operator PINNED it. So "take this tag off this // video" has two distinct meanings and they are different ops: // // add pin the tag, and clear any suppression of it // remove unpin only — a rule that matches this video still tags it // suppress reject the tag: unpin it AND record a suppression, which is the // ONLY way a rule-derived tag can be made to go away, because rule // hits are not stored anywhere to delete // unsuppress clear the rejection (the rule, if any, tags the video again) // // The editor's per-video toggle uses add/suppress; the bulk list actions use // add/remove/suppress; umtool uses add. Every pin AND every suppression records // provenance (`operator`, `agent: