# S02: Related Techniques Cross-Linking

**Goal:** Bottom of every technique page shows 3-4 related techniques from same creator or same sub-topic, computed dynamically from the database without requiring pre-populated join table entries.
**Demo:** After this: Bottom of every technique page shows 3-4 related techniques from same creator or same sub-topic

## Tasks
- [x] **T01: Added scored dynamic related-techniques query returning up to 4 ranked results with creator_name, topic_category, and reason fields** — 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)
  - 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 -x -v 2>&1 | tail -20
- [x] **T02: Replaced plain list rendering of related techniques with a responsive card grid showing title, creator name, category badge, and reason text** — Replace the plain list rendering of related techniques with a responsive card grid. Update the TypeScript interface to match the enriched backend schema.

## Steps

1. **Update `RelatedLinkItem` in `frontend/src/api/public-client.ts`**: Add `creator_name: string`, `topic_category: string`, `reason: string` fields (all optional with defaults for backward compat with empty strings).

2. **Replace list with card grid in `frontend/src/pages/TechniquePage.tsx`**: Remove the `<ul>` list and replace with a CSS grid of cards. Each card shows:
   - Technique title as a clickable `<Link>` to `/techniques/{target_slug}`
   - Creator name (if non-empty) in muted text
   - Topic category as a small badge (reuse existing `.badge` CSS patterns)
   - Reason text in small muted text below
   Use semantic class names: `.related-card`, `.related-card__title`, `.related-card__creator`, `.related-card__badge`, `.related-card__reason`

3. **Update CSS in `frontend/src/App.css`**: Replace `.technique-related__list` styles with:
   - `.technique-related__grid`: CSS grid, 2 columns on desktop (min-width: 600px), 1 column on mobile
   - `.related-card`: background var(--color-surface-card), border-radius, padding, border using existing design tokens
   - `.related-card__title`: link color, font-weight 600
   - `.related-card__creator`: muted text, small font
   - `.related-card__badge`: reuse existing badge pattern (small, inline, category-colored)
   - `.related-card__reason`: muted, italic, smallest font size
   Keep the existing `.technique-related` and `.technique-related h2` styles.

4. **Build verification**: Run `cd frontend && npm run build` to confirm zero TypeScript errors and successful production build.

## Must-Haves

- [ ] `RelatedLinkItem` TypeScript interface includes creator_name, topic_category, reason
- [ ] Related section renders as card grid, not plain list
- [ ] Cards show title (linked), creator name, category badge, reason
- [ ] Responsive: 2 columns on desktop, 1 on mobile
- [ ] `npm run build` passes with zero errors

## Verification

- `cd frontend && npm run build 2>&1 | tail -5` — build succeeds with zero errors
- `grep -c 'related-card' frontend/src/App.css` returns >= 4 (card styles exist)
- `grep -q 'creator_name' frontend/src/api/public-client.ts` confirms type updated
  - Estimate: 30m
  - Files: frontend/src/api/public-client.ts, frontend/src/pages/TechniquePage.tsx, frontend/src/App.css
  - Verify: cd frontend && npm run build 2>&1 | tail -5
