---
estimated_steps: 29
estimated_files: 3
skills_used: []
---

# T01: Add TechniquePageVersion model, migration, and stage 5 snapshot hook

Create the TechniquePageVersion SQLAlchemy model, Alembic migration, and the snapshot-on-write hook in stage 5 synthesis that captures existing page content + pipeline metadata before overwriting.

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL (migration) | Alembic raises — migration fails cleanly | N/A (local) | N/A |
| Stage 5 sync session | Snapshot INSERT fails → log ERROR, continue with page update (best-effort versioning) | N/A (local DB) | N/A |
| Prompt file hashing | FileNotFoundError → log WARNING, store empty hash in metadata | N/A | N/A |

## Steps

1. Add `TechniquePageVersion` model to `backend/models.py`: id (UUID PK), technique_page_id (FK to technique_pages.id, CASCADE), version_number (Integer, not null), content_snapshot (JSONB, not null), pipeline_metadata (JSONB, nullable), created_at (datetime). Add `sa_relationship` back to TechniquePage (versions list).

2. Create Alembic migration `alembic/versions/002_technique_page_versions.py` following the exact pattern of `001_initial.py` (revision ID format, imports, upgrade/downgrade). The migration creates the `technique_page_versions` table with all columns and a composite index on `(technique_page_id, version_number)`.

3. Add `_capture_pipeline_metadata()` helper function in `backend/pipeline/stages.py` that returns a dict with: model names (from settings), prompt file SHA-256 hashes (read each .txt file in prompts_path and hash), stage config (modalities). Use `hashlib.sha256` on file contents. Handle missing files gracefully (log warning, store empty hash).

4. In `stage5_synthesis`, inside the `if existing:` block, BEFORE any attribute assignment on `existing`, capture the snapshot: read existing page's content fields (title, slug, topic_category, topic_tags, summary, body_sections, signal_chains, plugins, source_quality) into a dict. Compute `version_number = session.execute(select(func.count()).where(TechniquePageVersion.technique_page_id == existing.id)).scalar() + 1`. Create a `TechniquePageVersion` row with content_snapshot=snapshot_dict, pipeline_metadata=_capture_pipeline_metadata(), and add it to the session. Log at INFO level.

5. Verify: run `alembic upgrade head` against test DB. Write a minimal manual test or rely on T02's integration tests.

## Must-Haves

- [ ] TechniquePageVersion model with correct columns and FK
- [ ] Alembic migration creates technique_page_versions table
- [ ] _capture_pipeline_metadata() hashes all prompt files and reads model config
- [ ] Stage 5 snapshots BEFORE mutating existing page attributes
- [ ] Version number is auto-incremented per technique_page_id
- [ ] Snapshot contains all content fields (title, summary, body_sections, signal_chains, topic_tags, plugins, source_quality, topic_category)

## Verification

- `cd backend && python -c "from models import TechniquePageVersion; print('Model OK')"` exits 0
- Alembic migration file exists and follows 001_initial.py pattern
- `grep -q '_capture_pipeline_metadata' backend/pipeline/stages.py` confirms helper exists
- `grep -q 'TechniquePageVersion' backend/pipeline/stages.py` confirms snapshot hook is wired

## Observability Impact

- Signals added: INFO log in stage 5 when version snapshot is created (includes version_number, page slug)
- How a future agent inspects this: `SELECT * FROM technique_page_versions ORDER BY created_at DESC LIMIT 5`
- Failure state exposed: ERROR log if snapshot INSERT fails (with exception details)

## Inputs

- ``backend/models.py` — existing TechniquePage model to add relationship and reference for snapshot fields`
- ``backend/pipeline/stages.py` — existing stage5_synthesis function (lines 480-618) with the `if existing:` upsert block where snapshot must be inserted BEFORE attribute mutation`
- ``alembic/versions/001_initial.py` — reference migration pattern (revision ID format, imports, upgrade/downgrade structure)`
- ``backend/config.py` — Settings class with model names, prompts_path, modality settings needed by _capture_pipeline_metadata()`

## Expected Output

- ``backend/models.py` — TechniquePageVersion model added with UUID PK, FK, version_number, content_snapshot JSONB, pipeline_metadata JSONB, created_at; relationship added to TechniquePage`
- ``alembic/versions/002_technique_page_versions.py` — new Alembic migration creating technique_page_versions table with composite index`
- ``backend/pipeline/stages.py` — _capture_pipeline_metadata() helper added; stage5_synthesis modified to snapshot existing page before overwrite`

## Verification

cd backend && python -c "from models import TechniquePageVersion; print('Model OK')" && test -f ../alembic/versions/002_technique_page_versions.py && grep -q '_capture_pipeline_metadata' pipeline/stages.py && grep -q 'TechniquePageVersion' pipeline/stages.py
