# S01: Synthesis Prompt v5 — Nested Sections + Citations — UAT

**Milestone:** M014
**Written:** 2026-04-03T00:55:48.824Z

## UAT: S01 — Synthesis Prompt v5 — Nested Sections + Citations

### Preconditions
- Working directory: `/home/aux/projects/content-to-kb-automator`
- Python 3.12+ with pytest available
- All slice files committed

---

### Test 1: BodySection schema models exist and validate

**Steps:**
1. Run: `python -c "from pipeline.schemas import BodySection, BodySubSection; s = BodySubSection(heading='Sub', content='text'); b = BodySection(heading='Main', content='body', subsections=[s]); print(b.model_dump())"`

**Expected:** Outputs a dict with heading, content, and subsections list containing one subsection object. No import or validation errors.

---

### Test 2: SynthesizedPage uses v2 format

**Steps:**
1. Run: `python -c "from pipeline.schemas import SynthesizedPage; p = SynthesizedPage(title='T', slug='t', summary='s', body_sections=[], moment_indices=[], tags=[]); assert p.body_sections_format == 'v2'; assert isinstance(p.body_sections, list); print('v2 format confirmed')"`

**Expected:** Prints "v2 format confirmed". body_sections is list type, body_sections_format defaults to 'v2'.

---

### Test 3: Citation extraction parses all marker formats

**Steps:**
1. Run: `python -c "from pipeline.citation_utils import extract_citations; assert extract_citations('claim [0] and [2,3] here') == [0, 2, 3]; assert extract_citations('no markers') == []; assert extract_citations('[1][1][5]') == [1, 5]; print('extraction OK')"`

**Expected:** All assertions pass. Single markers, multi-markers, deduplication, and empty input handled correctly.

---

### Test 4: Citation validation returns coverage stats

**Steps:**
1. Run: `python -c "from pipeline.citation_utils import validate_citations; from pipeline.schemas import BodySection; secs = [BodySection(heading='H', content='claim [0] and [1] here')]; r = validate_citations(secs, 3); assert r['valid'] == True; assert r['total_citations'] == 2; assert r['uncited_moments'] == [2]; assert abs(r['coverage_pct'] - 66.67) < 0.1; print('validation OK')"`

**Expected:** valid=True (no out-of-range), 2 citations found, moment index 2 uncited, ~66.67% coverage.

---

### Test 5: Full citation test suite passes

**Steps:**
1. Run: `cd /home/aux/projects/content-to-kb-automator && python -m pytest backend/pipeline/test_citation_utils.py -v`

**Expected:** 15 tests pass, 0 failures.

---

### Test 6: Full harness v2 format test suite passes

**Steps:**
1. Run: `cd /home/aux/projects/content-to-kb-automator && python -m pytest backend/pipeline/test_harness_v2_format.py -v`

**Expected:** 13 tests pass, 0 failures.

---

### Test 7: Prompt file contains v2 structure

**Steps:**
1. Run: `python -c "t=open('prompts/stage5_synthesis.txt').read(); assert 'body_sections_format' in t; assert 'Citation rules' in t; assert '\"subsections\"' in t; assert '\"heading\"' in t; print('Prompt v5 OK')"`

**Expected:** All assertions pass — prompt contains v2 JSON structure, citation rules, and subsection definitions.

---

### Test 8: Prompt backup exists

**Steps:**
1. Run: `ls prompts/stage5_synthesis.*.bak`

**Expected:** At least one .bak file exists (timestamped backup of pre-v5 prompt).

---

### Edge Case 9: Out-of-range citation index flagged

**Steps:**
1. Run: `python -c "from pipeline.citation_utils import validate_citations; from pipeline.schemas import BodySection; secs = [BodySection(heading='H', content='bad ref [99]')]; r = validate_citations(secs, 3); assert r['valid'] == False; assert 99 in r['invalid_indices']; print('out-of-range caught')"`

**Expected:** valid=False, invalid_indices contains 99.

---

### Edge Case 10: Empty sections produce zero coverage

**Steps:**
1. Run: `python -c "from pipeline.citation_utils import validate_citations; r = validate_citations([], 5); assert r['total_citations'] == 0; assert r['coverage_pct'] == 0.0; print('empty OK')"`

**Expected:** Zero citations, 0% coverage, no crashes on empty input.
