# Export Improvements Phase A — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add outcome_notes, next_steps, mid-session export awareness, and partial step cutoff to all four export formats. **Architecture:** Add `next_steps` column via migration. Extend `SessionExport` schema with new options. Update all four generators in `export_service.py` to render Resolution + Next Steps sections and support step slicing. Guard `exported=True` behind completion check. **Tech Stack:** Python, FastAPI, SQLAlchemy, Alembic, pytest **Spec:** [2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md](2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md) — Phase A only --- ## Task 1: Add `next_steps` column — Migration + Model **Files:** - Modify: `backend/app/models/session.py:56` (after scratchpad) - Create: `backend/alembic/versions/034_add_next_steps_to_sessions.py` **Step 1: Add column to Session model** In `backend/app/models/session.py`, add after the `scratchpad` column (line 58): ```python next_steps: Mapped[Optional[str]] = mapped_column( Text, nullable=True, server_default=sa.text("''") ) ``` **Step 2: Generate migration** Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic revision --autogenerate -m "add next_steps to sessions"` Review the generated migration — it should add a single `next_steps` TEXT column with server_default `''`. Rename the file to `034_add_next_steps_to_sessions.py` and update the revision ID comment if needed for clarity. **Step 3: Run migration** Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic upgrade head` **Step 4: Commit** ```bash git add backend/app/models/session.py backend/alembic/versions/*next_steps* git commit -m "feat: add next_steps column to sessions table" ``` --- ## Task 2: Update Schemas — SessionExport, SessionUpdate, SessionResponse, SessionComplete **Files:** - Modify: `backend/app/schemas/session.py` **Step 1: Update SessionExport** (line 82) Replace the current `SessionExport` class with: ```python class SessionExport(BaseModel): format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$") include_timestamps: bool = True include_tree_info: bool = True # Phase A include_outcome_notes: bool = True include_next_steps: bool = True max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff") ``` **Step 2: Add next_steps to SessionUpdate** (line 46) Add to `SessionUpdate`: ```python next_steps: Optional[str] = None ``` **Step 3: Add next_steps to SessionResponse** (line 56) Add after `outcome_notes`: ```python next_steps: str = "" ``` Update the validator to normalize both fields: ```python @validator('scratchpad', 'next_steps', pre=True, always=True) def normalize_text_fields(cls, v): return v or "" ``` Remove the old `normalize_scratchpad` validator. **Step 4: Add next_steps to SessionComplete** (line 88) ```python class SessionComplete(BaseModel): outcome: SessionOutcome outcome_notes: Optional[str] = None next_steps: Optional[str] = None ``` **Step 5: Run tests to verify no regressions** Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q` Expected: All existing tests pass (new fields have defaults). **Step 6: Commit** ```bash git add backend/app/schemas/session.py git commit -m "feat: add next_steps and export options to session schemas" ``` --- ## Task 3: Update Completion Endpoint to Save next_steps **Files:** - Modify: `backend/app/api/endpoints/sessions.py:236-238` **Step 1: Write failing test** In `backend/tests/test_sessions.py`, add after the existing completion tests: ```python @pytest.mark.asyncio async def test_complete_session_with_next_steps( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test completing session saves next_steps.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] response = await client.post( f"/api/v1/sessions/{session_id}/complete", json={ "outcome": "resolved", "outcome_notes": "Fixed the issue", "next_steps": "Monitor for 48 hours" }, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["next_steps"] == "Monitor for 48 hours" assert data["outcome_notes"] == "Fixed the issue" ``` **Step 2: Run test to verify it fails** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v` Expected: FAIL — `next_steps` not saved (returns `""` because endpoint doesn't set it). **Step 3: Update completion endpoint** In `backend/app/api/endpoints/sessions.py`, line 238, add after `session.outcome_notes = completion_data.outcome_notes`: ```python session.next_steps = completion_data.next_steps ``` **Step 4: Run test to verify it passes** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v` Expected: PASS **Step 5: Commit** ```bash git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py git commit -m "feat: save next_steps on session completion" ``` --- ## Task 4: Update SessionUpdate Endpoint to Save next_steps **Note:** `update_session` blocks updates to completed sessions (returns 400). This is intentional — `next_steps` is set during active sessions or at completion time, not after. No changes needed to that guard. **Files:** - Modify: `backend/app/api/endpoints/sessions.py` (the `update_session` handler) **Step 1: Write failing test** ```python @pytest.mark.asyncio async def test_update_session_next_steps( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test updating next_steps via session update.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] response = await client.put( f"/api/v1/sessions/{session_id}", json={"next_steps": "Schedule follow-up call"}, headers=auth_headers ) assert response.status_code == 200 assert response.json()["next_steps"] == "Schedule follow-up call" ``` **Step 2: Run test — verify it fails** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v` **Step 3: Update the update_session endpoint** Find the `update_session` handler in `sessions.py`. Look for where it applies `SessionUpdate` fields to the session object. Add `next_steps` to that block, following the same pattern as `scratchpad`: ```python if update_data.next_steps is not None: session.next_steps = update_data.next_steps ``` **Step 4: Run test — verify it passes** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v` **Step 5: Commit** ```bash git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py git commit -m "feat: allow next_steps update via session update endpoint" ``` --- ## Task 5: Add outcome_notes + next_steps to Export Generators **Files:** - Modify: `backend/app/services/export_service.py` - Test: `backend/tests/test_sessions.py` This is the core export change. All four generators need Resolution and Next Steps sections after the steps. **Step 1: Write failing tests** Add to `test_sessions.py`: ```python @pytest.mark.asyncio async def test_export_includes_outcome_notes_in_resolution( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test that outcome_notes appear as Resolution section in exports.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] await client.post( f"/api/v1/sessions/{session_id}/complete", json={ "outcome": "resolved", "outcome_notes": "Replaced failed DIMM in slot A2", "next_steps": "Monitor for 24 hours" }, headers=auth_headers ) # Test markdown response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown"}, headers=auth_headers ) assert response.status_code == 200 content = response.text assert "## Resolution" in content assert "Replaced failed DIMM in slot A2" in content assert "## Next Steps" in content assert "Monitor for 24 hours" in content # Test text response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "text"}, headers=auth_headers ) content = response.text assert "RESOLUTION" in content assert "Replaced failed DIMM in slot A2" in content assert "NEXT STEPS" in content assert "Monitor for 24 hours" in content # Test HTML response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "html"}, headers=auth_headers ) content = response.text assert "Resolution" in content assert "Replaced failed DIMM in slot A2" in content assert "Next Steps" in content assert "Monitor for 24 hours" in content # Test PSA response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "psa"}, headers=auth_headers ) content = response.text assert "Replaced failed DIMM in slot A2" in content assert "Monitor for 24 hours" in content @pytest.mark.asyncio async def test_export_omits_empty_resolution_and_next_steps( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test that empty outcome_notes/next_steps don't create empty sections.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] await client.post( f"/api/v1/sessions/{session_id}/complete", json={"outcome": "resolved"}, headers=auth_headers ) response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown"}, headers=auth_headers ) content = response.text assert "## Resolution" not in content assert "## Next Steps" not in content @pytest.mark.asyncio async def test_export_exclude_outcome_notes_flag( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test include_outcome_notes=False suppresses resolution section.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] await client.post( f"/api/v1/sessions/{session_id}/complete", json={ "outcome": "resolved", "outcome_notes": "Should not appear" }, headers=auth_headers ) response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown", "include_outcome_notes": False}, headers=auth_headers ) content = response.text assert "## Resolution" not in content assert "Should not appear" not in content ``` **Step 2: Run tests — verify they fail** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v` **Step 3: Update all four generators in export_service.py** Add Resolution and Next Steps sections after the Troubleshooting Steps in each generator. The generators receive `options: SessionExport` — use `options.include_outcome_notes` and `options.include_next_steps`. **Markdown generator** — add before `return "\n".join(lines)` (line 178): ```python # Resolution / Outcome Notes outcome_notes = getattr(session, 'outcome_notes', '') or '' if outcome_notes.strip() and options.include_outcome_notes: lines.append("---") lines.append("") lines.append("## Resolution") lines.append("") lines.append(outcome_notes.strip()) lines.append("") # Next Steps next_steps = getattr(session, 'next_steps', '') or '' if next_steps.strip() and options.include_next_steps: lines.append("---") lines.append("") lines.append("## Next Steps") lines.append("") lines.append(next_steps.strip()) lines.append("") ``` **Text generator** — add before `return "\n".join(lines)` (line 235): ```python # Resolution outcome_notes = getattr(session, 'outcome_notes', '') or '' if outcome_notes.strip() and options.include_outcome_notes: lines.append("") lines.append("RESOLUTION") lines.append("-" * 20) lines.append(outcome_notes.strip()) # Next Steps next_steps = getattr(session, 'next_steps', '') or '' if next_steps.strip() and options.include_next_steps: lines.append("") lines.append("NEXT STEPS") lines.append("-" * 20) lines.append(next_steps.strip()) ``` **HTML generator** — add before `html_parts.extend(['', ''])` (line 307): ```python # Resolution outcome_notes = getattr(session, 'outcome_notes', '') or '' if outcome_notes.strip() and options.include_outcome_notes: html_parts.append('

