Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/lib/search/evalTree.test.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/search/evalTree.ts | 21+++++++++++++++++++++
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); }