# Export Improvements Phase B — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add summary block, custom step markers, detail levels (standard/full), and editable preview modal to the export system.

**Architecture:** Backend changes to all 4 export generators (markdown, text, HTML, PSA) + schema additions. Frontend changes to ExportPreviewModal (editable textarea) and SessionDetailPage (detail level dropdown, summary toggle). No migration needed.

**Tech Stack:** Python FastAPI, Pydantic v2, React 19, TypeScript, Tailwind CSS

**Prerequisites:** Phase A frontend must be merged first (branch `feat/export-phase-a`). Task 7 depends on `maxStepIndex` state and `handleCopyForTicket` from Phase A.

---

## Task 1: Add Phase B Fields to Backend Schema

**Files:**
- Modify: `backend/app/schemas/session.py:84-91`

**Step 1: Add `include_summary` and `detail_level` to `SessionExport`**

```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")
    # Phase B
    include_summary: bool = False
    detail_level: Literal["standard", "full"] = "standard"
```

Note: `Literal` is already imported at line 2.

**Step 2: Run tests to verify no regressions**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All existing export tests pass (new fields have defaults, so backward-compatible).

**Step 3: Commit**

```bash
git add backend/app/schemas/session.py
git commit -m "feat: add include_summary and detail_level to SessionExport schema"
```

---

## Task 2: Add Frontend Types for Phase B

**Files:**
- Modify: `frontend/src/types/session.ts:79-86`

**Step 1: Add `include_summary` and `detail_level` to `SessionExport` interface**

```typescript
export interface SessionExport {
  format: 'text' | 'markdown' | 'html' | 'psa'
  include_timestamps?: boolean
  include_tree_info?: boolean
  include_outcome_notes?: boolean
  include_next_steps?: boolean
  max_step_index?: number
  include_summary?: boolean
  detail_level?: 'standard' | 'full'
}
```

**Step 2: Build to verify**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
Expected: No type errors.

**Step 3: Commit**

```bash
git add frontend/src/types/session.ts
git commit -m "feat(frontend): add include_summary and detail_level to SessionExport type"
```

---

## Task 3: Custom Step Markers (B2) in All 4 Generators

**Files:**
- Modify: `backend/app/services/export_service.py`

This is a pure backend change. Detect custom steps by `node_id.startswith("custom-")` and prefix the step title with `[CUSTOM]`.

**Step 1: Update `generate_markdown_export`** (line 163)

Change:
```python
        lines.append(f"### Step {i}: {question}")
```
To:
```python
        is_custom = decision.get("node_id", "").startswith("custom-")
        prefix = "[CUSTOM] " if is_custom else ""
        lines.append(f"### Step {i}: {prefix}{question}")
        if is_custom:
            lines.append("*Custom step added by engineer*")
```

**Step 2: Update `generate_text_export`** (line 250)

Change:
```python
        lines.append(f"\n{i}. {question}")
```
To:
```python
        is_custom = decision.get("node_id", "").startswith("custom-")
        prefix = "[CUSTOM] " if is_custom else ""
        lines.append(f"\n{i}. {prefix}{question}")
```

**Step 3: Update `generate_html_export`** (line 342)

Change:
```python
        html_parts.append(f'<h3>Step {i}: {question}</h3>')
```
To:
```python
        is_custom = decision.get("node_id", "").startswith("custom-")
        custom_badge = '<span style="background: #7c3aed; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.75em; margin-right: 6px;">CUSTOM</span>' if is_custom else ''
        html_parts.append(f'<h3>{custom_badge}Step {i}: {question}</h3>')
```

**Step 4: Update `generate_psa_export`** (line 413)

Change:
```python
            line = f"{i}. {question}"
```
To:
```python
            is_custom = decision.get("node_id", "").startswith("custom-")
            prefix = "[CUSTOM] " if is_custom else ""
            line = f"{i}. {prefix}{question}"
```

**Step 5: Run tests**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass. (Existing tests don't have custom steps, so markers won't appear — no regressions.)

**Step 6: Commit**

```bash
git add backend/app/services/export_service.py
git commit -m "feat: add [CUSTOM] markers to custom steps in all 4 export generators"
```

---

## Task 4: Command Output Truncation + Detail Levels (B3)

**Files:**
- Modify: `backend/app/services/export_service.py`

**Step 1: Add format-aware `_truncate_command_output` helper** (after `_get_command_output` at line 91)

The truncation suffix must match the output format — markdown uses `*(...)*`, text/PSA use plain `(...)`, HTML uses `<em>...</em>`.

