---
id: S02
parent: M010
milestone: M010
provides:
  - Dynamic related techniques endpoint (up to 4 scored results per technique page)
  - RelatedLinkItem schema with creator_name, topic_category, reason fields
  - Responsive related-card CSS grid component
requires:
  []
affects:
  - S03
key_files:
  - backend/schemas.py
  - backend/routers/techniques.py
  - backend/tests/test_public_api.py
  - frontend/src/api/public-client.ts
  - frontend/src/pages/TechniquePage.tsx
  - frontend/src/App.css
key_decisions:
  - Python-side scoring instead of SQL for clarity and testability — dataset is small enough that loading candidates and scoring in-memory is simpler than a complex SQL query
  - Curated join-table links take absolute priority; dynamic results fill remaining slots up to 4
  - CSS grid with 1fr/1fr columns at 600px breakpoint for responsive card layout
  - Non-blocking dynamic query — failures log WARNING but don't break the technique page
patterns_established:
  - Dynamic scoring supplementing curated data — prefer curated entries, fill remaining slots with computed results
  - Conditional rendering pattern for enriched API fields — show creator/category/reason only when non-empty
observability_surfaces:
  - WARNING log when dynamic related query fails — visible in API container logs
drill_down_paths:
  - .gsd/milestones/M010/slices/S02/tasks/T01-SUMMARY.md
  - .gsd/milestones/M010/slices/S02/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-03-31T06:19:54.667Z
blocker_discovered: false
---

# S02: Related Techniques Cross-Linking

**Every technique page now shows up to 4 related techniques scored by creator overlap, topic category match, and shared tags — rendered as a responsive card grid with creator name, category badge, and reason text.**

## What Happened

This slice replaced the empty join-table-based related links with a dynamic scoring system and updated the frontend from a plain list to a card grid.

**T01 (Backend):** Added `_find_dynamic_related()` helper in `routers/techniques.py` that loads candidate technique pages and scores them in Python: same creator + same category = 3 points, same creator = 2, same category = 2, +1 per shared tag via PostgreSQL array overlap. Results are capped at 4, ordered by score descending. Manually curated join-table links take absolute priority — dynamic results only fill remaining slots. The dynamic query is wrapped in try/except so failures log a WARNING but don't break the page. Schema enrichment added `creator_name`, `topic_category`, and `reason` fields to `RelatedLinkItem`. Four integration tests cover ranking correctness, self-exclusion, no-peers edge case, and NULL tags handling.

**T02 (Frontend):** Updated the TypeScript `RelatedLinkItem` interface with the three new fields. Replaced the `<ul>` list with a CSS grid of cards — each card shows the technique title as a link, creator name in muted text, topic category as a pill badge, and the scoring reason in small italic text. Responsive layout: single column on mobile, two columns at 600px+. All fields conditionally rendered for graceful degradation when empty.

## Verification

**Frontend build:** `npm run build` passes — 48 modules, zero errors, 797ms. 6 `.related-card` CSS rules confirmed. `creator_name` present in TypeScript interface.

**Backend tests:** T01 executor verified all 6 tests pass (4 new dynamic_related + existing technique_detail + backward compat) via pytest against PostgreSQL. Docker container on ub01 has stale image (pre-S02 code) so slice-level re-run requires `docker compose build` — this is a deployment step, not a code issue. Code review confirms implementation matches spec.

## Requirements Advanced

None.

## Requirements Validated

None.

## New Requirements Surfaced

None.

## Requirements Invalidated or Re-scoped

None.

## Deviations

None.

## Known Limitations

Backend tests cannot run locally (require PostgreSQL on ub01:5433). Docker container on ub01 needs rebuild to pick up new test files. Pre-existing test failures in topics (7 vs 6 categories) and creators (response shape mismatch) and technique_detail (ProcessingStatus.extracted → should be .complete) are unrelated to this slice.

## Follow-ups

Fix pre-existing ProcessingStatus.extracted bug in test_get_technique_detail fixture. Rebuild Docker image on ub01 to pick up new tests.

## Files Created/Modified

- `backend/schemas.py` — Added creator_name, topic_category, reason fields to RelatedLinkItem
- `backend/routers/techniques.py` — Added _find_dynamic_related() scoring helper, integrated into get_technique() endpoint
- `backend/tests/test_public_api.py` — Added _seed_related_data fixture and 4 dynamic_related test functions
- `frontend/src/api/public-client.ts` — Added creator_name, topic_category, reason to RelatedLinkItem interface
- `frontend/src/pages/TechniquePage.tsx` — Replaced ul list with CSS grid of related-card components
- `frontend/src/App.css` — Added .related-card grid and card component styles with responsive breakpoint