Resolution

') html_parts.append(f'
{html.escape(outcome_notes.strip())}
') # Next Steps next_steps = getattr(session, 'next_steps', '') or '' if next_steps.strip() and options.include_next_steps: html_parts.append('

Next Steps

') html_parts.append(f'
{html.escape(next_steps.strip())}
') ``` **PSA generator** — update the existing `--- RESOLUTION ---` section (lines 363-373) to use `outcome_notes` when available, and add a next steps section: Replace the resolution section with: ```python # Resolution lines.append("--- RESOLUTION ---") outcome_notes = getattr(session, 'outcome_notes', '') or '' if outcome_notes.strip() and options.include_outcome_notes: lines.append(outcome_notes.strip()) elif session.decisions: last_decision = session.decisions[-1] resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded") lines.append(resolution) else: lines.append("No resolution recorded.") if outcome_label: lines.append(f"Outcome: {outcome_label}") lines.append("") # Next Steps next_steps = getattr(session, 'next_steps', '') or '' if next_steps.strip() and options.include_next_steps: lines.append("--- NEXT STEPS ---") lines.append(next_steps.strip()) lines.append("") ``` **Step 4: Run tests — verify they pass** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v` **Step 5: Run full test suite** Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q` **Step 6: Commit** ```bash git add backend/app/services/export_service.py backend/tests/test_sessions.py git commit -m "feat: add Resolution and Next Steps sections to all export formats" ``` --- ## Task 6: Partial Export with Step Cutoff (max_step_index) **Files:** - Modify: `backend/app/services/export_service.py` - Test: `backend/tests/test_sessions.py` **Step 1: Write failing tests** ```python @pytest.mark.asyncio async def test_export_max_step_index( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test max_step_index limits exported steps.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] # Add 3 decisions decisions = [ {"node_id": "n1", "question": "Step one?", "answer": "Yes", "timestamp": "2026-02-13T10:00:00Z", "attachments": []}, {"node_id": "n2", "question": "Step two?", "answer": "No", "timestamp": "2026-02-13T10:01:00Z", "attachments": []}, {"node_id": "n3", "question": "Step three?", "answer": "Maybe", "timestamp": "2026-02-13T10:02:00Z", "attachments": []}, ] await client.put( f"/api/v1/sessions/{session_id}", json={"decisions": decisions}, headers=auth_headers ) # Export with cutoff at step 2 response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown", "max_step_index": 2}, headers=auth_headers ) content = response.text assert "Step one?" in content assert "Step two?" in content assert "Step three?" not in content @pytest.mark.asyncio async def test_export_max_step_index_exceeds_count( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test max_step_index larger than decision count returns all steps.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] decisions = [ {"node_id": "n1", "question": "Only step", "answer": "Done", "timestamp": "2026-02-13T10:00:00Z", "attachments": []}, ] await client.put( f"/api/v1/sessions/{session_id}", json={"decisions": decisions}, headers=auth_headers ) response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown", "max_step_index": 100}, headers=auth_headers ) assert response.status_code == 200 assert "Only step" in response.text @pytest.mark.asyncio async def test_export_max_step_index_zero_returns_422( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test max_step_index=0 returns validation error.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] response = await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown", "max_step_index": 0}, headers=auth_headers ) assert response.status_code == 422 ``` **Step 2: Run tests — verify they fail** (the 422 test should already pass due to `ge=1` on schema) Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v` **Step 3: Apply step slicing in all four generators** In each generator, right before the `for i, decision in enumerate(session.decisions, 1):` loop, add: ```python decisions = session.decisions if options.max_step_index is not None: decisions = decisions[:options.max_step_index] ``` Then change the loop to iterate over `decisions` instead of `session.decisions`. Apply this in: - `generate_markdown_export` (line ~153) - `generate_text_export` (line ~214) - `generate_html_export` (line ~283) - `generate_psa_export` (line ~338) **Step 4: Run tests — verify they pass** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v` **Step 5: Commit** ```bash git add backend/app/services/export_service.py backend/tests/test_sessions.py git commit -m "feat: add max_step_index for partial exports" ``` --- ## Task 7: Mid-Session Export — Don't Set exported=True **Files:** - Modify: `backend/app/api/endpoints/sessions.py:316-318` - Test: `backend/tests/test_sessions.py` **Step 1: Write failing test** ```python @pytest.mark.asyncio async def test_export_in_progress_session_does_not_mark_exported( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test that exporting an in-progress session does NOT set exported=True.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] # Export without completing await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown"}, headers=auth_headers ) # Check session - should NOT be marked exported response = await client.get( f"/api/v1/sessions/{session_id}", headers=auth_headers ) assert response.json()["exported"] is False @pytest.mark.asyncio async def test_export_completed_session_marks_exported( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test that exporting a completed session sets exported=True.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] # Complete first await client.post( f"/api/v1/sessions/{session_id}/complete", json={"outcome": "resolved"}, headers=auth_headers ) # Export await client.post( f"/api/v1/sessions/{session_id}/export", json={"format": "markdown"}, headers=auth_headers ) # Check - should be marked exported response = await client.get( f"/api/v1/sessions/{session_id}", headers=auth_headers ) assert response.json()["exported"] is True ``` **Step 2: Run tests — verify the first one fails** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v` **Step 3: Update export endpoint** In `backend/app/api/endpoints/sessions.py`, replace lines 316-318: ```python # Mark as exported session.exported = True await db.commit() ``` With: ```python # Only mark as exported if session is completed if session.completed_at: session.exported = True await db.commit() ``` **Step 4: Run tests — verify they pass** Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v` **Step 5: Run full test suite** Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q` **Step 6: Commit** ```bash git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py git commit -m "feat: only mark session exported when completed" ``` --- ## Task 8: Final Verification **Step 1: Run full test suite** Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -v` Expected: All tests pass, including all new tests. **Step 2: Verify backward compatibility** Confirm that the existing export tests (the ones from before our changes) still pass with no modifications — the new schema fields all have defaults. **Step 3: Commit any remaining changes and verify git status is clean** ```bash git status ```