Estimate: 50m · Depends on: 6.13.2
The category / tag model that makes the square browsable by topic — the GitHub-Topics / GitLab-Topics axis. A public project carries tags; the square filters to a tag (6.13.3) and lists the categories sorted by project count.
ProjectTag (a normalized tag — { id, slug, label }, bounded set) + a ProjectTagAssignment join to project, BOTH FKs modelled as Prisma @relation on both sides (the CLAUDE.md FK-as-@relation rule — no raw-SQL-only FK), unique on (projectId, tagId). Tags are reusable across projects (a shared topic vocabulary, the GitHub-Topics shape), not free-text per project duplicated.access_level = public filter, so a tag’s count is its public-project count, and a tag with only non-public projects does not inflate the square). This read feeds the 6.13.3 category filter + the categories-browse panel.Stay 4-layer: the tag model + assignment in prisma/schema.prisma + a single-op repository; the tagging service (admin-gated) + the tag-facet read service own their logic; no raw Prisma in routes.
ProjectTag + the ProjectTagAssignment join (every FK an @relation on both sides, unique on (projectId, tagId)); prisma migrate dev reports no drift.access_level = public filter the tag counts respect) + 6.13.3 (the category filter this model feeds).scripts/plan-seed/data/story-6.4.ts — the project-admin check the tagging gate reuses.motir-core/CLAUDE.md § 4-layer + § migration FK-as-@relation rule.