---
phase: 23-score-normalisation-mcp-sort
plan: 02
type: tdd
wave: 1
depends_on: []
files_modified:
  - src/corpus_analyzer/mcp/server.py
  - tests/mcp/test_server.py
  - tests/ingest/test_round_trip.py
autonomous: true
requirements:
  - SORT-01

must_haves:
  truths:
    - "MCP corpus_search accepts sort_by parameter with values: relevance|construct|confidence|date|path"
    - "Omitting sort_by defaults to relevance (matches CLI default)"
    - "sort_by is forwarded to hybrid_search() unchanged (engine already has full vocabulary)"
    - "test_round_trip.py includes Python class fixture producing ClassName.method_name chunks"
    - "test_round_trip.py includes TypeScript class fixture producing ClassName.method_name chunks"
  artifacts:
    - path: "src/corpus_analyzer/mcp/server.py"
      provides: "sort_by parameter in corpus_search tool signature"
      contains: "sort_by"
    - path: "tests/mcp/test_server.py"
      provides: "SORT-01 tests asserting sort_by is forwarded to hybrid_search"
      contains: "test_corpus_search_sort_by"
    - path: "tests/ingest/test_round_trip.py"
      provides: "Python class and TypeScript class round-trip fixtures"
      contains: "ClassName"
  key_links:
    - from: "src/corpus_analyzer/mcp/server.py"
      to: "engine.hybrid_search()"
      via: "sort_by=sort_by parameter forwarding"
      pattern: "sort_by=sort_by"
---

<objective>
Add sort_by parameter to MCP corpus_search tool and extend round-trip tests with
class-level fixtures.

Purpose: MCP callers gain sort_by parity with CLI. Round-trip test coverage is
extended to exercise the ClassName.method_name chunks produced by Phases 20-21
sub-chunking through the full chunk_file() → indexer → LanceDB pipeline.

Output: Updated server.py with sort_by in tool signature; new MCP tests; extended
test_round_trip.py with Python class and TypeScript class parametrised cases.
</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/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/23-score-normalisation-mcp-sort/23-CONTEXT.md
@src/corpus_analyzer/mcp/server.py
@tests/mcp/test_server.py
@tests/ingest/test_round_trip.py
</context>

<feature>
  <name>MCP sort_by parameter (SORT-01) + round-trip class fixtures</name>
  <files>
    src/corpus_analyzer/mcp/server.py
    tests/mcp/test_server.py
    tests/ingest/test_round_trip.py
  </files>
  <behavior>
    SORT-01 — MCP sort_by:
      corpus_search(query="...", sort_by="construct")  → passes sort_by="construct" to hybrid_search()
      corpus_search(query="...", sort_by=None)         → passes sort_by="relevance" (default)
      corpus_search(query="...", sort_by="invalid")    → hybrid_search() raises ValueError

    Valid sort_by values: "relevance" | "construct" | "confidence" | "date" | "path"
    (Same vocabulary as the engine — no translation needed.)

    Round-trip class fixtures:
      Python class with two methods → produces ClassName.method_name chunks
        content = """
        class Greeter:
            def __init__(self, name: str) -> None:
                self.name = name

            def greet(self) -> str:
                return f"Hello, {self.name}"
        """
        Expected chunks include "Greeter.__init__" and "Greeter.greet"

      TypeScript class with two methods → produces ClassName.method_name chunks
        content = """
        export class Greeter {
            constructor(private name: string) {}
            greet(): string {
                return `Hello, ${this.name}`;
            }
        }
        """
        Expected chunks include "Greeter.constructor" and "Greeter.greet"
  </behavior>
  <implementation>
    server.py changes:
    1. Add sort_by parameter to corpus_search() signature:
       ```python
       sort_by: Optional[str] = None,  # noqa: UP045
       ```
       Place after the existing `name` parameter.

    2. In the docstring, add a line under 'name' description:
       "sort_by: sort order for results. One of: relevance (default) | construct |
        confidence | date | path. Omit to use relevance order."

    3. Resolve effective_sort_by before calling hybrid_search:
       ```python
       effective_sort_by = sort_by if sort_by is not None else "relevance"
       ```

    4. Add sort_by=effective_sort_by to the hybrid_search() call:
       ```python
       raw_results = engine.hybrid_search(
           query,
           source=source,
           file_type=type,
           construct_type=construct,
           limit=limit,
           min_score=effective_min_score,
           name=name,
           sort_by=effective_sort_by,
       )
       ```

    test_server.py additions:
    - test_corpus_search_sort_by_forwarded: pass sort_by="construct", assert
      engine.hybrid_search called with sort_by="construct"
    - test_corpus_search_sort_by_default_is_relevance: call without sort_by, assert
      engine.hybrid_search called with sort_by="relevance"
    - Update test_corpus_search_passes_filters_to_hybrid_search to include
      sort_by="relevance" in expected call_kwargs (since default is now explicit)

    test_round_trip.py additions:
    Add two parametrised cases to the existing @pytest.mark.parametrize decorator:
    - Python class case: fixture_type=".py", content with Greeter class,
      expected_chunk_name="Greeter.__init__", expected_start_line=2,
      expected_chunk_text_prefix="def __init__"
    - TypeScript class case: fixture_type=".ts", content with Greeter class,
      expected_chunk_name="Greeter.constructor", expected_start_line=2,
      expected_chunk_text_prefix="constructor"
  </implementation>
