---
phase: 11-manual-ruff-cli-mypy
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
  - src/corpus_analyzer/utils/ui.py
  - src/corpus_analyzer/extractors/markdown.py
  - src/corpus_analyzer/extractors/__init__.py
  - src/corpus_analyzer/llm/ollama_client.py
  - src/corpus_analyzer/ingest/chunker.py
  - src/corpus_analyzer/analyzers/shape.py
autonomous: true
requirements:
  - MYPY-03
  - MYPY-04
  - MYPY-06

must_haves:
  truths:
    - "Zero mypy errors in utils/ui.py (4 errors fixed: missing return type + argument annotations)"
    - "Zero mypy errors in extractors/markdown.py (1 error fixed: bare dict annotation)"
    - "Zero mypy errors in extractors/__init__.py (1 error fixed: abstract class instantiation via explicit dict type)"
    - "Zero mypy errors in llm/ollama_client.py (1 error fixed: no-any-return in generate(); db field added)"
    - "Zero mypy errors in ingest/chunker.py (1 error fixed: current_lines var-annotated)"
    - "Zero mypy errors in analyzers/shape.py (1 error fixed: bare dict return annotation)"
    - "All 281 tests pass after all fixes"
  artifacts:
    - path: "src/corpus_analyzer/utils/ui.py"
      provides: "Mypy-clean UI utilities"
    - path: "src/corpus_analyzer/extractors/__init__.py"
      provides: "Mypy-clean extractor dispatch with explicit dict type annotation"
    - path: "src/corpus_analyzer/llm/ollama_client.py"
      provides: "Mypy-clean OllamaClient with optional db field"
  key_links:
    - from: "src/corpus_analyzer/llm/ollama_client.py"
      to: "src/corpus_analyzer/core/database.py"
      via: "TYPE_CHECKING guard for CorpusDatabase type"
      pattern: "TYPE_CHECKING"
    - from: "src/corpus_analyzer/extractors/__init__.py"
      to: "MarkdownExtractor | PythonExtractor"
      via: "explicit dict[str, type[MarkdownExtractor] | type[PythonExtractor]] annotation"
      pattern: "dict\\[str, type\\[Markdown"
---

<objective>
Fix the remaining 9 single-error mypy violations across 6 files: utils/ui.py (4), extractors/markdown.py (1), extractors/__init__.py (1), llm/ollama_client.py (1+db field), ingest/chunker.py (1), analyzers/shape.py (1).

Purpose: MYPY-03, MYPY-04, MYPY-06 — these are small independent fixes across thin files. Batching them into one plan allows parallel execution with plans 02 and 03 without file conflicts.

Critical note: `llm/ollama_client.py` must have the `db: CorpusDatabase | None = None` field added here because `llm/rewriter.py` (plan 05, wave 2) depends on it.

Output: Six mypy-clean files, all independently verified.
</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/11-manual-ruff-cli-mypy/11-RESEARCH.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Fix mypy errors in utils/ui.py, extractors/, ingest/chunker.py, analyzers/shape.py</name>
  <files>
    src/corpus_analyzer/utils/ui.py
    src/corpus_analyzer/extractors/markdown.py
    src/corpus_analyzer/extractors/__init__.py
    src/corpus_analyzer/ingest/chunker.py
    src/corpus_analyzer/analyzers/shape.py
  </files>
  <action>
Fix one file at a time. Read each file before editing.

**utils/ui.py (4 errors — lines 8 and 30):**
Two functions are missing return types and argument annotations. Add `Any` for the `table_obj` parameter (duck-typed sqlite-utils object) and `-> None` return types:
```python
from typing import Any

def print_table_schema(table_name: str, table_obj: Any) -> None:
    ...

def print_sample_data(table_name: str, table_obj: Any, limit: int = 5) -> None:
    ...
```
Add `from typing import Any` if not already imported. Verify: `uv run mypy src/ --no-error-summary 2>&1 | grep "utils/ui.py"` → empty.

**extractors/markdown.py (1 error — line 68):**
`_extract_title` has a bare `dict` parameter:
```python
# Before:
def _extract_title(self, ..., metadata: dict, ...) -> str:
# After:
def _extract_title(self, ..., metadata: dict[str, Any], ...) -> str:
```
Import `Any` from `typing` if not present. Verify: `uv run mypy src/ --no-error-summary 2>&1 | grep "extractors/markdown.py"` → empty.

**extractors/__init__.py (1 error — line 33):**
The `extractors` dict inside the dispatch function is inferred as `dict[str, type[BaseExtractor]]` (abstract), causing an instantiation error. Add an explicit type annotation using the concrete subclasses. The imports of `MarkdownExtractor` and `PythonExtractor` must stay INSIDE the function to avoid circular imports:
```python
def get_extractor_for(suffix: str, ...) -> ...:  # read actual signature
    from corpus_analyzer.extractors.markdown import MarkdownExtractor
    from corpus_analyzer.extractors.python import PythonExtractor

    extractors: dict[str, type[MarkdownExtractor] | type[PythonExtractor]] = {
        ".md": MarkdownExtractor,
        ".py": PythonExtractor,
        ".txt": MarkdownExtractor,
        ".rst": MarkdownExtractor,
    }
    extractor_class = extractors.get(suffix)
    if extractor_class:
        extractor = extractor_class()
        return extractor.extract(...)
```
Read the actual function body first — adapt the annotation to match the real dict keys and values. Verify: `uv run mypy src/ --no-error-summary 2>&1 | grep "extractors/__init__.py"` → empty.