```python
def _truncate_command_output(output: str, max_lines: int = 5, fmt: str = "text") -> str:
    """Truncate command output to max_lines for standard detail level.

    Args:
        fmt: One of "markdown", "text", "html", "psa" — controls suffix formatting.
    """
    lines = output.splitlines()
    if len(lines) <= max_lines:
        return output
    truncated = "\n".join(lines[:max_lines])
    count = len(lines)
    if fmt == "markdown":
        suffix = f"*(full output omitted — {count} lines)*"
    elif fmt == "html":
        suffix = f"<em>(full output omitted — {count} lines)</em>"
    else:  # text, psa
        suffix = f"(full output omitted — {count} lines)"
    return f"{truncated}\n{suffix}"
```

**Step 2: Apply truncation in `generate_markdown_export`** (line 168)

After `if command_output := _get_command_output(decision):` add:
```python
            if options.detail_level == "standard":
                command_output = _truncate_command_output(command_output, fmt="markdown")
```

**Step 3: Apply truncation in `generate_text_export`** (line 255)

After `if command_output := _get_command_output(decision):` add:
```python
            if options.detail_level == "standard":
                command_output = _truncate_command_output(command_output, fmt="text")
```

**Step 4: Apply truncation in `generate_html_export`** (line 347)

After `if command_output := _get_command_output(decision):` add:
```python
            if options.detail_level == "standard":
                command_output = _truncate_command_output(command_output, fmt="html")
```

**Step 5: Apply truncation in `generate_psa_export`** (line 421)

After `if command_output := _get_command_output(decision):` add:
```python
            if options.detail_level == "standard":
                command_output = _truncate_command_output(command_output, fmt="psa")
```

**Step 6: Run tests**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass (default `detail_level="standard"` but existing test command outputs are short).

**Step 7: Commit**

```bash
git add backend/app/services/export_service.py
git commit -m "feat: add format-aware command output truncation for standard detail level"
```

---

## Task 5: Summary Block Generation (B1)

**Files:**
- Modify: `backend/app/services/export_service.py`

Add summary block generation to all 4 generators. The summary is inserted after metadata, before Evidence/Steps. Only rendered when `options.include_summary is True`.

**Design decision — placeholder policy:** Empty fields (no outcome_notes, no next_steps) are left blank (empty string), NOT filled with `[Edit in preview]`. This avoids placeholder text leaking into copied/exported output. The frontend preview textarea is where users add missing info manually.

**Step 1: Add `_build_summary_fields` helper** (after `_get_outcome_label` at line 113)

```python
def _build_summary_fields(session: Session) -> dict[str, str]:
    """Build auto-populated summary fields from session data.

    Empty fields are left blank — users fill them in via the editable preview.
    """
    tree_name = session.tree_snapshot.get("name", "Troubleshooting Session")
    tree_desc = session.tree_snapshot.get("description", "")
    issue = f"{tree_name}: {tree_desc}" if tree_desc else tree_name

    if session.completed_at:
        status = "Resolved" if getattr(session, "outcome", None) == "resolved" else \
                 f"Completed — {_get_outcome_label(session) or 'Unknown'}"
    else:
        step_count = len(session.decisions) if session.decisions else 0
        status = f"In Progress — paused at step {step_count}" if step_count else "In Progress"

    _raw_notes = getattr(session, 'outcome_notes', None)
    resolution = (_raw_notes if isinstance(_raw_notes, str) else '').strip()

    _raw_next = getattr(session, 'next_steps', None)
    next_steps = (_raw_next if isinstance(_raw_next, str) else '').strip()

    return {
        "issue": issue,
        "impact": "",
        "status": status,
        "resolution": resolution,
        "next_steps": next_steps,
    }
```

**Step 2: Add `_escape_markdown_table` helper** (right after `_build_summary_fields`)

Pipe characters and newlines in values break markdown table cells:

```python
def _escape_markdown_table(value: str) -> str:
    """Escape value for use in a markdown table cell."""
    return value.replace("|", "\\|").replace("\n", " ")
```

**Step 3: Add summary block to `generate_markdown_export`**

Insert after the tree_info block (after line 138, before the scratchpad section):

