---
phase: 14-api-mcp-cli-parity
plan: "01"
type: tdd
wave: 1
depends_on: []
files_modified:
  - src/corpus_analyzer/cli.py
  - tests/cli/test_search_status.py
autonomous: true
requirements: [FILT-02, FILT-03]

must_haves:
  truths:
    - "corpus search --min-score 0.02 only shows results with RRF score >= 0.02"
    - "corpus search --help shows RRF score range (~0.009–0.033) in --min-score help text"
    - "corpus search --min-score 99 (filters everything) prints the FILT-03 hint message, not the generic no-results message"
    - "corpus search --sort-by score|date|title is accepted and forwarded to hybrid_search() via translation map"
    - "ruff check . and mypy src/ both exit 0 after CLI changes"
  artifacts:
    - path: "src/corpus_analyzer/cli.py"
      provides: "--min-score option, --sort-by option, FILT-03 hint logic"
      contains: "No results above"
    - path: "tests/cli/test_search_status.py"
      provides: "Tests for FILT-02 help text, FILT-03 hint, --sort-by forwarding"
  key_links:
    - from: "src/corpus_analyzer/cli.py search_command"
      to: "engine.hybrid_search()"
      via: "min_score=min_score kwarg"
      pattern: "hybrid_search.*min_score=min_score"
    - from: "src/corpus_analyzer/cli.py search_command"
      to: "FILT-03 hint"
      via: "if not results and min_score > 0.0:"
      pattern: "No results above"
---

<objective>
Add `--min-score` and `--sort-by` flags to the CLI `corpus search` command. `--min-score` filters results below the threshold and its help text documents the RRF score range. When filtering eliminates all results, a contextual hint tells users how to see the scores they're working with. `--sort-by` provides API-vocabulary parity (`score|date|title`) alongside the existing engine-vocabulary `--sort` flag.

Purpose: Complete FILT-02 and FILT-03 requirements — the CLI surface for the min-score engine built in Phase 13.
Output: Updated `cli.py` with two new options and FILT-03 hint logic, plus TDD tests proving all behaviors.
</objective>

<execution_context>
@/home/ollie/.claude/get-shit-done/workflows/execute-plan.md
@/home/ollie/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-engine-min-score-filter/13-01-SUMMARY.md
@src/corpus_analyzer/cli.py
@tests/cli/test_search_status.py
</context>

<tasks>

<task type="tdd">
  <name>Task 1 RED: Write failing CLI tests for FILT-02, FILT-03, and --sort-by</name>
  <files>tests/cli/test_search_status.py</files>
  <action>
Append three new test functions to `tests/cli/test_search_status.py`. Follow the existing test patterns in that file (mock `_open_engine` / `CorpusSearch`, use `CliRunner`, invoke `app`).

**Test 1 — FILT-02 help text (test_min_score_option_help_text):**
Invoke `app ["search", "--help"]` and assert the output contains all three of:
- `"--min-score"`
- `"RRF scores range approximately 0.009"`
- `"0.0 keeps all results"`

**Test 2 — FILT-03 filtered-all hint (test_min_score_filters_all_prints_hint):**
Patch `_open_engine` to return a mock engine whose `hybrid_search` returns `[]`. Invoke `app ["search", "anything", "--min-score", "0.999"]`. Assert the result output contains `"No results above 0.999"` and `"Run without --min-score to see available scores."`. Assert it does NOT contain `"No results for"` (that is the generic no-results message, not the FILT-03 hint).

**Test 3 — --sort-by forwarding (test_sort_by_option_forwards_to_engine):**
Patch `_open_engine` to return a mock engine whose `hybrid_search` returns a non-empty list of `SearchResult`-compatible objects. Invoke `app ["search", "q", "--sort-by", "score"]`. Assert `mock_engine.hybrid_search.call_args.kwargs["sort_by"] == "relevance"` (translation: `score` → `relevance`). Repeat for `--sort-by title` asserting translated value is `"path"`.

