Archilyzer · Source

archilyzer

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

commit 8667b5a6fa2f9057109e0afdcffbd0ff8ffd0f83
parent 5b8456aff59dd3ee8c4c599d4cc4416ee9f3963f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 22 Sep 2026 16:13:51 -0400

mcp: a tag filter takes the posts corpus out of the search

Curated tags are assigned to VIDEO records, so every post fails a tag
filter — but both search paths were still scanning the post corpus to
drop it, a manifest probe and a shard read per posting channel spent on
a foregone conclusion. The export viewer already answers this way
(SearchSessionContext's globalScopeSlugs), and it learned the harder
version of the lesson: counting rows that a later fold discards gives a
header nothing on the page accounts for.

Skipping silently would be its own trap — "no post matched" and "posts
were never searched" read identically on the wire — so postsScanned
carries `skippedForTagFilter` and the footer of both search_transcripts
and enumerate_matches says which one happened.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mmcp/src/protocol.test.ts | 9+++++++++
Mmcp/src/search.test.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Mmcp/src/search.ts | 30+++++++++++++++++++++++++++---
Mmcp/src/server.ts | 8++++++++
4 files changed, 89 insertions(+), 3 deletions(-)

diff --git a/mcp/src/protocol.test.ts b/mcp/src/protocol.test.ts @@ -259,6 +259,7 @@ test("stdio: a site with no /tags.json says so instead of returning nothing", as }), ); assert.match(search, /No matches for "coffee"/); + assert.match(search, /posts: skipped — a tag filter was given/); assert.match(search, /publishes no \/tags\.json/); assert.match(search, /not evidence of absence/); }); @@ -309,6 +310,14 @@ test("stdio: enumerate_matches filters by tag and names the filter", async (t) = assert.match(tagged, /filters — tags: eva-collab/); // …and the complete-set line is still the honest one. assert.match(tagged, /complete set: yes/); + // The posts corpus is not part of that count and the footer says why: a post + // carries no curated tags, so a tag filter drops the corpus whole. Without + // the sentence "no post matches" and "posts were never searched" read the + // same on the wire. + assert.match( + tagged, + /posts: skipped — a tag filter was given and posts carry no curated tags \(the export UI does the same\)/, + ); }); test("stdio: prompts are served on both eras", async (t) => { diff --git a/mcp/src/search.test.ts b/mcp/src/search.test.ts @@ -1117,6 +1117,51 @@ test("posts: a posts-scope spec leaf matches post text", async () => { assert.deepEqual(r.hits.map((h) => h.videoId).sort(), ["p1", "p2"]); }); +test("posts: a tag filter takes the posts corpus out of the search", async () => { + // Curated tags live on video records, so every post fails a tag filter. + // Scanning them to drop them all costs a manifest probe and a shard read per + // posting channel — and counting them before the drop is the bug the export + // viewer had (tag-chips.spec.ts). The corpus is skipped, and the result says + // so rather than leaving a caller to read "0 posts" as "searched, unmatched". + const src = new StubSource(); + const r = await searchTranscripts(src, { + query: "zephyrpost", + filters: { ...KEEP_ALL, curatedTags: ["eva-collab"] }, + }); + assert.equal(r.total, 0); + assert.equal(r.postsScanned.skippedForTagFilter, true); + assert.equal(r.postsScanned.requested, false); + assert.equal(r.postsScanned.channels, 0, "no manifest was even probed"); +}); + +test("posts: without a tag filter the same query still searches them", async () => { + // The control: posts are silenced BY the tag filter, never by this change. + const src = new StubSource(); + const r = await searchTranscripts(src, { + query: "zephyrpost", + filters: { ...KEEP_ALL }, + }); + assert.equal(r.total, 2); + assert.equal(r.postsScanned.skippedForTagFilter, false); + assert.equal(r.postsScanned.requested, true); +}); + +test("posts: a posts-scope spec leaf under a tag filter matches nothing", async () => { + // The spec path is the other half: an explicit posts leaf carries its own + // slug set, so the default-scope exclusion above does not cover it. + const src = new StubSource(); + const root = newGroup({ + op: "AND", + children: [newLeaf({ id: "l1", query: "zephyrpost", scope: "posts" })], + }); + const channels = await src.listChannels(); + const r = await runSearchSpec(src, channels, { + tree: root, + filters: { ...KEEP_ALL, curatedTags: ["eva-collab"] }, + }); + assert.equal(r.total, 0); +}); + test("posts: findPost resolves by id and getThread returns the whole thread", async () => { const src = new StubSource(); const found = await findPost(src, "p2"); diff --git a/mcp/src/search.ts b/mcp/src/search.ts @@ -234,7 +234,16 @@ export type SearchResult = { // across 0 channel(s)" — indistinguishable from "searched everything, found // nothing". `channels` counts the channels that actually HAVE a posts index, // so 0 with `requested` true means the post corpus is empty here. - postsScanned: { requested: boolean; channels: number; pages: number }; + // `skippedForTagFilter` is the one way `requested: false` is worth saying out + // loud: a post carries no curated tags, so a tag filter drops the whole posts + // corpus rather than reporting it as searched-and-unmatched. The export UI + // makes the same call (SearchSessionContext's globalScopeSlugs). + postsScanned: { + requested: boolean; + channels: number; + pages: number; + skippedForTagFilter: boolean; + }; // What duplicate collapsing did to the count. `available: false` means this // corpus ships no duplicates.json, so no claim about mirrors can be made // either way — distinct from "checked, found none". @@ -541,8 +550,18 @@ export async function searchTranscripts( // content_types:["video"] must not be widened by a posts scope. const wantVideos = contentTypes.includes("video") && (!scopes || scopes.some((s) => s !== "posts")); - const wantPosts = + const postsAsked = contentTypes.includes("post") && (!scopes || scopes.includes("posts")); + // A tag filter takes the posts corpus out of the search entirely: curated + // tags live on VIDEO records (lib/curatedTags.ts), so every post would fail + // the filter, and scanning them to drop them all is a manifest probe and a + // shard read per posting channel spent on a foregone conclusion. Counting + // them as hits would be worse: the export viewer hit exactly that bug (see + // tag-chips.spec.ts) — rows dropped by the display fold that the header had + // already counted. + const postsSkippedForTagFilter = + postsAsked && (opts.filters?.curatedTags?.length ?? 0) > 0; + const wantPosts = postsAsked && !postsSkippedForTagFilter; // Counted apart from the video pass so "no posts index anywhere in scope" is // distinguishable from "searched the posts and found nothing". let postChannelsScanned = 0; @@ -774,6 +793,7 @@ export async function searchTranscripts( requested: wantPosts, channels: postChannelsScanned, pages: postPagesScanned, + skippedForTagFilter: postsSkippedForTagFilter, }, duplicates, truncated, @@ -1131,7 +1151,11 @@ export async function runSearchSpec( // Only run when the tree actually has a posts leaf: a video-only spec must // not pay a manifest probe per channel. Post records reuse the same evalNode // with `postText` set and no cues, so AND/OR/negate semantics are identical. - const wantsPosts = [...matchers.values()].some((m) => m.scope === "posts"); + // …and the same exclusion as the plain path: a posts leaf under a tag filter + // can only ever match nothing, because a post carries no curated tags. + const wantsPosts = + [...matchers.values()].some((m) => m.scope === "posts") && + (filters?.curatedTags?.length ?? 0) === 0; if (wantsPosts) { postsOuter: for (const ch of channels) { let pm; diff --git a/mcp/src/server.ts b/mcp/src/server.ts @@ -1366,6 +1366,14 @@ function incompletePageBanner( // `scanned 0 page(s) across 0 channel(s)`, which reads like nothing ran. function describePostsPass(result: SearchResult): string { const p = result.postsScanned; + // Said out loud, because the alternative is a caller concluding from silence + // that the posts were searched and matched nothing. + if (p.skippedForTagFilter) { + return ( + "posts: skipped — a tag filter was given and posts carry no curated " + + "tags (the export UI does the same)" + ); + } if (!p.requested) return ""; if (p.channels === 0) { return (