---
status: pending
priority: p2
issue_id: "003"
tags: [code-review, performance, architecture, python]
---

# Double chapter detection + Mistral client recreated per chapter

## Problem Statement

Two efficiency bugs that compound each other:

1. **Double detection:** CLI calls `get_chapters()` (for progress total) then `summarize()` (which calls `detect_chapters()` again internally). For heading-based detection this means 4 full document passes (2 passes per `_detect_from_headings()` call, called twice).

2. **Client per chapter:** `summarize_text()` constructs a new `Mistral(api_key=...)` client on every call, tearing down and rebuilding the HTTP connection pool for every chapter.

## Findings

**Double detection — `condensr/cli.py:29,34`:**
```python
chapter_titles = get_chapters(pdf_path)   # detect_chapters() call 1
...
for i, (chapter_title, summary) in enumerate(summarize(pdf_path, model=model), 1):
    # summarize() → detect_chapters() call 2
```

**Client per chapter — `condensr/summarizer.py:55`:**
```python
def summarize_text(text, chapter_title, model=..., api_key=None) -> str:
    client = Mistral(api_key=api_key)  # new client per chapter
```

## Proposed Solutions

### Fix 1: Add optional `chapters` parameter to `summarize()` (Recommended)

```python
# condensr/__init__.py
from condensr.parser import Chapter

def summarize(
    pdf_path: str,
    *,
    model: str = "mistral-small-latest",
    api_key: str | None = None,
    on_chapter: Callable[[str, str], None] | None = None,
    chapters: list[Chapter] | None = None,
) -> Generator[tuple[str, str], None, None]:
    if chapters is None:
        chapters = detect_chapters(pdf_path)
    ...
```

CLI then reuses detected chapters:

```python
# condensr/cli.py
from condensr.parser import detect_chapters

chapters = detect_chapters(pdf_path)
total = len(chapters) if chapters else 1
...
for i, (chapter_title, summary) in enumerate(summarize(pdf_path, chapters=chapters, model=model), 1):
```

### Fix 2: Create `Mistral` client once in `summarize()`, pass to `summarize_text()` (Recommended)

```python
# condensr/summarizer.py
from mistralai import Mistral

def summarize_text(
    text: str,
    chapter_title: str,
    model: str = "mistral-small-latest",
    api_key: str | None = None,
    client: Mistral | None = None,
) -> str:
    if client is None:
        client = Mistral(api_key=api_key)
    ...

# condensr/__init__.py — create client once
client = Mistral(api_key=api_key)
for chapter in chapters:
    text = extract_chapter_text(pdf_path, chapter)
    summary = summarize_text(text, chapter.title, model=model, client=client)
```

**Bonus:** Tests can pass a `MagicMock()` client directly instead of patching the constructor.

## Technical Details

- `condensr/__init__.py`: `summarize()` function
- `condensr/cli.py`: lines 29-34
- `condensr/summarizer.py`: line 55

## Acceptance Criteria

- [ ] `summarize()` accepts an optional `chapters` parameter
- [ ] CLI passes pre-detected chapters to `summarize()` — detection runs once
- [ ] `Mistral` client is constructed once per `summarize()` call
- [ ] `summarize_text()` accepts an optional `client` parameter
- [ ] All existing tests pass
- [ ] Summarizer tests can pass a mock client directly (no `@patch` needed)
