# S04: Article Versioning + Pipeline Tuning Metadata

**Goal:** Technique pages track version history with pipeline metadata; re-running stage 5 snapshots the previous content before overwriting; API returns version list and version detail; frontend shows version count.
**Demo:** After this: Technique pages track version history with pipeline metadata; API returns version list

## Tasks
- [x] **T01: Added TechniquePageVersion model, Alembic migration 002, pipeline metadata capture, and pre-overwrite snapshot hook in stage 5 synthesis** — 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)
  - Estimate: 45m
  - Files: backend/models.py, alembic/versions/002_technique_page_versions.py, backend/pipeline/stages.py
  - Verify: 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
- [x] **T02: Added version list/detail API endpoints, Pydantic schemas, version_count on technique detail, and 6 integration tests** — Add Pydantic schemas for version responses, API endpoints for version list and version detail, extend TechniquePageDetail with version_count, and write integration tests covering the full flow.

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL query | SQLAlchemy raises → FastAPI returns 500 | N/A (local) | N/A |
| Missing technique slug | Return 404 with clear message | N/A | N/A |
| Missing version number | Return 404 with clear message | N/A | N/A |

## Negative Tests

- **Malformed inputs**: GET /techniques/nonexistent-slug/versions → 404; GET /techniques/{slug}/versions/999 → 404
- **Boundary conditions**: Technique with zero versions → empty list response; version_count = 0 on fresh page

## Steps

1. Add Pydantic schemas to `backend/schemas.py`:
   - `TechniquePageVersionSummary`: version_number (int), created_at (datetime), pipeline_metadata (dict | None)
   - `TechniquePageVersionDetail`: version_number (int), content_snapshot (dict), pipeline_metadata (dict | None), created_at (datetime)
   - `TechniquePageVersionListResponse`: items (list[TechniquePageVersionSummary]), total (int)
   - Add `version_count: int = 0` field to `TechniquePageDetail`

2. Add endpoints to `backend/routers/techniques.py`:
   - `GET /{slug}/versions` — query TechniquePageVersion rows for the page, ordered by version_number DESC. Return TechniquePageVersionListResponse. 404 if slug not found.
   - `GET /{slug}/versions/{version_number}` — return single TechniquePageVersionDetail. 404 if slug or version not found.

3. Modify the existing `get_technique` endpoint to include `version_count` by counting TechniquePageVersion rows for the page.

4. Write integration tests in `backend/tests/test_public_api.py`:
   - Test version list returns empty for page with no versions
   - Test version list returns versions after inserting a TechniquePageVersion row directly
   - Test version detail returns correct content_snapshot
   - Test version detail 404 for nonexistent version number
   - Test technique detail includes version_count field
   - Test versions endpoint 404 for nonexistent slug

5. Run all existing tests to confirm no regressions.

## Must-Haves

- [ ] TechniquePageVersionSummary, TechniquePageVersionDetail, TechniquePageVersionListResponse schemas
- [ ] version_count field on TechniquePageDetail
- [ ] GET /{slug}/versions endpoint returns version list sorted by version_number desc
- [ ] GET /{slug}/versions/{version_number} endpoint returns full content snapshot
- [ ] 404 responses for missing slug or version number
- [ ] Integration tests pass for all version endpoints
- [ ] All existing tests still pass

## Verification

- `cd backend && python -m pytest tests/test_public_api.py -v` — all tests pass including new version tests
- `cd backend && python -m pytest tests/ -v` — full test suite passes (no regressions)
  - Estimate: 45m
  - Files: backend/schemas.py, backend/routers/techniques.py, backend/tests/test_public_api.py
  - Verify: cd backend && python -m pytest tests/test_public_api.py -v && python -m pytest tests/ -v --timeout=60
- [x] **T03: Added TypeScript version types, fetchTechniqueVersions function, and version count display in technique page meta stats** — Add TypeScript types for version responses to the API client, fetch version count from the technique detail response, and display it in the technique page meta stats line.

## Steps

1. Update `frontend/src/api/public-client.ts`:
   - Add `version_count: number` to `TechniquePageDetail` interface
   - Add `TechniquePageVersionSummary` interface: { version_number: number, created_at: string, pipeline_metadata: Record<string, unknown> | null }
   - Add `TechniquePageVersionListResponse` interface: { items: TechniquePageVersionSummary[], total: number }
   - Add `fetchTechniqueVersions(slug: string)` function that calls GET /techniques/{slug}/versions

2. Update `frontend/src/pages/TechniquePage.tsx`:
   - The technique detail response now includes `version_count`. Display it in the meta stats line after moment count: `· N version(s)` (only show if version_count > 0).

3. Run `npm run build` to verify zero TypeScript errors.

## Must-Haves

- [ ] TechniquePageDetail TypeScript interface includes version_count: number
- [ ] fetchTechniqueVersions API function exists
- [ ] Version count displayed in technique page meta stats (when > 0)
- [ ] `npm run build` exits 0 with zero TypeScript errors

## Verification

- `cd frontend && npm run build` exits 0
- `grep -q 'version_count' frontend/src/api/public-client.ts` confirms type added
- `grep -q 'version_count' frontend/src/pages/TechniquePage.tsx` confirms display wired
  - Estimate: 20m
  - Files: frontend/src/api/public-client.ts, frontend/src/pages/TechniquePage.tsx
  - Verify: cd frontend && npm run build && grep -q 'version_count' src/api/public-client.ts && grep -q 'version_count' src/pages/TechniquePage.tsx