</feature>

<tasks>

<task type="tdd">
  <name>RED: Write failing SORT-01 and round-trip class tests</name>
  <files>
    tests/mcp/test_server.py
    tests/ingest/test_round_trip.py
  </files>
  <action>
    In tests/mcp/test_server.py, add two new tests after the existing name filter tests:

    ```python
    def test_corpus_search_sort_by_forwarded() -> None:
        """SORT-01: sort_by parameter is forwarded to hybrid_search() unchanged."""
        from corpus_analyzer.mcp.server import corpus_search

        engine = _make_engine([])
        ctx = _make_ctx(engine)

        async def _run_test() -> None:
            await corpus_search(query="q", sort_by="construct", ctx=ctx)

        asyncio.run(_run_test())

        call_kwargs = engine.hybrid_search.call_args[1]
        assert call_kwargs["sort_by"] == "construct"


    def test_corpus_search_sort_by_default_is_relevance() -> None:
        """SORT-01: omitting sort_by defaults to 'relevance'."""
        from corpus_analyzer.mcp.server import corpus_search

        engine = _make_engine([])
        ctx = _make_ctx(engine)

        async def _run_test() -> None:
            await corpus_search(query="q", ctx=ctx)

        asyncio.run(_run_test())

        call_kwargs = engine.hybrid_search.call_args[1]
        assert call_kwargs["sort_by"] == "relevance"
    ```

    Also update test_corpus_search_passes_filters_to_hybrid_search — its
    assert_called_once_with() currently does not include sort_by, so it will fail once
    sort_by is added to the call. Update the assertion to include sort_by="relevance".

    In tests/ingest/test_round_trip.py, extend the @pytest.mark.parametrize list to
    add two new cases after the existing ".ts" case:

    ```python
    (
        ".py",
        (
            "class Greeter:\n"
            '    """A greeter class."""\n'
            "\n"
            "    def __init__(self, name: str) -> None:\n"
            "        self.name = name\n"
            "\n"
            "    def greet(self) -> str:\n"
            '        return f"Hello, {self.name}"\n'
        ),
        "Greeter.__init__",
        4,
        "def __init__",
    ),
    (
        ".ts",
        (
            "export class Greeter {\n"
            "    constructor(private name: string) {}\n"
            "    greet(): string {\n"
            "        return `Hello, ${this.name}`;\n"
            "    }\n"
            "}\n"
        ),
        "Greeter.constructor",
        2,
        "constructor",
    ),
    ```

    Run tests — expect RED (sort_by not in hybrid_search call kwargs; class chunk names
    not yet matching expected values OR test infrastructure confirms correct state).
    Commit: `test(23-02): add failing SORT-01 and class round-trip tests`
  </action>
  <verify>
    <automated>cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run pytest tests/mcp/test_server.py::test_corpus_search_sort_by_forwarded tests/mcp/test_server.py::test_corpus_search_sort_by_default_is_relevance -x 2>&1 | tail -20</automated>
    <manual>Confirm sort_by tests fail with KeyError or AssertionError (not in call_kwargs), confirming RED state</manual>
  </verify>
  <done>SORT-01 tests fail RED; round-trip class tests are collected</done>
