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

# T01: Dynamic related-techniques query and enriched schema

Replace the empty join-table-based related links with a dynamic scored query in the technique detail endpoint. Enrich the RelatedLinkItem schema with creator_name, topic_category, and reason fields.

## Steps

1. **Enrich `RelatedLinkItem` in `backend/schemas.py`**: Add three optional string fields: `creator_name: str = ""`, `topic_category: str = ""`, `reason: str = ""`. Keep existing fields unchanged for backward compatibility.

2. **Add dynamic query in `backend/routers/techniques.py`**: After loading the technique page in `get_technique()`, run a second query against `TechniquePage` to find related pages. Scoring logic:
   - Same `creator_id` AND same `topic_category`: score += 3 (reason: 'Same creator, same topic')
   - Same `creator_id`, different `topic_category`: score += 2 (reason: 'Same creator')
   - Same `topic_category`, different `creator_id`: score += 2 (reason: 'Also about {topic_category}')
   - Overlapping `topic_tags` (PostgreSQL `&&` via `TechniquePage.topic_tags.overlap(current_tags)`): score += 1 per shared tag (reason: 'Shared tags: {tags}')
   - Exclude the current page by ID
   - ORDER BY score DESC, LIMIT 4
   - Build enriched `RelatedLinkItem` objects with the new fields
   - If the join table also has entries, prefer those (they're manually curated) and fill remaining slots with dynamic results up to 4 total

3. **Keep existing join-table loading**: Don't remove the `selectinload` for outgoing/incoming links — manually curated links take priority when they exist. The dynamic query supplements them.

4. **Write integration test** in `backend/tests/test_public_api.py`: Add `test_dynamic_related_techniques` that:
   - Creates 4+ technique pages with overlapping tags/categories across 2 creators
   - Calls `GET /techniques/{slug}` for one of them
   - Asserts `related_links` is non-empty, contains expected entries, respects the 4-item limit
   - Asserts `creator_name`, `topic_category`, and `reason` fields are populated
   - Asserts same-creator same-category pages rank above cross-creator pages

5. **Verify existing test still passes**: Run the full `test_public_api.py` suite to confirm backward compatibility.

## Must-Haves

- [ ] `RelatedLinkItem` schema has `creator_name`, `topic_category`, `reason` fields
- [ ] Dynamic query uses PostgreSQL array overlap for tag matching
- [ ] Results limited to 4 items, ordered by relevance score
- [ ] Current page excluded from results
- [ ] Manually curated join-table links take priority when present
- [ ] New integration test passes
- [ ] Existing `test_get_technique_detail` still passes

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| PostgreSQL array overlap | Falls back to creator_id + topic_category matches only (skip tag scoring) | N/A (same DB) | N/A |
| Empty topic_tags on current page | Skip tag overlap scoring, use only creator/category matching | N/A | N/A |

## Negative Tests

- Technique with no peers (solo creator, unique category, no shared tags) → related_links is empty []
- Technique with NULL topic_tags → scoring still works on creator_id/topic_category alone
- Related results don't include the technique itself

## Verification

- `cd backend && python -m pytest tests/test_public_api.py -x -v` — all tests pass including new `test_dynamic_related_techniques`
- Existing `test_get_technique_detail` still passes (related_links populated from join table + dynamic query)

## Inputs

- ``backend/schemas.py` — existing RelatedLinkItem schema (line 243)`
- ``backend/routers/techniques.py` — existing get_technique endpoint (line 109-181)`
- ``backend/models.py` — TechniquePage model with topic_tags, topic_category, creator_id fields`
- ``backend/tests/test_public_api.py` — existing test patterns and _seed_full_data helper`

## Expected Output

- ``backend/schemas.py` — RelatedLinkItem enriched with creator_name, topic_category, reason fields`
- ``backend/routers/techniques.py` — dynamic scored query in get_technique() endpoint`
- ``backend/tests/test_public_api.py` — new test_dynamic_related_techniques test function`

## Verification

cd backend && python -m pytest tests/test_public_api.py -x -v 2>&1 | tail -20
