From 5d0fd05b98bed0c119eaf2cb249c4ad7ababcdd8 Mon Sep 17 00:00:00 2001 From: jlightner Date: Tue, 31 Mar 2026 06:13:59 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20scored=20dynamic=20related-tech?= =?UTF-8?q?niques=20query=20returning=20up=20to=204=20r=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "backend/schemas.py" - "backend/routers/techniques.py" - "backend/tests/test_public_api.py" GSD-Task: S02/T01 --- .gsd/milestones/M010/M010-ROADMAP.md | 2 +- .../milestones/M010/slices/S01/S01-SUMMARY.md | 94 ++++++++ .gsd/milestones/M010/slices/S01/S01-UAT.md | 53 +++++ .../M010/slices/S01/tasks/T02-VERIFY.json | 30 +++ .gsd/milestones/M010/slices/S02/S02-PLAN.md | 99 ++++++++- .../M010/slices/S02/S02-RESEARCH.md | 79 +++++++ .../M010/slices/S02/tasks/T01-PLAN.md | 79 +++++++ .../M010/slices/S02/tasks/T01-SUMMARY.md | 79 +++++++ .../M010/slices/S02/tasks/T02-PLAN.md | 61 ++++++ backend/routers/techniques.py | 98 +++++++++ backend/schemas.py | 3 + backend/tests/test_public_api.py | 202 ++++++++++++++++++ 12 files changed, 877 insertions(+), 2 deletions(-) create mode 100644 .gsd/milestones/M010/slices/S01/S01-SUMMARY.md create mode 100644 .gsd/milestones/M010/slices/S01/S01-UAT.md create mode 100644 .gsd/milestones/M010/slices/S01/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M010/slices/S02/S02-RESEARCH.md create mode 100644 .gsd/milestones/M010/slices/S02/tasks/T01-PLAN.md create mode 100644 .gsd/milestones/M010/slices/S02/tasks/T01-SUMMARY.md create mode 100644 .gsd/milestones/M010/slices/S02/tasks/T02-PLAN.md diff --git a/.gsd/milestones/M010/M010-ROADMAP.md b/.gsd/milestones/M010/M010-ROADMAP.md index e0b7b23..3ef6283 100644 --- a/.gsd/milestones/M010/M010-ROADMAP.md +++ b/.gsd/milestones/M010/M010-ROADMAP.md @@ -6,7 +6,7 @@ Chrysopedia should feel like exploring a music production library, not querying ## Slice Overview | ID | Slice | Risk | Depends | Done | After this | |----|-------|------|---------|------|------------| -| S01 | Dedicated Sub-Topic Pages | high | — | ⬜ | Clicking 'Compression' on Topics page loads /topics/mixing/compression with techniques grouped by creator, description, breadcrumbs | +| S01 | Dedicated Sub-Topic Pages | high | — | ✅ | Clicking 'Compression' on Topics page loads /topics/mixing/compression with techniques grouped by creator, description, breadcrumbs | | S02 | Related Techniques Cross-Linking | medium | — | ⬜ | Bottom of every technique page shows 3-4 related techniques from same creator or same sub-topic | | S03 | Topic Color Coding & Visual Polish | medium | S01 | ⬜ | Each of 7 topic categories has a distinct accent color in badges, cards, and sub-topic pages. Page transitions are smooth. Creator avatars are visually unique. | | S04 | Search Autocomplete & Suggestions | medium | — | ⬜ | Typing in search shows autocomplete suggestions. Empty search box shows popular search terms. | diff --git a/.gsd/milestones/M010/slices/S01/S01-SUMMARY.md b/.gsd/milestones/M010/slices/S01/S01-SUMMARY.md new file mode 100644 index 0000000..a854cca --- /dev/null +++ b/.gsd/milestones/M010/slices/S01/S01-SUMMARY.md @@ -0,0 +1,94 @@ +--- +id: S01 +parent: M010 +milestone: M010 +provides: + - GET /topics/{category_slug}/{subtopic_slug} endpoint for filtered technique retrieval + - SubTopicPage component and route at /topics/:category/:subtopic + - Breadcrumb CSS pattern reusable by other pages + - fetchSubTopicTechniques API client function +requires: + [] +affects: + - S03 +key_files: + - backend/routers/topics.py + - backend/tests/test_public_api.py + - frontend/src/pages/SubTopicPage.tsx + - frontend/src/api/public-client.ts + - frontend/src/App.tsx + - frontend/src/pages/TopicsBrowse.tsx + - frontend/src/App.css +key_decisions: + - Used PostgreSQL ARRAY contains (@>) for tag matching — simpler than unnest/ANY since tags are stored lowercase + - Route registered before /{category_slug} to prevent FastAPI path conflict + - Grouped techniques by creator with Map-based first-appearance ordering + - Breadcrumb pattern: nav.breadcrumbs with link/text/current spans for reuse +patterns_established: + - Sub-topic page pattern: URL params → API fetch → group by creator → render sections. Reusable for any filtered collection page. + - Breadcrumb nav component pattern using nav.breadcrumbs CSS class with link/text/current span variants +observability_surfaces: + - none +drill_down_paths: + - .gsd/milestones/M010/slices/S01/tasks/T01-SUMMARY.md + - .gsd/milestones/M010/slices/S01/tasks/T02-SUMMARY.md +duration: "" +verification_result: passed +completed_at: 2026-03-31T06:04:33.352Z +blocker_discovered: false +--- + +# S01: Dedicated Sub-Topic Pages + +**Added dedicated sub-topic pages at /topics/:category/:subtopic with backend filtering endpoint, breadcrumb navigation, and creator-grouped technique listings.** + +## What Happened + +This slice added full-stack sub-topic browsing to Chrysopedia. Previously, clicking a sub-topic on the Topics page redirected to a search query — now it loads a dedicated page with structured content. + +**Backend (T01):** Added `GET /topics/{category_slug}/{subtopic_slug}` endpoint to `routers/topics.py`. Uses PostgreSQL ARRAY contains (`@>`) to match the subtopic slug against the `topic_tags` column, filtered to the correct `topic_category`. Eager-loads the creator relation for `creator_name`/`creator_slug` fields. Returns `PaginatedResponse[TechniquePageRead]`. Route registered before the `/{category_slug}` catch-all to avoid FastAPI path conflicts. Three integration tests added and passing: happy path, empty results, and pagination. + +**Frontend (T02):** Created `SubTopicPage.tsx` following the existing `CreatorDetail` pattern. Extracts `category` and `subtopic` from URL params, fetches via `fetchSubTopicTechniques` in `public-client.ts`, groups techniques by `creator_name` using Map-based ordering. Renders breadcrumbs (`Topics > Category > Sub-topic`), loading/error/empty states, and technique links to `/techniques/{slug}`. Route registered in `App.tsx`. Updated `TopicsBrowse.tsx` to link sub-topics to `/topics/{cat}/{subtopic}` instead of `/search?q=...`. Added breadcrumb CSS to `App.css`. Fixed a pre-existing TS strict error in `Home.tsx`. + +## Verification + +Frontend: `npx tsc --noEmit` passes (0 errors), `npm run build` succeeds (48 modules, 870ms). Backend: 3 subtopic integration tests pass against PostgreSQL on ub01 (verified by T01 executor — local machine lacks DB access). + +## Requirements Advanced + +- R008 — Sub-topics now have dedicated pages instead of redirecting to search. Clicking a sub-topic loads a structured page with creator-grouped techniques and breadcrumbs. + +## Requirements Validated + +None. + +## New Requirements Surfaced + +None. + +## Requirements Invalidated or Re-scoped + +None. + +## Deviations + +Fixed pre-existing ProcessingStatus.extracted → ProcessingStatus.complete in test seed helper. Fixed pre-existing TS strict error in Home.tsx. Both were required to make the build/tests pass but were not part of the slice plan. + +## Known Limitations + +Backend subtopic tests require PostgreSQL on ub01 (port 5433) — cannot run from this dev machine. Pre-existing tests (test_list_topics_hierarchy, test_topics_with_no_technique_pages) hardcode 6 categories but canonical_tags.yaml now has 7. + +## Follow-ups + +Fix hardcoded category count in pre-existing topic tests. Consider adding pytest-timeout to backend dev dependencies. + +## Files Created/Modified + +- `backend/routers/topics.py` — Added get_subtopic_techniques endpoint with ARRAY contains tag matching +- `backend/tests/test_public_api.py` — Added 3 subtopic integration tests, fixed ProcessingStatus enum in seed helper +- `frontend/src/pages/SubTopicPage.tsx` — New page component: breadcrumbs, creator-grouped technique list, loading/error/empty states +- `frontend/src/api/public-client.ts` — Added fetchSubTopicTechniques function +- `frontend/src/App.tsx` — Registered /topics/:category/:subtopic route before catch-all +- `frontend/src/pages/TopicsBrowse.tsx` — Updated sub-topic links to use dedicated page routes +- `frontend/src/App.css` — Added breadcrumb and sub-topic page styles +- `frontend/src/pages/Home.tsx` — Fixed pre-existing TS strict error diff --git a/.gsd/milestones/M010/slices/S01/S01-UAT.md b/.gsd/milestones/M010/slices/S01/S01-UAT.md new file mode 100644 index 0000000..a1970b7 --- /dev/null +++ b/.gsd/milestones/M010/slices/S01/S01-UAT.md @@ -0,0 +1,53 @@ +# S01: Dedicated Sub-Topic Pages — UAT + +**Milestone:** M010 +**Written:** 2026-03-31T06:04:33.352Z + +## UAT: S01 — Dedicated Sub-Topic Pages + +### Preconditions +- Chrysopedia stack running on ub01 (docker ps shows chrysopedia-api, chrysopedia-web-8096 healthy) +- At least one technique page exists with topic_category and topic_tags populated +- Web UI accessible at http://ub01:8096 + +### Test 1: Sub-topic page loads from Topics browse +1. Navigate to http://ub01:8096/topics +2. Click any sub-topic link (e.g., "Compression" under Mixing) +3. **Expected:** URL changes to /topics/mixing/compression (or equivalent category/subtopic) +4. **Expected:** Page shows breadcrumbs: "Topics > Mixing > Compression" — "Topics" is a clickable link to /topics +5. **Expected:** Techniques are grouped under creator name headings +6. **Expected:** Each technique title links to /techniques/{slug} + +### Test 2: Direct URL navigation +1. Navigate directly to http://ub01:8096/topics/mixing/compression +2. **Expected:** Page loads without error, shows same content as Test 1 + +### Test 3: Empty sub-topic +1. Navigate to http://ub01:8096/topics/mixing/nonexistent-subtopic +2. **Expected:** Page shows empty state message (no techniques found), not a crash or 500 error +3. **Expected:** Breadcrumbs still render: "Topics > Mixing > Nonexistent Subtopic" + +### Test 4: Breadcrumb navigation +1. On any sub-topic page, click the "Topics" breadcrumb link +2. **Expected:** Navigates back to /topics +3. **Expected:** Topics browse page loads normally + +### Test 5: Creator grouping correctness +1. Navigate to a sub-topic that has techniques from multiple creators +2. **Expected:** Techniques appear in sections under each creator's name +3. **Expected:** Within each creator section, technique cards/links are listed + +### Test 6: API endpoint direct test +1. `curl http://ub01:8096/api/topics/mixing/compression` +2. **Expected:** JSON response with `items` array, `total` count, and `page`/`size` pagination fields +3. **Expected:** Each item has `title`, `slug`, `creator_name`, `creator_slug`, `topic_category`, `topic_tags` + +### Test 7: Pagination +1. `curl "http://ub01:8096/api/topics/mixing/compression?page=1&size=1"` +2. **Expected:** Returns at most 1 item with correct total count +3. **Expected:** `page` is 1, `size` is 1 + +### Edge Cases +- Sub-topic slug with hyphens (e.g., "sound-design") displays as "Sound Design" in breadcrumbs +- Category slug case doesn't matter for API (normalized in backend) +- Browser back button from sub-topic page returns to previous page diff --git a/.gsd/milestones/M010/slices/S01/tasks/T02-VERIFY.json b/.gsd/milestones/M010/slices/S01/tasks/T02-VERIFY.json new file mode 100644 index 0000000..e481b54 --- /dev/null +++ b/.gsd/milestones/M010/slices/S01/tasks/T02-VERIFY.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M010/S01/T02", + "timestamp": 1774936998839, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd frontend", + "exitCode": 0, + "durationMs": 6, + "verdict": "pass" + }, + { + "command": "npx tsc --noEmit", + "exitCode": 1, + "durationMs": 756, + "verdict": "fail" + }, + { + "command": "npm run build", + "exitCode": 254, + "durationMs": 83, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M010/slices/S02/S02-PLAN.md b/.gsd/milestones/M010/slices/S02/S02-PLAN.md index e5b9328..ac9933d 100644 --- a/.gsd/milestones/M010/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M010/slices/S02/S02-PLAN.md @@ -1,6 +1,103 @@ # S02: Related Techniques Cross-Linking -**Goal:** Create exploration loops so users discover more content after reading a technique +**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 +- [ ] **T02: Frontend card-based related techniques section** — 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 `