```python
    if options.include_summary:
        summary = _build_summary_fields(session)
        esc = _escape_markdown_table
        lines.append("## Summary")
        lines.append("")
        lines.append("| Field | Details |")
        lines.append("|-------|---------|")
        lines.append(f"| Issue | {esc(summary['issue'])} |")
        lines.append(f"| Impact | {esc(summary['impact'])} |")
        lines.append(f"| Status | {esc(summary['status'])} |")
        lines.append(f"| Resolution | {esc(summary['resolution'])} |")
        lines.append(f"| Next Steps | {esc(summary['next_steps'])} |")
        lines.append("")
        lines.append("---")
        lines.append("")
```

**Step 4: Add summary block to `generate_text_export`**

Insert after tree_info block (after line 227, before scratchpad section):

```python
    if options.include_summary:
        summary = _build_summary_fields(session)
        lines.append("SUMMARY")
        lines.append("-" * 20)
        lines.append(f"Issue: {summary['issue']}")
        lines.append(f"Impact: {summary['impact']}")
        lines.append(f"Status: {summary['status']}")
        lines.append(f"Resolution: {summary['resolution']}")
        lines.append(f"Next Steps: {summary['next_steps']}")
        lines.append("")
```

**Step 5: Add summary block to `generate_html_export`**

Insert after tree_info `</div>` (after line 321, before scratchpad section):

```python
    if options.include_summary:
        summary = _build_summary_fields(session)
        html_parts.append('<h2>Summary</h2>')
        html_parts.append('<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">')
        for label, value in [("Issue", summary["issue"]), ("Impact", summary["impact"]),
                             ("Status", summary["status"]), ("Resolution", summary["resolution"]),
                             ("Next Steps", summary["next_steps"])]:
            html_parts.append(f'<tr><td style="padding: 6px 12px; border: 1px solid #ddd; font-weight: bold; width: 120px;">{html.escape(label)}</td>')
            html_parts.append(f'<td style="padding: 6px 12px; border: 1px solid #ddd;">{html.escape(value)}</td></tr>')
        html_parts.append('</table>')
```

**Step 6: Add summary block to `generate_psa_export`**

Insert after `lines.append("")` on line 394 (after the header, before PROBLEM section):

```python
    if options.include_summary:
        summary = _build_summary_fields(session)
        lines.append("--- SUMMARY ---")
        lines.append(f"Issue: {summary['issue']}")
        lines.append(f"Impact: {summary['impact']}")
        lines.append(f"Status: {summary['status']}")
        lines.append(f"Resolution: {summary['resolution']}")
        lines.append(f"Next Steps: {summary['next_steps']}")
        lines.append("")
```

**Step 7: Run tests**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v`
Expected: All pass (summary is opt-in, existing tests don't set `include_summary=True`).

**Step 8: Commit**

```bash
git add backend/app/services/export_service.py
git commit -m "feat: add summary block generation to all 4 export generators"
```

---

## Task 6: Editable Preview Modal (B4)

**Files:**
- Modify: `frontend/src/components/session/ExportPreviewModal.tsx`

**Design decision — edit preservation on summary toggle:** Toggling "Include Summary" re-fetches from the backend, which resets `editedContent`. This is documented and expected — the toast says "Summary updated" so the user understands. Edits are lightweight (engineers tweak a few words), so the cost of re-typing is low vs. the complexity of merging diffs.

**Step 1: Replace read-only preview with editable textarea and add controls**

Rewrite the component:

```tsx
import { useState, useEffect } from 'react'
import { Copy, Download, Check, RotateCcw } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'

interface ExportPreviewModalProps {
  isOpen: boolean
  onClose: () => void
  content: string
  filename: string
  format: 'markdown' | 'text' | 'html' | 'psa'
  onDownload: (content: string) => void
  includeSummary?: boolean
  onToggleSummary?: (include: boolean) => void
}

