/** * tm_search — the governed search FRONT of the two-channel web design. * * One call, one query, clean results: the engine URL is built here, fetched * through the SAME pipeline as tm_webfetch (allowlist, manual redirects, * 20 s timeout, 2 MB cap) and collapsed into a numbered title+URL(+snippet) * hit list before governance — the agent never sees raw SERP chrome. The * structured engines (npm registry, GitHub repo search, StackExchange * question search, HN Algolia, MediaWiki) render with stars/score/tags/ * snippets. * * T4 quality upgrade (live benchmark 2026-09-14, CN host): of the HTML * SERPs only bing still returns real results — sogou/so/baidu serve * anti-bot shells and bing-int (ensearch=1) is 100% dead — so they are * REMOVED from the engine table (webfetch's domain whitelist keeps their * hosts untouched; users can still fetch those URLs directly). In their * place two no-key JSON engines: stackoverflow (api.stackexchange.com, * 300 req/day/IP — quota_remaining is tracked and auto-routing degrades to * bing once exhausted) and hn (hn.algolia.com). * * `engine: "auto"` (the new default, TM_SEARCH_DEFAULT_ENGINE) classifies * the query, fans out to the matching engine set IN PARALLEL, normalizes * every engine's hits to SearchHit, dedupes by host+path and re-ranks * with weighted RRF (stackoverflow 0.4 / bing 0.4 / others 0.2) into a * top-10 list tagged with its source engine(s). Explicit single-engine * calls keep the classic per-engine render. * * tm_webfetch stays the "fetch a KNOWN URL" tool; the host's native browser * tools cover JS-rendered pages. All share one domain allowlist and one * pipelines instance. */ import { type FetchImpl, type SearchHit } from "./webfetch.js"; import type { WebCache } from "./cache.js"; import { type TmConfig } from "./config.js"; import type { TmPipelines } from "./pipelines.js"; export interface SearchEngine { name: string; /** html engines are extracted via extractSearchHits; json engines have a * dedicated parser/renderer below. */ kind: "html" | "npm-json" | "github-json" | "wiki-json" | "so-json" | "hn-json"; buildUrl: (encodedQuery: string) => string; /** Hint appended when the engine returns nothing usable. */ alt: string; /** Transform the RAW query before URL-encoding (CJK phrase protection, * GitHub qualifier folding). Identity when absent. */ prepareQuery?: (query: string) => string; } /** * CJK phrase protection: a multi-word Chinese query gets its CORE phrase * (the token holding the most CJK chars, ≥2) wrapped in double quotes — * CN SERPs otherwise free-match tokens across pages and drown the result * list in tangential hits. Single-word queries and already-quoted queries * pass through untouched (never fabricate a phrase boundary, never fight * the user's own quoting). Applied to bing via prepareQuery; JSON query * APIs are skipped because they treat quotes literally. */ export declare function protectCjkPhrase(query: string): string; export declare function foldGithubQualifiers(query: string): { q: string; qualifiers: string[]; }; export type QueryClass = "error-code" | "dev-ecosystem" | "cjk" | "general"; export declare function classifyQuery(query: string): QueryClass; export declare const AUTO_ENGINE_KEY = "auto"; /** RRF fusion constants. bing LOST its 0.4 trust weight (2026-09-18): with * 0.4-vs-0.2 weights and a k=60 damping range of only 61..70, bing's WORST * hit (0.4/70 = 0.0057) outranked every other engine's BEST (0.2/61 = * 0.0033) — a weight ratio above the whole rank-damping range means the * under-weighted engine can never place a single hit. bing is still in the * table (it is the only live CN HTML SERP); it now has to EARN rank by * overlapping the query. Override per engine via TM_SEARCH_WEIGHTS. */ export declare const RRF_K = 60; export declare const RRF_WEIGHTS: Record; export declare const RRF_DEFAULT_WEIGHT = 0.2; /** One engine's fusion weight: a TM_SEARCH_WEIGHTS override first, then the * built-in table, then the default. */ export declare function weightFor(engine: string, overrides?: Record): number; /** * Tokenize a query for the relevance floor. CJK has no word boundaries and * the plugin ships no segmenter, so CJK runs index as every BIGRAM: "关键 * 词" contributes 关键/键词/词… which a Chinese title contains verbatim. * Substring matching against that is a cheap, deterministic overlap proxy — * no model, no network, no fabricated relevance. */ export declare function queryTerms(query: string): string[]; /** 0..1 overlap between the query and one hit (title + snippet + host). */ export declare function relevanceOf(terms: readonly string[], hit: SearchHit): number; /** Mutable SO quota state (per process). api.stackexchange answers every * payload with quota_remaining/quota_max; hitting 0 flips `exhausted` * and engine:"auto" swaps the SO leg for bing until the process restarts * (the daily quota rolls over on the server, not here). */ export declare const SO_QUOTA: { max: number; remaining: number | null; exhausted: boolean; }; export declare function resetSoQuota(): void; /** Resolve the auto route set for a query (exported for tests + PTC). * `disabled` (TM_SEARCH_DISABLED_ENGINES) removes legs; if that empties a * route the caller still gets bing rather than an error — a search tool * that refuses to answer because of its own config is worse than a * single-engine answer. */ export declare function resolveAutoRoutes(query: string, disabled?: readonly string[]): { routes: string[]; queryClass: QueryClass; notes: string[]; }; /** The engine table — keys are the tm_search `engine` arg values. The * dead CN SERPs (sogou/so/baidu/bing-int) are GONE, not even manually * selectable (2026-09-14 benchmark: anti-bot shells / 100% empty). */ export declare const SEARCH_ENGINES: Record; export declare const SEARCH_ENGINE_NAMES: string[]; /** npm registry search JSON → numbered package list (name@version — desc). */ export declare function renderNpmResults(query: string, body: string): string | null; /** GitHub repo search JSON → numbered repo list (owner/repo ★N — desc). */ export declare function renderGithubResults(query: string, body: string): string | null; /** MediaWiki search API JSON (moegirl) → numbered entry list with * tag-stripped snippets and direct article URLs. */ export declare function renderWikiResults(query: string, body: string): string | null; /** StackExchange search JSON → numbered question list; the per-hit snippet * is SYNTHESIZED from the engine's own metadata (score/answers/tags — * search/advanced carries no body text). The quota footer reflects the * tracked quota_remaining. */ export declare function renderSoResults(query: string, body: string): string | null; /** HN Algolia story search JSON → numbered post list; the per-hit snippet * is the engine's own points/comments/date metadata. */ export declare function renderHnResults(query: string, body: string): string | null; /** Collapse an HTML SERP into the compact hit list; [] when the engine * gave nothing extractable (anti-bot shell / markup change). */ export declare function extractHtmlResults(html: string, max?: number): SearchHit[]; /** host+path identity across engines (query/fragment/trailing-slash/www * ignored) — the fusion dedupe key. */ export declare function dedupeKey(url: string): string; interface FusionGroup { engine: string; hits: SearchHit[]; } /** Built-in zero-overlap multiplier (TmConfig.searchRelevanceFloor overrides). */ export declare const RELEVANCE_FLOOR_DEFAULT = 0.35; /** * Reciprocal Rank Fusion over the routed engines' hit lists: normalize → * host+path dedupe → score = Σ (weight × relevance × fetchability)/(k+rank) * → top-N, every hit tagged with its contributing engine(s). * * The relevance term is the fix for "auto returns a bing mirror": pure * rank-position fusion lets a trusted engine's junk outrank another * engine's best hit, so a hit that shares no token with the query now keeps * only a floor fraction of its engine's weight. Nothing is deleted — bing * still carries useful data, and an over-filtered list is unfixable from * the agent side. */ export declare function fuseRrf(groups: FusionGroup[], opts?: { max?: number; query?: string; weights?: Record; floor?: number; fetchable?: (url: string) => boolean; }): SearchHit[]; /** Exported for the §16 pin: the auto route builds its OWN header line, so the * recency anchor has to be added here as well — a date on the per-engine path * and none on the default one is the drift this project keeps getting caught * out by (the unknown-action menu, the browser verb table). */ export declare function renderFusedHits(query: string, routes: string[], hits: SearchHit[], notes: string[]): string; /** Build the tm_search ToolDefinition over the SHARED main pipelines * instance. `fetchImpl` is injectable for tests. */ export declare function buildTmSearchTool(deps: { pipelines: TmPipelines; cfg: TmConfig; args?: Record; fetchImpl?: FetchImpl; /** The SAME cache tm_webfetch uses: an `auto` fan-out whose legs overlap a * previous query (or a page the lead just fetched) costs one fetch, not * two. Keyed by URL, so the sharing is exact. */ cache?: WebCache; /** Injectable sleep for the HTTP retry ladder (a test pins the waits without * living them). Omitted = real setTimeout, same as buildTmWebfetchTool. */ sleep?: (ms: number) => Promise; }): { description: string; args: Record; execute: (rawArgs: Record, ctx: unknown) => Promise; }; export {}; //# sourceMappingURL=search.d.ts.map