commit 052cb600053633e48767d4b9762b1d2a1411e6f5
parent ac144c6c416b819802881d77fd3673d9a017728a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 21 Sep 2026 13:03:58 -0400
tags S2.6: curatedTags on the share-filter predicate
`curatedTags?: string[]` on SearchFilters (the selection) and on
FilterableRecord (what a record carries), ORed in passesFilters: a record
passes when it carries ANY of the selected tags. OR and not AND is the
operator's decision — the chips union, so "Collab" plus "In chat" is every
stream with either, not only the ones with both.
Two cases the tests pin because getting either backwards is silent:
an EMPTY selection is no filter at all (the chip row starts empty and hands
its state straight through, so [] must never read as "matches nothing"), and
a record from a site built before corpus spec 4 carries no `curatedTags` key,
which is "carries none" and not "carries every".
filterIsSelective is true for a non-empty set, which is what lets the MCP's
filter-first page planner prune by tag instead of falling back to a full scan.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
Diffstat:
2 files changed, 74 insertions(+), 0 deletions(-)
diff --git a/common/lib/search/evalTree.test.ts b/common/lib/search/evalTree.test.ts
@@ -321,3 +321,56 @@ test("needsAvailability and filterIsSelective are both false for an all-permissi
true,
);
});
+
+// ─── curated tags (tg) ───
+
+test("the curated-tag filter is an OR over the selection", () => {
+ const untagged = { uploadDate: "20250601" };
+ const collab = { uploadDate: "20250601", curatedTags: ["eva-collab"] };
+ const both = {
+ uploadDate: "20250601",
+ curatedTags: ["eva-collab", "eva-topic"],
+ };
+
+ // ANY of the selected tags is enough — the chips union, they do not intersect.
+ const one = openFilters({ curatedTags: ["eva-collab"] });
+ assert.equal(passesFilters(collab, one, undefined), true);
+ assert.equal(passesFilters(both, one, undefined), true);
+ assert.equal(passesFilters(untagged, one, undefined), false);
+
+ const two = openFilters({ curatedTags: ["eva-collab", "eva-in-chat"] });
+ assert.equal(passesFilters(collab, two, undefined), true);
+ assert.equal(
+ passesFilters({ uploadDate: "20250601", curatedTags: ["eva-in-chat"] }, two, undefined),
+ true,
+ );
+ assert.equal(passesFilters(untagged, two, undefined), false);
+
+ // A tag nothing carries excludes everything rather than erroring.
+ const none = openFilters({ curatedTags: ["nobody"] });
+ assert.equal(passesFilters(both, none, undefined), false);
+});
+
+test("an empty curated-tag selection is no filter at all", () => {
+ // The chip row starts empty and hands its state straight through, so [] must
+ // read as "unfiltered" — never as "matches nothing".
+ const empty = openFilters({ curatedTags: [] });
+ assert.equal(passesFilters({ uploadDate: "20250601" }, empty, undefined), true);
+ assert.equal(filterIsSelective(empty), false);
+});
+
+test("a record from a pre-spec-4 site carries no tags and passes no tag filter", () => {
+ // Records built before curated tags existed have no `curatedTags` key at all.
+ // That is not "matches every tag" — it is "carries none".
+ const legacy = { uploadDate: "20250601", isLivestream: false };
+ assert.equal(
+ passesFilters(legacy, openFilters({ curatedTags: ["eva-collab"] }), undefined),
+ false,
+ );
+});
+
+test("a non-empty curated-tag selection makes a filter selective", () => {
+ // filterIsSelective gates the MCP's filter-first page pruner: without this,
+ // a tag-only query would plan a full scan.
+ assert.equal(filterIsSelective(openFilters({ curatedTags: ["eva-collab"] })), true);
+});
diff --git a/common/lib/search/evalTree.ts b/common/lib/search/evalTree.ts
@@ -321,6 +321,14 @@ export type SearchFilters = {
// fdf / fdt — inclusive upload-date bounds, "YYYYMMDD".
dateFrom?: string;
dateTo?: string;
+ // tg — curated tag ids (common/lib/curatedTags.ts), ORed together: a record
+ // passes when it carries ANY of them. NOT the yt-dlp keywords in
+ // `TranscriptSummary.tags` — see the naming note there.
+ //
+ // Absent or empty means "no tag filter", which is why an empty array must
+ // never read as "matches nothing": the UI hands this straight through from a
+ // chip row that starts empty.
+ curatedTags?: string[];
};
// Typed on the three fields it actually reads rather than on TranscriptDetail,
@@ -330,6 +338,10 @@ export type FilterableRecord = {
isLivestream?: boolean;
ageRestricted?: boolean;
uploadDate: string;
+ // Curated tag ids carried by the record. OPTIONAL and omitted-when-empty on
+ // the wire, so a record from a site built before corpus spec 4 simply has
+ // none — and, correctly, passes no tag filter.
+ curatedTags?: string[];
};
export function passesFilters(
@@ -346,6 +358,14 @@ export function passesFilters(
// fdf / fdt — upload-date range (lexicographic on YYYYMMDD)
if (f.dateFrom && rec.uploadDate < f.dateFrom) return false;
if (f.dateTo && rec.uploadDate > f.dateTo) return false;
+ // tg — curated tags. OR across the selection (a video tagged either
+ // "eva-collab" OR "eva-in-chat" passes both-chips-selected), which is the
+ // operator's decision: the chips are a union, not an intersection.
+ if (f.curatedTags && f.curatedTags.length > 0) {
+ const has = rec.curatedTags;
+ if (!has || has.length === 0) return false;
+ if (!f.curatedTags.some((t) => has.includes(t))) return false;
+ }
return true;
}
@@ -364,5 +384,6 @@ export function filterIsSelective(f: SearchFilters | null | undefined): boolean
if (!VIDEO_STATES.every((s) => f.states.has(s))) return true;
if (!f.videos || !f.livestreams) return true;
if (!f.allAges || !f.restricted) return true;
+ if (f.curatedTags && f.curatedTags.length > 0) return true;
return Boolean(f.dateFrom || f.dateTo);
}