Archilyzer · Source

archilyzer

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

commit 96205fd3f63a65f92bae6712eb203543e8984bff
parent 4a71c45cba4d69016b8dc9a5c05d455bf23e01c1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 12 Sep 2026 11:56:23 -0400

common: lib/search holds no caller's budget as a default

Review finding F3. `truncate(text, max = MCP_POLICY.snippetChars)` and
`ctx.snippetChars ?? MCP_POLICY.snippetChars` baked ONE caller's 240 into
the shared module. The failure mode is the bad kind: a viewer adopter
that forgets to pass its policy does not break, it silently clips every
excerpt at 240 with the whole suite green.

So the width is now a required parameter — `truncate`'s `max`,
`windowedTranscript`'s `maxLines`, `RecordCtx.snippetChars` — and
`MCP_POLICY` is no longer imported anywhere under `lib/search/` except by
`policy.ts`, which defines it. tsc is the enforcement; two tests pin the
observable the default was hiding, by running the same input under
MCP_POLICY and VIEWER_POLICY and asserting the widths differ (240 vs
uncapped). VIEWER_POLICY now has consumers.

The ripple was four lines outside the tests: mcp already passed
`policy.snippetChars` at every scanner site and `opts.maxLines ??
MCP_POLICY.windowLineCap` at getWindowedTranscript, and the two post
literals (120 / 480) were never the policy's to set. **No read path
changed — the bench counters cannot move.**

common 1128 → 1129 (+1 window case; the evalTree default-vs-policy case
was rewritten in place, not added). mcp 205 unchanged.

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

Diffstat:
Mcommon/lib/search/evalTree.test.ts | 26++++++++++++++------------
Mcommon/lib/search/evalTree.ts | 10+++++-----
Mcommon/lib/search/window.test.ts | 17++++++++++++++++-
Mcommon/lib/search/window.ts | 26+++++++++++++++++---------
4 files changed, 52 insertions(+), 27 deletions(-)