</task>

<task type="tdd">
  <name>GREEN: Add sort_by to MCP server and verify round-trip fixtures pass</name>
  <files>src/corpus_analyzer/mcp/server.py</files>
  <action>
    In src/corpus_analyzer/mcp/server.py:

    1. Add sort_by parameter after name in the corpus_search() signature:
       ```python
       sort_by: Optional[str] = None,  # noqa: UP045
       ```

    2. Add a description line to the tool docstring after 'name' description:
       "sort_by: result ordering. One of: relevance (default) | construct | confidence |
        date | path. Omit to use relevance (highest-score-first) ordering."

    3. Before the try/except block, resolve effective sort:
       ```python
       effective_sort_by = sort_by if sort_by is not None else "relevance"
       ```

    4. Add sort_by=effective_sort_by to the engine.hybrid_search() call (in the try block).

    Run the full MCP test suite to confirm GREEN:
    `uv run pytest tests/mcp/test_server.py -v -q`

    Then run round-trip tests to check class fixture state:
    `uv run pytest tests/ingest/test_round_trip.py -v -q`

    The round-trip class fixtures (Greeter.__init__, Greeter.constructor) test the output
    of Phases 20-21 sub-chunking. If they fail RED at this point, the sub-chunking
    implementation is incomplete — report the failure verbatim in the SUMMARY without
    modifying the tests (the tests are the specification).

    Commit: `feat(23-02): add sort_by to MCP corpus_search tool (SORT-01)`
  </action>
  <verify>
    <automated>cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run pytest tests/mcp/test_server.py tests/ingest/test_round_trip.py -v -q 2>&1 | tail -30</automated>
    <manual>If round-trip class tests fail, note the actual chunk_names returned for the Greeter class in the SUMMARY — this is evidence for the Phase 20-21 implementation state</manual>
  </verify>
  <done>
    SORT-01: all test_corpus_search_sort_by_* tests pass GREEN.
    Round-trip class fixtures: pass if sub-chunking (Phases 20-21) is complete; if they
    fail, the failure is captured in SUMMARY as a known gap (not a blocker for this plan).
    Linting clean: uv run ruff check src/corpus_analyzer/mcp/server.py passes.
  </done>
</task>

</tasks>

<verification>
Full test suite for affected modules:

```bash
cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run pytest tests/mcp/test_server.py tests/ingest/test_round_trip.py -v 2>&1 | tail -40
```

Also lint check:
```bash
cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run ruff check src/corpus_analyzer/mcp/server.py
```

Check that:
- test_corpus_search_sort_by_forwarded passes
- test_corpus_search_sort_by_default_is_relevance passes
- test_corpus_search_passes_filters_to_hybrid_search still passes (sort_by="relevance" in expected kwargs)
- Existing round-trip .md, .py function, .ts function tests still pass
</verification>

<success_criteria>
- corpus_search() tool signature includes sort_by: Optional[str] = None
- Calling corpus_search(sort_by="construct") results in hybrid_search(sort_by="construct")
- Calling corpus_search() without sort_by results in hybrid_search(sort_by="relevance")
- All tests/mcp/test_server.py tests pass
- tests/ingest/test_round_trip.py includes Greeter.__init__ and Greeter.constructor parametrised cases
- uv run ruff check src/corpus_analyzer/mcp/server.py exits 0
</success_criteria>

<output>
After completion, create `.planning/phases/23-score-normalisation-mcp-sort/23-score-normalisation-mcp-sort-02-SUMMARY.md`
</output>