Run `uv run pytest tests/cli/test_search_status.py -x -v` — confirm all three new tests FAIL (TypeError or similar — the new options don't exist yet). Commit: `test(14-01): add failing CLI tests for FILT-02, FILT-03, --sort-by`.
  </action>
  <verify>
    <automated>uv run pytest tests/cli/test_search_status.py -x -v 2>&1 | tail -20</automated>
    <manual>Confirm the three new tests are collected and fail (RED state). Existing tests in the file should still pass.</manual>
    <sampling_rate>run after this task, before Task 2</sampling_rate>
  </verify>
  <done>Three new tests exist in test_search_status.py and fail because --min-score and --sort-by options do not yet exist in cli.py.</done>
</task>

<task type="tdd">
  <name>Task 2 GREEN: Implement --min-score, FILT-03 hint, and --sort-by in search_command</name>
  <files>src/corpus_analyzer/cli.py</files>
  <action>
Modify `search_command` in `src/corpus_analyzer/cli.py` to add the two new options and the FILT-03 hint logic.

**Step 1 — Add translation map constant near the top of the file (after imports, before or near the existing `_VALID_SORT` logic if any, or just above `search_command`):**

```python
_CLI_SORT_BY_MAP: dict[str, str] = {
    "score": "relevance",
    "date": "date",
    "title": "path",
}
_VALID_CLI_SORT_BY_VALUES = frozenset(_CLI_SORT_BY_MAP.keys())
```

**Step 2 — Add `--min-score` parameter to `search_command` signature** (after the existing `sort` parameter):

```python
min_score: Annotated[
    float,
    typer.Option(
        "--min-score",
        help=(
            "Exclude results below this RRF score threshold. "
            "RRF scores range approximately 0.009–0.033 (K=60); "
            "0.0 keeps all results (default)."
        ),
    ),
] = 0.0,
sort_by: Annotated[
    str,
    typer.Option(
        "--sort-by",
        help="Sort order using API vocabulary: score|date|title",
    ),
] = "score",
```

**Step 3 — Forward `min_score` to `hybrid_search()`.** Locate the existing `hybrid_search()` call inside `search_command`. Add `min_score=min_score` as a keyword argument.

**Step 4 — Handle `--sort-by`.** Before the `hybrid_search()` call, add validation:

```python
if sort_by not in _VALID_CLI_SORT_BY_VALUES:
    console.print(
        f"[red]Invalid --sort-by value: {sort_by!r}. "
        f"Allowed: {sorted(_VALID_CLI_SORT_BY_VALUES)}[/red]"
    )
    raise typer.Exit(code=1)
```

Then pass `sort_by=_CLI_SORT_BY_MAP[sort_by]` to `hybrid_search()`. If both `--sort` and `--sort-by` are provided, `--sort-by` takes precedence: use `_CLI_SORT_BY_MAP[sort_by]` as the `sort_by` kwarg, ignoring `sort`. If only `--sort` is provided, keep existing behavior (pass `sort_by=sort` as before).

**Step 5 — Replace the "no results" block** with the FILT-03 branched check:

```python
if not results:
    if min_score > 0.0:
        console.print(
            f"No results above {min_score:.3f}. "
            "Run without --min-score to see available scores."
        )
    else:
        console.print(f'[yellow]No results for "[bold]{query}[/bold]"[/yellow]')
    return
```

Run `uv run pytest tests/cli/test_search_status.py -x -v` — all tests including the three new ones must pass (GREEN state). Then run full suite and linting:

```
uv run pytest -x -v
uv run ruff check .
uv run mypy src/
```

All must exit 0. Commit: `feat(14-01): add --min-score, --sort-by, FILT-03 hint to CLI search`.
  </action>
  <verify>
    <automated>uv run pytest tests/cli/test_search_status.py -x -v && uv run pytest -x -q && uv run ruff check . && uv run mypy src/</automated>
    <manual>Run `uv run corpus search --help` and confirm --min-score and --sort-by appear with correct help text including "0.009–0.033".</manual>
    <sampling_rate>run after this task commits</sampling_rate>
  </verify>
  <done>All tests pass (285+ green). `corpus search --help` shows --min-score with RRF range help text. Filtering all results with a high threshold prints the FILT-03 hint. ruff and mypy both exit 0.</done>
</task>

</tasks>

<verification>
Full pass:
```
uv run pytest -v
uv run ruff check .
uv run mypy src/
```

Behavioral checks:
1. `uv run corpus search --help` output includes `"RRF scores range approximately 0.009"` and `"--min-score"` and `"--sort-by"`
2. Mocked engine returning `[]` with `--min-score 0.999` triggers `"No results above 0.999. Run without --min-score..."` output
3. `--sort-by score` translates to `sort_by="relevance"` in the `hybrid_search()` call
4. `--sort-by title` translates to `sort_by="path"` in the `hybrid_search()` call
</verification>

<success_criteria>
- FILT-02 satisfied: `--min-score` help text contains RRF score range `~0.009–0.033`
- FILT-03 satisfied: filtered-all state prints exact requirement message with 3dp threshold
- `--sort-by` accepted with `score|date|title` values, forwarded via translation map
- All existing CLI tests still green
- ruff and mypy both clean
</success_criteria>

<output>
After completion, create `.planning/phases/14-api-mcp-cli-parity/14-01-SUMMARY.md`
</output>
