/** * Phrase blocklist — non-dev-context 2-word English compounds. * * Why this module exists (R4-T2 of the Round 4 plan): * The fixture v2 negative bucket exposed 5 false positive triggers * ("performance review meeting notes", "system architecture overview * document", "database backup recovery procedure", "validation of * insurance claims", "solar system planets astronomy"). All five share * the same structural problem: a single common dev-adjacent word * ("performance", "system", "database", "validation", "system") is * simultaneously a legitimate dev tag AND a legitimate English noun. * Tag-based matching cannot distinguish "user typed dev term in dev * context" from "user typed the same word in a non-dev context" * without external semantic signal. * * T4 BM25 was prototyped as a fix (frequency-based down-weighting) and * skipped — see `docs/plans/2026-04-08-t4-bm25-skip-adr.md` for the * full rationale. The structural reason BM25 didn't help: with N=15 * solutions, common dev-adjacent words still cluster in the high-IDF * range, so even after IDF the bare-tag match wins. * * R4-T2's approach is the inverse: instead of trying to make the * matcher smarter, surface the non-dev *context* directly. A 2-word * English compound like "performance review" or "system architecture" * is a strong signal that the surrounding query is NOT a dev question. * When such a compound appears in the query, the function below masks * its constituent tokens from the prompt tag list, removing the false * evidence the matcher would otherwise rank on. Other dev tokens in * the same query are preserved, so a dev query that happens to include * one of these compounds (e.g., "performance review of caching * strategy") still surfaces the legitimate cache match. * * Curation rules (for entries in PHRASE_BLOCKLIST): * 1. **2 words minimum**, lowercase ASCII, single space separator. * Single words are too prone to false negatives — "performance" * alone is a real dev concept; "performance review" is not. * 2. **NEVER block legitimate dev compounds.** "code review", "function * call", "error message", "database query", "system design", "type * check", "unit test", "build pipeline" — all of these are first- * class dev terms and MUST stay matchable. * 3. **Prefer concrete English compounds with a known false-positive * footprint.** Each entry should trace back to either (a) one of * the 5 known fixture v2 trigger queries, or (b) a manual review * of top-50 corpus tags for English homographs. * 4. **Plurals as separate entries.** "performance review" and * "performance reviews" are both common; we list both rather than * apply automatic stemming, since stemming would risk over-blocking * ("review" → "reviews" → "reviewed" cascade). * 5. **No regex / wildcards.** Literal phrase matching keeps the * blocklist auditable and avoids ReDoS surface. * * Roll-out posture: * Start with ~15 entries (5 known fixture triggers + 10 homograph * candidates), measure on the bootstrap eval, expand only if metrics * indicate real-world false positives that aren't covered. The ADR * targeted ~50 phrases as an upper bound — exceeding that without * measured evidence is a sign that the blocklist is becoming a leaky * abstraction for a deeper matcher problem. */ /** * Lowercase ASCII 2-word phrases that signal a non-dev context. * * Audit owner: matcher maintainer. Adding/removing entries MUST be * accompanied by a fixture eval re-run and (if the move shifts metrics) * a `ROUND3_BASELINE` update in the same PR. */ export declare const PHRASE_BLOCKLIST: readonly string[]; /** * Find every blocked phrase that appears in the query as a whole-word match. * * Whole-word means the phrase is bounded by start-of-string, end-of-string, * any whitespace, or any punctuation/non-ASCII-letter character on both * sides. Substring matching alone would over-block ("performance reviewer" * must NOT match "performance review"); whitespace-only boundary checks * would under-detect natural-language punctuation ("performance review."). * * Iterates ALL occurrences of each phrase, not just the first — so a query * like "performance reviewer and performance review meeting" still detects * the second occurrence as a valid match even though the first overlaps a * longer word. * * Returns the list of matched phrases in input order; the same phrase is * never reported twice even if it appears multiple times. Empty array * when no blocked phrase is present. */ export declare function findBlockedPhrases(rawQuery: string): string[]; /** * Mask the tokens of any blocked phrase from a prompt tag list. * * Given the raw query (used for phrase detection) and the already-extracted * prompt tags, this function: * 1. Finds every blocked phrase in the raw query. * 2. Computes the union of all phrase-constituent tokens (after running * them through `extractTags` so the masking matches the same * lowercase / Korean-aware token shape the matcher already uses). * 3. Returns a new prompt tag list with the masked tokens removed. * * If no blocked phrase is found, the input array is returned unchanged * (referentially — for the hot path's allocation cost). Otherwise a new * filtered array is returned. * * Example: query "performance review meeting notes" * - Blocked phrases found: ["performance review", "meeting notes"] * - Masked tokens: {performance, review, meeting, notes} * - extractTags("performance review meeting notes") = * [performance, review, meeting, notes] * - Result: [] (every prompt tag was masked) * * Example: query "performance review of caching strategy" * - Blocked phrases found: ["performance review"] * - Masked tokens: {performance, review} * - extractTags result: [performance, review, caching, strategy] * - Filtered result: [caching, strategy] ← legitimate dev tags survive * * Korean queries: blocked phrases are ASCII-only, so a Korean query never * triggers masking. Mixed queries (Korean + English) only mask the * English-side tokens that participate in a blocked phrase. */ export declare function maskBlockedTokens(rawQuery: string, promptTags: readonly string[]): string[];