export function ExportPreviewModal({
  isOpen,
  onClose,
  content,
  filename,
  format,
  onDownload,
  includeSummary = false,
  onToggleSummary,
}: ExportPreviewModalProps) {
  const [copied, setCopied] = useState(false)
  const [editedContent, setEditedContent] = useState(content)

  // Sync editedContent when content prop changes (new fetch / summary toggle)
  useEffect(() => {
    setEditedContent(content)
  }, [content])

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(editedContent)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error('Failed to copy:', err)
    }
  }

  const handleDownload = () => {
    onDownload(editedContent)
    onClose()
  }

  const handleReset = () => {
    setEditedContent(content)
  }

  const handleClose = () => {
    setCopied(false)
    onClose()
  }

  const isModified = editedContent !== content

  return (
    <Modal isOpen={isOpen} onClose={handleClose} title="Export Preview" size="xl">
      {/* Filename, format info, and controls */}
      <div className="mb-3 flex flex-wrap items-center justify-between gap-2">
        <p className="text-sm text-white/70">
          Filename: <span className="font-mono text-white">{filename}</span>
          <span className="ml-3 rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
            {format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : format === 'psa' ? 'PSA' : 'Plain Text'}
          </span>
          {isModified && (
            <span className="ml-2 text-xs text-yellow-400">(edited)</span>
          )}
        </p>
        <div className="flex items-center gap-3">
          {onToggleSummary && (
            <label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
              <input
                type="checkbox"
                checked={includeSummary}
                onChange={(e) => onToggleSummary(e.target.checked)}
                className="h-4 w-4 rounded border-white/20 bg-black/50"
              />
              Include Summary
            </label>
          )}
          {isModified && (
            <button
              onClick={handleReset}
              className="flex items-center gap-1 text-xs text-white/40 hover:text-white"
              title="Reset to original"
            >
              <RotateCcw className="h-3 w-3" />
              Reset
            </button>
          )}
        </div>
      </div>

      {/* Editable Content */}
      <textarea
        value={editedContent}
        onChange={(e) => setEditedContent(e.target.value)}
        className={cn(
          'h-96 w-full resize-y rounded-md border border-white/10 bg-black/50 p-4',
          'font-mono text-sm text-white',
          'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
        )}
      />

      {/* Actions */}
      <div className="mt-4 flex items-center justify-end gap-2">
        <button
          onClick={handleCopy}
          className={cn(
            'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium',
            'text-white/60 hover:bg-white/10 hover:text-white',
            'focus:outline-none focus:ring-2 focus:ring-white/20'
          )}
        >
          {copied ? (
            <>
              <Check className="h-4 w-4 text-emerald-400" />
              Copied!
            </>
          ) : (
            <>
              <Copy className="h-4 w-4" />
              Copy to Clipboard
            </>
          )}
        </button>
        <button
          onClick={handleDownload}
          className={cn(
            'flex items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
            'hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white/20'
          )}
        >
          <Download className="h-4 w-4" />
          Download
        </button>
      </div>
    </Modal>
  )
}

export default ExportPreviewModal
```

Key changes:
- `<pre>` → `<textarea>` with `editedContent` local state
- Copy/Download use `editedContent` instead of `content` prop
- Reset button appears when content is modified
- `onDownload` signature changes to `(content: string) => void` to receive edited content
- New optional `includeSummary` + `onToggleSummary` props for summary checkbox
- `(edited)` indicator when content has been modified
- Summary toggle re-fetches content, resetting edits (documented behavior — avoids diff-merge complexity)

**Step 2: Build to verify**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

Fix any type errors from the `onDownload` signature change (see Task 7).

**Step 3: Commit**

```bash
git add frontend/src/components/session/ExportPreviewModal.tsx
git commit -m "feat(frontend): convert ExportPreviewModal to editable textarea with reset"
```

---

## Task 7: Wire Up Phase B Controls in SessionDetailPage

**Files:**
- Modify: `frontend/src/pages/SessionDetailPage.tsx`

**Step 1: Add state for detail level and include summary** (after line 34)

```typescript
const [detailLevel, setDetailLevel] = useState<'standard' | 'full'>('standard')
const [includeSummary, setIncludeSummary] = useState(false)
```

**Step 2: Update `fetchExportContent` to include Phase B options** (line 92-101)

```typescript
  const fetchExportContent = async () => {
    if (!session) return null
    const options: SessionExport = {
      format: exportFormat,
      include_timestamps: true,
      include_tree_info: true,
      ...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
      detail_level: detailLevel,
      include_summary: includeSummary,
    }
    return await sessionsApi.export(session.id, options)
  }
```

**Step 3: Update `handleCopyForTicket` to include Phase B options** (line 140-145)

```typescript
      const options: SessionExport = {
        format: 'psa',
        include_timestamps: true,
        include_tree_info: true,
        ...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
        detail_level: detailLevel,
        include_summary: includeSummary,
      }