diff --git a/common/lib/search/evalTree.test.ts b/common/lib/search/evalTree.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { newGroup, newLeaf } from "../searchQuery"; import type { SearchAlias } from "../searchAliases"; import { VIDEO_STATES, type VideoState } from "../availability"; +import { MCP_POLICY, VIEWER_POLICY } from "./policy"; import { buildMatcher, buildLeafMatchers, @@ -24,6 +25,7 @@ const ctx = (over: Partial<RecordCtx> = {}): RecordCtx => ({ chatCues: [], snippetsPerVideo: 4, includeSnippets: true, + snippetChars: MCP_POLICY.snippetChars, ...over, }); @@ -182,20 +184,20 @@ test("a chat leaf reads chatCues and tags the live_chat track", () => { assert.equal(r.hits[0].seconds, 30); }); +// `RecordCtx.snippetChars` is REQUIRED — no MCP default survives in lib/search, +// because a viewer adopter that omitted it used to get 240-character excerpts +// with nothing failing. tsc enforces that it is passed; this pins that passing +// the viewer's policy actually widens the excerpt. test("snippetChars comes from the policy the caller passes", () => { const long = "y".repeat(300); - const wide = evalLeaf( - newLeaf({ id: "l", query: "y" }), - { scope: "transcripts", test: () => true }, - ctx({ cues: [cue(0, long)], snippetChars: Number.POSITIVE_INFINITY }), - ); - assert.equal(wide.hits[0].text.length, 300); - const narrow = evalLeaf( - newLeaf({ id: "l", query: "y" }), - { scope: "transcripts", test: () => true }, - ctx({ cues: [cue(0, long)] }), - ); - assert.equal(narrow.hits[0].text.length, 240, "…defaulting to the MCP's 240"); + const run = (snippetChars: number) => + evalLeaf( + newLeaf({ id: "l", query: "y" }), + { scope: "transcripts", test: () => true }, + ctx({ cues: [cue(0, long)], snippetChars }), + ); + assert.equal(run(VIEWER_POLICY.snippetChars).hits[0].text.length, 300); + assert.equal(run(MCP_POLICY.snippetChars).hits[0].text.length, 240); }); // ─── the tree algebra, per record ─── diff --git a/common/lib/search/evalTree.ts b/common/lib/search/evalTree.ts @@ -33,7 +33,6 @@ import { matchAliases, type SearchAlias } from "../searchAliases"; import { VIDEO_STATES, type VideoState } from "../availability"; import type { Cue } from "../vtt"; import { clock, truncate, type Matcher } from "./window"; -import { MCP_POLICY } from "./policy"; export type { Matcher }; @@ -138,9 +137,10 @@ export type RecordCtx = { postText?: string; snippetsPerVideo: number; includeSnippets: boolean; - // Snippet truncation width — `SearchPolicy.snippetChars`. Defaults to the - // MCP's 240 so an existing caller's output is byte-identical. - snippetChars?: number; + // Snippet truncation width — `SearchPolicy.snippetChars`. REQUIRED: a default + // here would be one caller's budget imposed on every other, and the way a + // viewer adopter discovers it is by shipping quietly clipped excerpts. + snippetChars: number; }; export type LeafOutcome = { matched: boolean; count: number; hits: ScopedSnippet[] }; @@ -151,7 +151,7 @@ export function evalLeaf( ctx: RecordCtx, ): LeafOutcome { if (!isLeaf(leaf)) return { matched: false, count: 0, hits: [] }; - const max = ctx.snippetChars ?? MCP_POLICY.snippetChars; + const max = ctx.snippetChars; const hits: ScopedSnippet[] = []; const push = (s: ScopedSnippet): void => { if (ctx.includeSnippets && hits.length < ctx.snippetsPerVideo) hits.push(s); diff --git a/common/lib/search/window.test.ts b/common/lib/search/window.test.ts @@ -9,6 +9,7 @@ import { findFirstMatchInRange, } from "./window"; import type { Cue } from "../vtt"; +import { MCP_POLICY, VIEWER_POLICY } from "./policy"; const cue = (start: number, text: string): Cue => ({ start, end: start + 3, text }); @@ -20,11 +21,22 @@ test("clock renders 0 as 0:00 rather than the empty string", () => { }); test("truncate collapses whitespace and clips with the ellipsis inside the budget", () => { - assert.equal(truncate(" a b\n c "), "a b c"); + assert.equal(truncate(" a b\n c ", MCP_POLICY.snippetChars), "a b c"); assert.equal(truncate("abcdef", 4), "abc…"); assert.equal(truncate("abcd", 4), "abcd"); }); +// The regression this guards: `max` used to default to MCP_POLICY.snippetChars, +// so a viewer path that forgot to pass its width clipped at 240 with every test +// green. The default is gone (tsc now requires the argument); this pins that the +// two policies actually produce different widths, which is the observable the +// silent default hid. +test("the width is the CALLER's, and the two policies differ", () => { + const long = "z".repeat(600); + assert.equal(truncate(long, MCP_POLICY.snippetChars).length, 240); + assert.equal(truncate(long, VIEWER_POLICY.snippetChars), long); +}); + test("windowedTranscript merges overlapping windows and stamps each line", () => { const cues = [ cue(0, "one"), @@ -36,6 +48,7 @@ test("windowedTranscript merges overlapping windows and stamps each line", () => const r = windowedTranscript(cues, (t) => t.includes("needle"), { before: 15, after: 15, + maxLines: MCP_POLICY.windowLineCap, }); assert.equal(r.matchCount, 2); // Both windows overlap, so every cue appears exactly once. @@ -51,11 +64,13 @@ test("windowedTranscript honours maxLines and the timestamps/stamp options", () const bare = windowedTranscript([cue(7, "hit")], () => true, { timestamps: false, + maxLines: MCP_POLICY.windowLineCap, }); assert.deepEqual(bare.lines, ["hit"]); const linked = windowedTranscript([cue(7, "hit")], () => true, { stamp: (c, s) => `${c}|${s}`, + maxLines: MCP_POLICY.windowLineCap, }); assert.deepEqual(linked.lines, ["[0:07|7] hit"]); }); diff --git a/common/lib/search/window.ts b/common/lib/search/window.ts @@ -30,7 +30,6 @@ import { mergeSnippets, type WindowSnippet, } from "../transcriptWindow"; -import { MCP_POLICY } from "./policy"; // A zero-second cue reads as "0:00", not as the empty string formatDuration // returns for a falsy input. @@ -40,9 +39,17 @@ export function clock(seconds: number): string { } // Collapse whitespace and clip to `max` characters, ellipsis included in the -// budget. `max` comes from a SearchPolicy (`snippetChars`); the default is the -// MCP's, which is what every existing caller passed implicitly. -export function truncate(text: string, max = MCP_POLICY.snippetChars): string { +// budget. +// +// `max` is REQUIRED, and that is the point. It used to default to +// `MCP_POLICY.snippetChars`, which meant a viewer adopter that simply forgot to +// pass its policy would silently clip every excerpt at the MCP's 240 characters +// with every test still green — the failure mode where nothing is broken, the +// output is just quietly wrong. A shared module does not get to hold one +// caller's budget as its default. Pass `policy.snippetChars`, or a literal when +// the width is genuinely not the policy's to set (a post's 120-character +// stand-in title). +export function truncate(text: string, max: number): string { const t = text.trim().replace(/\s+/g, " "); return t.length > max ? t.slice(0, max - 1) + "…" : t; } @@ -65,10 +72,11 @@ export function windowedTranscript( // Markdown link "[m:ss](url)" or the compact base form "m:ss|156"), given // the line's clock + start seconds. Bare clock when omitted. stamp?: (clock: string, seconds: number) => string; - // Cap on merged excerpt lines emitted for this video. `maxCues` above stays - // the per-window bound. - maxLines?: number; - } = {}, + // Cap on merged excerpt lines emitted for this video (`policy.windowLineCap`). + // REQUIRED, for the same reason `truncate`'s `max` is. `maxCues` above + // stays the per-window bound. + maxLines: number; + }, ): { lines: string[]; matchCount: number } { const list = cues as Cue[]; const timestamps = opts.timestamps !== false; @@ -84,7 +92,7 @@ export function windowedTranscript( maxCues: opts.maxCues, }), ); - merged = mergeSnippets(merged, win, opts.maxLines ?? MCP_POLICY.windowLineCap); + merged = mergeSnippets(merged, win, opts.maxLines); } const lines = merged.map((s) => { if (!timestamps) return s.text;