// ONE SEARCH PIPELINE — cross-platform mirror collapsing. // // Keeps one row per RECORDING rather than per upload. Pure and synchronous: // the caller supplies the already-fetched duplicate index (reader.duplicateIndex() // on the MCP side), so this module never reaches for a transport. // // Two rules make it safe to have on by default: // // 1. The kept row is the cluster's canonical member WHEN that member is // itself among the matches — otherwise it is simply the first match. A // mirror is frequently the only surviving copy of a deleted upload, and // preferring an absent canonical would delete exactly the evidence a // "what did the removed videos say" question is asking for. // 2. The collapsed copies are NAMED on the row they folded into. Nothing // vanishes; the count stops double-counting. // // And the one it must never break: timestamps are NEVER mapped between copies // here. That requires the per-pair `aligned` gate (see `ClusterMembership` in // `lib/archive/reader.ts`), and this function does not move a single second of // anything. // The mirror rows carried on a kept hit. A mirror is named, never dropped. export type Mirror = { videoId: string; channelName: string; slug: string }; // What collapsing needs of a hit. Anything with these four fields collapses — // the MCP's SearchHit today, a viewer result group tomorrow. export type CollapsibleHit = { videoId: string; slug: string; channelName: string; mirrors?: Mirror[]; }; // What collapsing needs of the duplicate index: which cluster a slug belongs // to, and whether it is that cluster's canonical member. Structurally satisfied // by `DuplicateIndex` from `lib/archive/reader.ts`, which carries far more. export type ClusterOf = { clusterId: string; isCanonical: boolean }; export type CollapseResult = { collapsed: number; clusters: number; kept: T[]; }; export function collapseDuplicates( hits: readonly T[], index: ReadonlyMap, ): CollapseResult { const repIndexOf = new Map(); // clusterId -> index in `kept` const kept: T[] = []; let collapsed = 0; for (const hit of hits) { const membership = index.get(hit.slug); if (!membership) { kept.push(hit); continue; } const at = repIndexOf.get(membership.clusterId); if (at === undefined) { repIndexOf.set(membership.clusterId, kept.length); kept.push(hit); continue; } collapsed++; const rep = kept[at]; const repIsCanonical = index.get(rep.slug)?.isCanonical === true; const fold = (into: T, gone: T): T => ({ ...into, mirrors: [ ...(into.mirrors ?? []), ...(gone.mirrors ?? []), { videoId: gone.videoId, channelName: gone.channelName, slug: gone.slug }, ], }); // Promote the canonical member to the representative if it turns up later; // otherwise fold this copy into the incumbent. kept[at] = repIsCanonical || !membership.isCanonical ? fold(rep, hit) : fold(hit, rep); } return { collapsed, clusters: repIndexOf.size, kept }; }