```

**Step 4: Update `handleDownload` to accept content parameter** (line 159)

Change:
```typescript
  const handleDownload = () => {
    if (!exportContent || !session) return
    const blob = new Blob([exportContent], { type: 'text/plain' })
```
To:
```typescript
  const handleDownload = (content: string) => {
    if (!session) return
    const blob = new Blob([content], { type: 'text/plain' })
```

**Step 5: Add `onToggleSummary` handler** with error handling

```typescript
  const handleToggleSummary = async (include: boolean) => {
    setIncludeSummary(include)
    if (!session) return
    const options: SessionExport = {
      format: exportFormat,
      include_timestamps: true,
      include_tree_info: true,
      ...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
      detail_level: detailLevel,
      include_summary: include,
    }
    try {
      const content = await sessionsApi.export(session.id, options)
      if (content) {
        setExportContent(content)
        toast.success(include ? 'Summary added' : 'Summary removed')
      }
    } catch (err) {
      console.error('Failed to re-fetch export:', err)
      toast.error('Failed to update export')
      setIncludeSummary(!include)  // Revert checkbox on failure
    }
  }
```

**Step 6: Add detail level dropdown** to the export controls area (after step cutoff dropdown, before copy button — after line 408)

```tsx
<select
  value={detailLevel}
  onChange={(e) => setDetailLevel(e.target.value as 'standard' | 'full')}
  aria-label="Detail level"
  className={cn(
    'rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
    'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
  )}
>
  <option value="standard">Standard</option>
  <option value="full">Full Detail</option>
</select>
```

**Step 7: Update ExportPreviewModal props** (line 516-523)

```tsx
<ExportPreviewModal
  isOpen={showPreview}
  onClose={() => setShowPreview(false)}
  content={exportContent || ''}
  filename={getFilename()}
  format={exportFormat}
  onDownload={handleDownload}
  includeSummary={includeSummary}
  onToggleSummary={handleToggleSummary}
/>
```

**Step 8: Build to verify**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
Expected: No type errors.

**Step 9: Commit**

```bash
git add frontend/src/pages/SessionDetailPage.tsx
git commit -m "feat(frontend): add detail level dropdown and summary toggle to export controls"
```

---

## Task 8: Backend Tests for Phase B Features

**Files:**
- Modify: `backend/tests/test_psa_export.py`

Uses existing `_make_session` helper and `_default_options` from `test_psa_export.py`. Also imports additional generators for cross-format tests.

**Step 1: Add imports** at the top of `test_psa_export.py`

```python
from app.services.export_service import (
    generate_psa_export, generate_text_export, generate_markdown_export,
    generate_html_export, _format_duration,
)
```

**Step 2: Add `TestPhaseB` test class** at the bottom of the file

```python
class TestPhaseB:
    """Tests for Phase B export features: custom markers, detail levels, summary."""

    def test_custom_step_markers_psa(self):
        """Custom steps should have [CUSTOM] prefix in PSA export."""
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Check DNS", "answer": "OK"},
            {"node_id": "custom-abc123", "question": "Check Additional Logs", "answer": "Found error"},
        ])
        options = SessionExport(format="psa")
        result = generate_psa_export(session, options)
        assert "[CUSTOM] Check Additional Logs" in result
        assert "[CUSTOM] Check DNS" not in result

    def test_custom_step_markers_markdown(self):
        """Custom steps should have [CUSTOM] prefix and subtitle in markdown."""
        session = _make_session(decisions=[
            {"node_id": "custom-xyz", "question": "Manual Check", "answer": "Done"},
        ])
        options = SessionExport(format="markdown")
        result = generate_markdown_export(session, options)
        assert "[CUSTOM] Manual Check" in result
        assert "*Custom step added by engineer*" in result

    def test_custom_step_markers_html(self):
        """Custom steps should have purple badge in HTML export."""
        session = _make_session(decisions=[
            {"node_id": "custom-xyz", "question": "Manual Check", "answer": "Done"},
        ])
        options = SessionExport(format="html")
        result = generate_html_export(session, options)
        assert "CUSTOM</span>" in result

    def test_command_output_truncation_standard(self):
        """Standard detail level truncates long command output."""
        long_output = "\n".join(f"line {i}" for i in range(20))
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Run diagnostics", "answer": "See output",
             "command_output": long_output},
        ])
        options = SessionExport(format="text", detail_level="standard")
        result = generate_text_export(session, options)
        assert "(full output omitted — 20 lines)" in result
        assert "line 19" not in result

    def test_command_output_full_detail(self):
        """Full detail level shows all command output."""
        long_output = "\n".join(f"line {i}" for i in range(20))
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Run diagnostics", "answer": "See output",
             "command_output": long_output},
        ])
        options = SessionExport(format="text", detail_level="full")
        result = generate_text_export(session, options)
        assert "(full output omitted" not in result
        assert "line 19" in result

    def test_truncation_short_output_unchanged(self):
        """Short command output is not truncated even in standard mode."""
        short_output = "line 1\nline 2\nline 3"
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Check", "answer": "OK",
             "command_output": short_output},
        ])
        options = SessionExport(format="text", detail_level="standard")
        result = generate_text_export(session, options)
        assert "(full output omitted" not in result
        assert "line 3" in result

    def test_truncation_markdown_format(self):
        """Markdown format uses italic truncation marker."""
        long_output = "\n".join(f"line {i}" for i in range(20))
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Check", "answer": "OK",
             "command_output": long_output},
        ])
        options = SessionExport(format="markdown", detail_level="standard")
        result = generate_markdown_export(session, options)
        assert "*(full output omitted — 20 lines)*" in result

    def test_truncation_html_format(self):
        """HTML format uses <em> truncation marker."""
        long_output = "\n".join(f"line {i}" for i in range(20))
        session = _make_session(decisions=[
            {"node_id": "node-1", "question": "Check", "answer": "OK",
             "command_output": long_output},
        ])
        options = SessionExport(format="html", detail_level="standard")
        result = generate_html_export(session, options)
        assert "<em>(full output omitted — 20 lines)</em>" in result

    def test_summary_block_psa(self):
        """Summary block appears when include_summary is True."""
        session = _make_session()
        options = SessionExport(format="psa", include_summary=True)
        result = generate_psa_export(session, options)
        assert "--- SUMMARY ---" in result
        assert "Issue:" in result
        assert "Status:" in result

    def test_no_summary_by_default(self):
        """Summary block should not appear by default."""
        session = _make_session()
        options = SessionExport(format="psa")
        result = generate_psa_export(session, options)
        assert "--- SUMMARY ---" not in result

    def test_summary_block_markdown(self):
        """Summary block in markdown uses table format."""
        session = _make_session()
        options = SessionExport(format="markdown", include_summary=True)
        result = generate_markdown_export(session, options)
        assert "## Summary" in result
        assert "| Issue |" in result

    def test_summary_status_completed(self):
        """Completed resolved session shows 'Resolved' status in summary."""
        session = _make_session()
        session.outcome = "resolved"
        options = SessionExport(format="psa", include_summary=True)
        result = generate_psa_export(session, options)
        assert "Status: Resolved" in result

    def test_summary_status_in_progress(self):
        """In-progress session shows step count in summary status."""
        session = _make_session(
            decisions=[{"node_id": "n1", "question": "Step 1", "answer": "Done"}],
            completed_at=None,
        )
        session.completed_at = None
        options = SessionExport(format="psa", include_summary=True)
        result = generate_psa_export(session, options)
        assert "In Progress — paused at step 1" in result

    def test_summary_empty_fields_no_placeholders(self):
        """Empty summary fields should be blank, not show placeholders."""
        session = _make_session()
        session.outcome_notes = None
        session.next_steps = None
        options = SessionExport(format="psa", include_summary=True)
        result = generate_psa_export(session, options)
        assert "[Edit in preview]" not in result
