feat: Skills feature — user-orderable sections + nested skills (#22) #126
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "22-skills"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #22. Mirrors the notes reference pattern (ADR-0012) for a two-table per-user hierarchy:
skill_sectionsgroupingskillsunder labels like "Languages" / "Frameworks", with both rows user-orderable via up/down arrows and section-collapsible in the UI.Shape
Backend (migration 007)
skill_sectionsuser_idFK→users CASCADE,display_order,nameskillsuser_idFK→users CASCADE,section_idFK→skill_sections CASCADE,display_order,nameuser_idis denormalised ontoskillsso per-user isolation stays a one-column WHERE (cheap) — backed byskills_user_idx. The double cascade on skills (user AND section) makes the row die on the first matching cascade; SQLite + Postgres both honour it.Repositories (notes pattern)
findById/update/deleteByIdunscoped (caller verifies ownership in the route layer, per CLAUDE.md's "don't leak existence" rule).createauto-assignsdisplay_order = max(siblings) + 1.moveUp/moveDownswap with the immediate higher/lower-ordered sibling within the scope (user for sections, section for skills).Swap is two independent UPDATEs, not wrapped in a transaction. Kysely transactions on libsql in-memory mode (the CI test path) trip the schema between the transaction and subsequent queries — not reproducible against file-backed sqlite or Postgres but the test path is in-memory. Two-rows-sharing-display_order crash window is acceptable: the next move corrects it.
API (
loadOwned*sentinel pattern)Cross-user → 404 everywhere; never 403 (don't leak existence).
UI (
app/(app)/skills/)TanStack Query + Form. Sections render as collapsible cards (state in
localStorage, per-section), with rename / up / down / delete controls. Skills list inside with the same controls. Add-section + add-skill inline forms.Tests (+51)
tests/db/skill-sections.test.tsdescribePerEngine, sequentialdisplay_order, per-user numbering, move + boundary, cross-user isolation in move + list, cascade on user deletetests/db/skills.test.tstests/api/skill-sections.test.tstests/api/skills.test.tsVerification
npm run typecheck— clean.npm run lint— clean.npm test— 332 passed / 75 skipped (was 281 / 50; +51 net new on this branch).npm run build— succeeds./stays○ Static(ADR-0008),/skillsbecomesƒ Dynamicper theforce-dynamicflag./skills, add a section, add skills, reorder via up/down arrows, collapse a section, refresh the page (collapsed state survives vialocalStorage), register a second user and confirm sections don't bleed between accounts.Files
New:
db/migrations/007_skills.ts,db/entities/skill-section.ts,db/entities/skill.ts,db/repositories/skill-sections.ts,db/repositories/skills.tslib/dto/skill.tsapp/api/skill-sections/{route,[id]/route,[id]/move/route}.tsapp/api/skills/{route,[id]/route,[id]/move/route}.tsapp/(app)/skills/skills-client.tsxtests/db/skill-sections.test.ts,tests/db/skills.test.ts,tests/api/skill-sections.test.ts,tests/api/skills.test.tsModified:
db/schema.ts,db/migrator.ts(register migration 007).app/(app)/skills/page.tsx(placeholder → server-component with HydrationBoundary prefetch).tests/db/_engines.ts(postgres cleanup list extended withskills+skill_sections).Acceptance criteria
Closes #22. Part of epic #4.
🤖 Generated with Claude Code
📊 Test coverage
Patch coverage: 90.7% (534/589 added lines) ✅ (soft target ≥ 80%)
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Changed files in this PR (source only — tests excluded):
app/api/skill-sections/[id]/move/route.tsapp/api/skill-sections/[id]/route.tsapp/api/skill-sections/route.tsapp/api/skills/[id]/move/route.tsapp/api/skills/[id]/route.tsapp/api/skills/route.tsdb/migrations/007_skills.tsdb/migrator.tsdb/repositories/skill-sections.tsdb/repositories/skills.tslib/dto/skill.tsSoft thresholds per ADR-0019. Coverage is informational and does not block merge.