**ingest/chunker.py (1 error — line 273):**
Variable missing annotation:
```python
# Before:
current_lines = []
# After:
current_lines: list[str] = []
```
Verify: `uv run mypy src/ --no-error-summary 2>&1 | grep "ingest/chunker.py"` → empty.

**analyzers/shape.py (1 error — line 212):**
Bare dict return type:
```python
# Before:
def _generate_recommended_schema(report: ShapeReport) -> dict:
# After:
def _generate_recommended_schema(report: ShapeReport) -> dict[str, Any]:
```
Import `Any` from `typing` if not present. Verify: `uv run mypy src/ --no-error-summary 2>&1 | grep "analyzers/shape.py"` → empty.

Run pytest after all edits.
  </action>
  <verify>
    <automated>uv run mypy src/ --no-error-summary 2>&1 | grep -E "utils/ui\.py|extractors/|ingest/chunker\.py|analyzers/shape\.py" | wc -l | grep -q "^0$" && echo "All 5 files: 0 errors" && uv run pytest -x -q 2>&1 | tail -3</automated>
  </verify>
  <done>Zero mypy errors in utils/ui.py, extractors/markdown.py, extractors/__init__.py, ingest/chunker.py, analyzers/shape.py; all 281 tests pass.</done>
</task>

<task type="auto">
  <name>Task 2: Add OllamaClient.db field and fix no-any-return in ollama_client.py</name>
  <files>src/corpus_analyzer/llm/ollama_client.py</files>
  <action>
Read the full file first. There are two things to fix:

**Fix 1 — no-any-return in `generate()` (line 41):**
`response["message"]["content"]` is subscript access on the ollama `ChatResponse` type, returning `Any`. First check if attribute access is typed:
```bash
python -c "import ollama; import inspect; print(inspect.getsource(ollama.Message))" 2>/dev/null | head -20
```
If `response.message.content` is a typed `str` attribute, use:
```python
return str(response.message.content)
```
If not typed, use cast:
```python
from typing import cast
return cast(str, response["message"]["content"])
```
Also check for similar patterns in `generate_stream` and `list_models` — fix any additional `no-any-return` errors in those methods using the same approach.

**Fix 2 — Add `db` attribute to `OllamaClient`:**
`OllamaClient` is a plain class (not Pydantic). Add `self.db: CorpusDatabase | None = None` to `__init__`. Use a `TYPE_CHECKING` guard to avoid a circular import (ollama_client.py → database.py is a new import edge; verify it does not create a cycle):
```python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from corpus_analyzer.core.database import CorpusDatabase
```
Then in `__init__`:
```python
self.db: CorpusDatabase | None = None
```
The `from __future__ import annotations` enables the string annotation at runtime without actually importing `CorpusDatabase` at module load time.

Check circular import: `ollama_client.py` imports from `ollama` and `corpus_analyzer.config`. `database.py` imports from `models.py`. Neither imports `ollama_client.py` currently. The `TYPE_CHECKING` guard is a safe precaution regardless.

After fix: `uv run mypy src/ --no-error-summary 2>&1 | grep "ollama_client.py"` → empty.

Then verify rewriter.py can reference `.db`: `uv run mypy src/ --no-error-summary 2>&1 | grep "rewriter.py.*attr-defined"` → should now be gone (this error is triggered by `adv_rewriter.client.db = db` on line 409 of rewriter.py). If it disappears here, plan 05 will have one less error to fix.
  </action>
  <verify>
    <automated>uv run mypy src/ --no-error-summary 2>&1 | grep "ollama_client.py" | wc -l | grep -q "^0$" && echo "ollama_client.py: 0 errors" && uv run pytest -x -q 2>&1 | tail -3</automated>
  </verify>
  <done>Zero mypy errors in ollama_client.py; OllamaClient has `db: CorpusDatabase | None = None`; all 281 tests pass.</done>
</task>

</tasks>

<verification>
After both tasks:
1. `uv run mypy src/ --no-error-summary 2>&1 | grep -E "utils/ui|extractors/|chunker\.py|shape\.py|ollama_client"` — empty
2. `OllamaClient.__init__` sets `self.db = None`
3. TYPE_CHECKING guard prevents circular import at runtime
4. `uv run pytest -x -q` — all 281 tests green
</verification>

<success_criteria>
- Zero mypy errors in all 6 files addressed by this plan
- OllamaClient.db field exists (unblocks plan 05)
- All 281 tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/11-manual-ruff-cli-mypy/11-04-SUMMARY.md`
</output>