```

**Step 3: Run all export tests**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py -v`
Expected: All tests pass including new ones.

**Step 4: Commit**

```bash
git add backend/tests/test_psa_export.py
git commit -m "test: add Phase B tests for custom markers, detail levels, and summary block"
```

---

## Task 9: Final Build Verification

**Step 1: Run full backend tests**

Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest --override-ini="addopts=" -v`
Expected: All tests pass.

**Step 2: Run frontend build**

Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
Expected: Build succeeds with no errors.

**Step 3: Verify git status is clean**

```bash
git status
git log --oneline feat/export-phase-a --not main | head -20
```

---

## Frontend Acceptance Checklist (Manual QA)

1. **Editable preview:** Open Preview, edit text, verify Copy/Download use edited content. Click Reset to restore original.
2. **Summary toggle:** Check "Include Summary" in preview — export re-fetches with summary block (edits reset, toast confirms). Uncheck removes it.
3. **Summary toggle error:** Disconnect network, toggle summary — checkbox reverts, error toast shown.
4. **Detail level:** Select "Full Detail", export a session with long command output — no truncation. Switch to "Standard" — output truncated with format-appropriate marker.
5. **Custom step markers:** Export a session with custom steps — should show `[CUSTOM]` prefix.
6. **Summary block content:** Summary should auto-populate Issue from tree name, Status from completion state, Resolution from outcome_notes. Empty fields are blank (no placeholder text).
7. **No placeholder leak:** Enable summary on a session with no outcome_notes — Resolution field should be blank, not show `[Edit in preview]`.
