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

## Summary

S01 rewrites the stage 5 synthesis prompt to produce a new output format: `body_sections` changes from a flat `dict[str, str]` to a list of section objects with H2/H3 nesting and inline citation markers. The scope is prompt + schema + test harness validation only — no DB migration, no pipeline wiring, no frontend changes.

The current prompt (`prompts/stage5_synthesis.txt`, ~400 lines) is mature and battle-tested. The v5 rewrite preserves its voice/tone/synthesis philosophy but restructures the output format section and adds citation instructions. The Pydantic schema (`SynthesizedPage`) needs a type change from `dict` to `list` plus a new nested model. The test harness needs updated word-count logic.

**Primary risk:** LLM compliance with the new nested JSON structure. Flat dict output is simpler for LLMs. Nested objects with citation markers add complexity that may cause parse failures or inconsistent structure.

## Recommendation

**Approach:** Rewrite the prompt's output format section + schema + harness in three tasks. Use a structured list-of-objects format with explicit `level` fields rather than deep nesting.

**Format design:**
```json
{
  "body_sections": [
    {
      "heading": "Why your bass disappears in mono",
      "level": 2,
      "content": "Mid-side processing separates a stereo signal [0]...",
      "subsections": [
        {
          "heading": "The mid channel check",
          "level": 3,
          "content": "Solo the mid channel and listen for [2]..."
        }
      ]
    }
  ]
}
```

**Citation format:** `[N]` where N is the zero-indexed moment from the input. Multiple citations: `[0,2]`. Only on specific factual claims (settings, values, techniques), not on general observations. This maps directly to the `[0]`, `[1]` indices already present in the input prompt format.

**Why list-of-objects, not nested dict:** A list enforces ordering (important for workflow-ordered sections). Objects with explicit `heading`/`level`/`content` fields are easier for LLMs to produce consistently than deeply nested structures. The `subsections` array is optional — sections without sub-topics just omit it.

## Implementation Landscape

### Files to Change

| File | What Changes | Notes |
|------|-------------|-------|
| `prompts/stage5_synthesis.txt` | Rewrite output format section. Add citation rules. Update JSON example. Keep all voice/tone/synthesis guidance intact. | ~400 lines, majority stays. Output format section + example are ~80 lines that get rewritten. |
| `backend/pipeline/schemas.py` | New `BodySection` model. Change `SynthesizedPage.body_sections` from `dict` to `list[BodySection]`. | Add `body_sections_format: str = "v2"` field to mark pages using new format. |
| `backend/pipeline/test_harness.py` | Update word-count logic (lines 283, 457) and per-page summary logging. | Currently iterates `.values()` on a dict — needs to walk list-of-objects instead. |

### Files That Read body_sections (NOT changed in S01 — awareness only)

| File | How it reads body_sections | Impact |
|------|---------------------------|--------|
| `backend/pipeline/stages.py:1324,1357,1372` | Stores raw JSONB to DB column | **No change needed** — JSONB accepts any structure. |
| `backend/pipeline/quality/scorer.py:552-554` | `.items()` iteration for scoring | **Breaks** — needs update in S03/S04 timeframe. |
| `backend/pipeline/quality/__main__.py:337` | `.items()` iteration | **Breaks** — same. |
| `backend/pipeline/quality/variant_generator.py:84` | Format marker check | Marker string "body_sections" still present — OK. |
| `frontend/src/pages/TechniquePage.tsx:405+` | `Object.entries(displaySections)` | **Breaks** — fixed in S05. |
| `frontend/src/api/public-client.ts:66` | Type: `Record<string, unknown>` | Needs type update in S05. |
| Stage 6 embedding (`stages.py:1507-1510`) | Only reads `summary`, `title`, `topic_tags` | **Not affected** — never touches body_sections. |

### New Pydantic Schema Design

```python
class BodySubSection(BaseModel):
    """An H3 subsection within a body section."""
    heading: str = Field(description="H3 subsection heading")
    content: str = Field(description="Prose content with inline [N] citation markers")

class BodySection(BaseModel):
    """A top-level H2 body section with optional H3 subsections."""
    heading: str = Field(description="H2 section heading")
    content: str = Field(description="Prose content with inline [N] citation markers")
    subsections: list[BodySubSection] = Field(
        default_factory=list,
        description="Optional H3 subsections for deeper topics"
    )

class SynthesizedPage(BaseModel):
    # ... existing fields ...
    body_sections: list[BodySection] = Field(...)
    body_sections_format: str = Field(default="v2", description="Format version for body_sections")
```

### Citation Rules for the Prompt

- `[N]` after a specific claim (setting, value, routing decision) where N is the zero-indexed moment from the input
- `[N,M]` when a claim draws from multiple moments
- Citations only on factual/technical claims, not on general observations or orienting sentences
- Every section should have at least one citation
- A moment can be cited multiple times across sections
- Citation is placed at the end of the sentence containing the claim, before the period: "Set the LFO to 3/16th timing [2]."

### Test Harness Adaptations

Word count: Walk `section.content` + `sum(sub.content for sub in section.subsections)` instead of `.values()`.

Section count: `len(body_sections)` gives H2 count. Can also report subsection count.

New validation checks the harness should report:
- Citation coverage: what % of moments are cited at least once
- Citation format: all `[N]` markers reference valid moment indices
- Nesting depth: H3 subsections are present (not all flat)
- Format version: output includes `body_sections_format: "v2"`

### Existing Test Fixtures

The test harness uses `export_fixture.py` to create fixture JSONs from the DB. There are small sample fixtures in `backend/pipeline/quality/fixtures/` but no full-sized synthesis fixtures checked in. The harness CLI (`python -m pipeline.test_harness run --fixture <path>`) is the primary verification tool.

**To verify S01:** Run the test harness with an existing fixture (or create one from the live DB on ub01) against the new prompt and confirm:
1. Output parses as the new `SynthesisResult` schema
2. body_sections is a list of objects with heading/content/subsections
3. Citation markers `[N]` appear in prose
4. All citation indices are valid (within moment range)
5. Word count and section count meet minimums

### Prompt Backup

A backup mechanism exists: `prompts/stage5_synthesis.20260401_101518.bak`. The promote subcommand also archives to `prompts/archive/`. Before overwriting the production prompt, the current version should be backed up.

## Natural Task Seams

1. **T01: Schema + citation validation utilities** — New `BodySection`/`BodySubSection` models in schemas.py. A small utility function to validate citation indices against moment count (reusable by harness and pipeline). ~30 min.

2. **T02: Prompt v5 rewrite** — Rewrite the output format section of stage5_synthesis.txt. Add citation rules. Update the JSON example. Preserve all voice/tone/ordering guidance. Back up the current prompt first. ~45 min.

3. **T03: Test harness update + verification run** — Update word-count/section-count logic in test_harness.py for new structure. Add citation coverage reporting. Run the harness against a fixture with the new prompt and capture output. ~30 min.

T01 and T02 are independent. T03 depends on both.

## Risks and Mitigations

| Risk | Likelihood | Mitigation |
|------|-----------|------------|
| LLM doesn't reliably produce nested subsections | Medium | Make subsections optional. Prompt with explicit "at least 2 sections must have subsections" rule. |
| LLM omits or misformats citation markers | Medium | Explicit citation examples in prompt. Harness reports citation coverage as a warning, not a hard fail. |
| Schema change breaks quality scorer mid-milestone | Low | Scorer is only used during prompt optimization runs, not in the production pipeline. Can be updated separately. |
| Very large prompts push token count over limit | Low | The v5 prompt will be similar length to v4 — format section grows but doesn't double. |
