---
id: S04
parent: M014
milestone: M014
provides:
  - Compose-or-create branching in stage5_synthesis
  - body_sections_format='v2' on all technique pages
  - TechniquePageVideo join table populated for every page+video combination
requires:
  - slice: S01
    provides: Synthesis prompt v5 with nested sections + citations format
  - slice: S02
    provides: Compose prompt + test harness validation
  - slice: S03
    provides: Data model with body_sections_format column and TechniquePageVideo table
affects:
  - S06
  - S07
key_files:
  - backend/pipeline/stages.py
  - backend/pipeline/test_compose_pipeline.py
key_decisions:
  - Compose detection queries all matching pages and warns on multiple matches, uses first
  - pg_insert with on_conflict_do_nothing for idempotent TechniquePageVideo inserts
  - Source-code assertions for branching logic tests instead of fragile full-session mocks
patterns_established:
  - Compose-or-create branching: query existing pages by creator_id + LOWER(category), compose into first match, fall through to standard synthesis otherwise
  - XML-tagged compose prompt with offset-indexed moments: existing [0]-[N-1], new [N]-[N+M-1]
observability_surfaces:
  - INFO log when compose path triggered: 'Stage 5: Composing into existing page ...'
  - WARNING log when multiple pages match creator+category
drill_down_paths:
  - .gsd/milestones/M014/slices/S04/tasks/T01-SUMMARY.md
  - .gsd/milestones/M014/slices/S04/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-04-03T01:34:24.402Z
blocker_discovered: false
---

# S04: Pipeline Compose-or-Create Logic

**Stage 5 now detects existing technique pages by creator+category and branches to a compose path that merges new video content into them, with body_sections_format='v2' and TechniquePageVideo tracking on all pages.**

## What Happened

Two tasks delivered the compose-or-create pipeline logic and its test suite.

T01 added `_build_compose_user_prompt()` and `_compose_into_existing()` helper functions to stages.py, then wired compose detection into the stage5_synthesis per-category loop. The detection query uses `creator_id + LOWER(topic_category)` for case-insensitive matching. When an existing page matches, the compose path loads its linked moments, builds an XML-tagged prompt with offset-indexed moment references, and calls the LLM with the stage5_compose.txt system prompt. If no match, the existing chunked synthesis path runs unchanged. All pages now get `body_sections_format='v2'` set unconditionally, and a `TechniquePageVideo` row is inserted via `pg_insert` with `on_conflict_do_nothing` for idempotency.

T02 created 12 unit tests across 4 test classes: prompt construction (5 tests for XML structure, offset indices, empty existing moments, page JSON serialization, moment content), branching logic (3 tests using source-code assertions + focused mocks), format/tracking (3 tests for v2 flag, pg_insert usage, INSERT values), and case sensitivity (1 test verifying func.lower on both sides of the category comparison). All 12 pass.

## Verification

All slice-level verification checks pass:
1. `PYTHONPATH=backend python -c "from pipeline.stages import _build_compose_user_prompt, _compose_into_existing; print('imports OK')"` → exit 0
2. `grep -q 'body_sections_format' backend/pipeline/stages.py` → exit 0
3. `grep -q 'TechniquePageVideo' backend/pipeline/stages.py` → exit 0
4. `grep -q 'stage5_compose' backend/pipeline/stages.py` → exit 0
5. `PYTHONPATH=backend python -m pytest backend/pipeline/test_compose_pipeline.py -v` → 12 passed in 1.50s

## Requirements Advanced

- R012 — Stage 5 now composes new video content into existing technique pages instead of overwriting, fulfilling the incremental update aspect of R012

## Requirements Validated

None.

## New Requirements Surfaced

None.

## Requirements Invalidated or Re-scoped

None.

## Deviations

Used `default=str` in `json.dumps()` for page serialization in `_build_compose_user_prompt()` to handle UUID/datetime fields — not in plan but necessary for robustness. T02 replaced integration-level branching tests with source-code structure assertions + focused unit tests due to session mock fragility.

## Known Limitations

None.

## Follow-ups

None.

## Files Created/Modified

- `backend/pipeline/stages.py` — Added _build_compose_user_prompt(), _compose_into_existing(), compose-or-create branching in stage5_synthesis, body_sections_format='v2' setting, TechniquePageVideo insertion
- `backend/pipeline/test_compose_pipeline.py` — New file: 12 unit tests covering compose prompt construction, branching logic, format tracking, and case-insensitive matching
