diff --git a/README.md b/README.md index 7f6b74c..7fbcb5b 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Bot-authored comments (`sender.login` ending in `[bot]`) and comments shorter th | Name | Type | Description | |------|------|-------------| | `query` | string (optional) | Natural-language query. Omit or empty = scan mode. | -| `repo` | string | Filter by repository — full slug (`owner/repo`), exact match. A bare repository name matches nothing; search mode reports that as `"repo"` in the response's `filters_unmatched`. | +| `repo` | string | Filter by repository — full slug (`owner/repo`), exact match. A bare repository name matches nothing; search mode reports that as `"repo"` in the response's `filters_unmatched`. Pass it whenever the question is about a known repository — without it, a near-duplicate from another repository can rank first; leave it unset only when the question spans repositories. | | `path_prefix` | string | Filter docs by repository-relative directory prefix. Requires `type: "doc"`, a trailing `/`, and at most 64 UTF-8 bytes. | | `state` | `"open"` \| `"closed"` \| `"all"` | Filter by state (default `all`). | | `labels` | string[] | Filter by label names (AND). | diff --git a/docs/0-requirements.md b/docs/0-requirements.md index 64cd50b..9a58ecc 100644 --- a/docs/0-requirements.md +++ b/docs/0-requirements.md @@ -525,7 +525,7 @@ Returns: - `same_entity` on results that absorbed other rows of the same entity (see Entity Aggregation); `top_k` counts entities, not rows - top-level metadata: `fusion`, `dense_candidates`, `sparse_candidates`, `rerank_requested`, `rerank_applied`, `filters_unmatched` -**Unmatched filters (`filters_unmatched`).** `repo` takes the full slug (`owner/repo`) and matches exactly — on the dense side as a Vectorize metadata `$eq`, on the sparse side as `d.repo = ?`. `path_prefix` selects doc rows whose repository-relative path begins with the named directory prefix. A bare repository name or a path prefix that selects no doc row therefore produces a response shaped exactly like a genuine zero-hit search. `filters_unmatched` separates the two: it is always present in search mode, `[]` means every applied filter selected a non-empty population (so `count: 0` really is "no hits"), and a listed name means that filter's population is empty — the value is wrong, not the query. +**Unmatched filters (`filters_unmatched`).** `repo` takes the full slug (`owner/repo`) and matches exactly — on the dense side as a Vectorize metadata `$eq`, on the sparse side as `d.repo = ?`. `path_prefix` selects doc rows whose repository-relative path begins with the named directory prefix. A bare repository name or a path prefix that selects no doc row therefore produces a response shaped exactly like a genuine zero-hit search. `filters_unmatched` separates the two: it is always present in search mode, `[]` means every applied filter selected a non-empty population (so `count: 0` really is "no hits"), and a listed name means that filter's population is empty — the value is wrong, not the query. The tool description tells the caller when to pass `repo`: whenever the question is about a known repository, since without it a near-duplicate from another repository can rank first, and to leave it unset only when the question spans repositories. The default stays unfiltered so cross-repository questions keep working. The distinction is worth a field because agentic multi-step search inverts the cost of a silent zero. In a single search a zero is a dead end the caller inspects; in a search loop it is a normal intermediate result ("nothing down this angle") that the caller consumes and moves past, so the mis-specified filter never surfaces and one query out of the budget is spent on a false negative. diff --git a/mcp-server/server/search-schema.json b/mcp-server/server/search-schema.json index b0e212e..f3103cf 100644 --- a/mcp-server/server/search-schema.json +++ b/mcp-server/server/search-schema.json @@ -9,7 +9,7 @@ "maxLength": 4096 }, "repo": { - "description": "Filter by repository — full slug (owner/repo), exact match. A bare repository name (\"my-repo\") matches nothing and yields an empty result set; search mode flags that case as \"repo\" in the response's filters_unmatched.", + "description": "Filter by repository — full slug (owner/repo), exact match. A bare repository name (\"my-repo\") matches nothing and yields an empty result set; search mode flags that case as \"repo\" in the response's filters_unmatched. Pass it whenever the question is about a known repository: without it, a near-duplicate from another repository can rank first. Leave it unset only when the question spans repositories.", "type": "string", "maxLength": 256 }, diff --git a/src/search-contract.ts b/src/search-contract.ts index de6d15c..8911ec0 100644 --- a/src/search-contract.ts +++ b/src/search-contract.ts @@ -15,7 +15,9 @@ export const searchInputSchema = z.object({ .describe( "Filter by repository — full slug (owner/repo), exact match. " + "A bare repository name (\"my-repo\") matches nothing and yields an empty result set; " + - "search mode flags that case as \"repo\" in the response's filters_unmatched.", + "search mode flags that case as \"repo\" in the response's filters_unmatched. " + + "Pass it whenever the question is about a known repository: without it, a near-duplicate from another repository can rank first. " + + "Leave it unset only when the question spans repositories.", ), path_prefix: z .string().max(256)