skill-service-retierrun.md00_intake/stub.mdSkills are grouped under skillCategory, a taxonomy that is disconnected from products and services.
The tiered catalogue wants skills to derive from services, not from an independent category tree,
so skill categories must go and skills must instead link to services.
Remove the skillCategory collection entirely and link skill ↔ service many-to-many (a
generic skill can support several services). Retire everything that depended on categories: the
skillCategory model, the required skillCategory FK on skill, the category admin pages
(/skills/categories/**) and their nav, the category CRUD on SkillService, and the
category grouping utilities (groupSkillsByCategory, and formatSkillsForSelect if category-shaped).
Update the skill create/edit admin to pick services instead of a category. Because skillCategory
is currently a required FK and is read across admin + utils, this feature necessarily updates those
call sites so the build stays green.
skillCategory model, FK, admin pages, nav entries and CRUD are gone; nothing references it and the build is green.skills[] on leads and experts continue to resolve (skills still exist as documents); only the categorisation changes.skillCategory import/route/nav
remains. A grep sweep for skillCategory / skill-categor / groupSkillsByCategory is the
completeness check.mongodb-schema-design.packages/services/src/db/models/{skill,skill-category}.ts,
packages/services/src/db/services/skill/, packages/services/src/utils/{groupSkillsByCategory,formatSkillsForSelect}.ts,
apps/web/app/(app)/skills/** (esp. categories/** and skill create/edit).01_define/output/spec.mdSkills are grouped under skillCategory, a taxonomy that is disconnected from products and services.
The tiered catalogue (platform → product → service → skill, see the catalogue-hierarchy epic) wants
skills to derive from services instead of an independent category tree. This advances the Build the
Bridge / 2026-Q2 Objective 2 goal of a repeatable lead-generation pipeline with accurate lead-to-vendor
matching: a catalogue where skills trace back to the services and products a vendor actually offers is
a prerequisite for the lead-capture cascade (product → service → skill) that later stubs in this epic
build on top of.
Remove the skillCategory collection entirely and link skill ↔ service many-to-many (a generic
skill can support several services). Retire everything that depends on categories:
skillCategory Mongoose model and its required FK on skill/skills/categories/**) and their nav entrygroupSkillsByCategory, and formatSkillsForSelect where it is
category-shaped)skillCategory populate in the leads service (packages/services/src/db/services/leads/index.ts)
— leads keep populating skills, just without the nested categoryAdd a tenant-scoped skill ↔ service many-to-many link, with a reverse lookup ("skills for service
X") available in the service layer for later stubs (lead-capture cascade, demo reseed) to consume.
Update the skill create/edit admin to pick one or more services in place of the old category
selector — this field is required, at least one service, mirroring the integrity guarantee the old
required skillCategory FK gave.
Because skillCategory is currently a required FK and is read across admin, the leads service, and
utils, this feature necessarily updates every one of those call sites so the build stays green.
Migration: this run includes a real db-migration (symmetric up/down): drop the skillCategory
collection, remove the skillCategory field from skill, and add the new services M2M field
(backfilled empty on existing skill documents — no attempt to infer service links from the retired
category data; that reconciliation is explicitly out of scope, see below).
Docs flag for Release: apps/docs/app/business/roles/page.mdx currently lists a CSM capability as
"Skills — Skill categories and expertise area management". This wording is stale once categories are
gone and must be updated by docs-sync at Release (e.g. to reflect skills-linked-to-services).
skillCategory model, FK, admin pages (/skills/categories/**), nav entries, and CRUD are gone; nothing references it and the build is green.skills[] on leads and experts continue to resolve (skills still exist as documents; the leads service's skill populate no longer nests a category); only the categorisation mechanism changes.db-migration ships with this run: drops the skillCategory collection, removes skillCategory from skill, adds the new services M2M field (empty on backfill), with a working down migration.apps/docs/app/business/roles/page.mdx's CSM "Skills" capability line is flagged for docs-sync to update at Release (stale category wording).product-service-links) and product admin (product-services-admin) — separate stubs in this epic.lead-capture-cascade) and demo reseed (catalogue-demo-reseed) — separate stubs.skills[].skill ↔ service links from the retired skillCategory data — the migration backfills services empty; populating real links is a data-entry/reseed concern (catalogue-demo-reseed for demo tenants), not this migration.02_build/output/notes.mdpackages/services/src/db/models/skill.ts: dropped the skillCategory FK; added a required,
non-empty services: ObjectId[] M2M field with a tenant+services index for the reverse lookup.packages/services/src/db/models/skill-category.ts: deleted (model retired).packages/services/src/db/models/index.ts, taxonomy.ts: dropped the skillCategory export /
stale doc-comment references.packages/services/src/db/services/skill/index.ts: rewritten — SkillCreateInput/SkillUpdateInput
now take serviceIds: string[] (validated non-empty + must exist for the tenant); findAll/findById
populate services ({_id, name}[]) instead of skillCategory; all skillCategory CRUD methods
removed; added findActiveByServiceIds as the explicit reverse lookup for later stubs
(lead-capture-cascade, catalogue-demo-reseed).packages/services/src/db/services/leads/index.ts: the lead bid-detail populate now nests
skills.services instead of skills.skillCategory; dropped the getSkillCategoryModel dependency.packages/services/src/utils/{groupSkillsByCategory,formatSkillsForSelect}.ts: deleted (dead code,
category-shaped, unused anywhere in the app).packages/services/src/db/migrations/1782800000000-skill-service-retier.ts: new migration —
backfills skill.services = [] / unsets skillCategory, swaps the old category index for a
tenantId+services index, and drops the skillcategories collection. down reverses the schema
shape (data in the dropped collection is not recoverable, called out in the migration's docstring).apps/web/app/(app)/skills/**, apps/web/components/skills/**: the skill admin now manages a
single /skills list (no more skills/categories tabs) with a service filter dropdown; skill
create/edit uses a required multi-select checkbox list of services instead of a category <select>.
Removed the whole /skills/categories/** route tree and its components
(category-form, category-delete-button, category-row-actions, categories-list,
skill-categories-list-toolbar, skill-categories-list-query).apps/web/components/expert/my-knowledge/{my-knowledge,my-knowledge-form}.tsx: the skill grouping
key changed from the category name to the joined linked-service names (experts' own skill selection
UX is otherwise untouched, per the epic's "expert profiles out of scope").skillCategory model, FK, admin pages (/skills/categories/**), nav entries, and CRUD are gone; nothing references it and the build is green.findActiveByServiceIds, plus the serviceId filter on findAll)..min(1, ...)).skills[] on leads and experts continue to resolve; the leads service's skill populate no longer nests a category (now nests services).db-migration ships with this run: drops the skillCategory collection, removes skillCategory from skill, adds the new services M2M field (empty on backfill), with a working down migration.apps/docs/app/business/roles/page.mdx's CSM "Skills" capability line is flagged in spec.md's Proposed change for docs-sync to update at Release.pnpm --filter @sustentus/services typecheck — clean.pnpm --filter web typecheck — clean (after rebuilding @sustentus/services so dist/*.d.ts
picked up the model/service changes — the web app resolves @sustentus/services/* through the
package's built output, not source).pnpm --filter web exec eslint . / pnpm --filter @sustentus/services exec eslint . — 0 errors
(pre-existing warnings only, none in touched files).pnpm --filter web build — succeeds; /skills, /skills/[id], /skills/[id]/edit,
/skills/create compile, /skills/categories/** no longer exists in the route list.db-migrate workflow applies it on merge to main, per packages/services/AGENTS.md.SkillService.softDelete still guards on lead/expert skill references only — unchanged from
before, since neither of those checks depended on categories.skillcategories collection drop in the migration is irreversible data loss by design (this is
the one destructive stub in the catalogue-hierarchy epic, per the intake breakdown); down
restores the schema shape, not the original per-skill category assignments.03_release/output/changelog.mdYou can now link each skill directly to the services it supports, instead of filing it under a separate category.
Every skill needs at least one linked service, so the catalogue always shows what each skill is actually for.
03_release/output/investor-update.mdWho it's for: Admin, CSM, SDM What shipped: Skills now link directly to the services they support, replacing a disconnected category taxonomy. Why it matters: Build the Bridge / 2026-Q2 Objective 2 (Build Repeatable Lead Generation Pipeline) — accurate lead-to-vendor matching needs skills traceable to services.
Every skill must now name at least one supported service — no more orphaned entries.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdapps/docs/app/technical/demo-environment/page.mdx, apps/docs/public/taxonomy-governance-matrix.md updated in this PRapps/docs/app/business/roles/page.mdx (CSM "Skills" capability line) updated in this PRapps/help/app/changelog/2026-07-06-skill-service-retier/page.mdx) + investor draft (pipeline/runs/skill-service-retier/03_release/output/investor-update.md) in this PRRan /code-review medium (complexity: standard) via 8 parallel finder angles against origin/main...HEAD. Fixed on the branch:
ServiceService.guardReferences (packages/services/src/db/services/service/index.ts) didn't check whether any skill still linked to a service before archiving it, unlike ProductService's equivalent guard — the old skillCategory archive guard had no successor on the new skill→service edge. Added a skill-count check alongside the existing product-link check.apps/web/components/expert/my-knowledge/my-knowledge.tsx) joined a skill's service names in DB order, so two skills sharing the same services in a different order rendered as separate groups. Sorted before joining, and reused the sorted list for both the sort key and the grouping key instead of computing it twice.type, never interface") — ISkillPopulated in packages/services/src/db/services/skill/index.ts was declared with interface on a line this PR itself edited. Converted to type.SkillService.update discarded findOneAndUpdate's returned document and re-fetched via findById; now populates services directly from the update result, saving a round trip. The skill admin's create/edit/list pages each independently fetched-and-mapped the service option list; extracted into apps/web/lib/service-options.ts and parallelized the edit page's two independent reads with Promise.all.packages/services/src/db/services/leads/index.ts and the taxonomy-governance-matrix's Service guard row (which predated this PR but was in scope since this PR touches that exact row).Findings surfaced but not actioned (documented trade-offs, not regressions):
skill.services empty on existing skills, which — combined with the schema's new required-non-empty validator — leaves pre-existing skills needing manual re-linking. This is the explicit, spec-documented trade-off of the "one destructive stub" in the catalogue-hierarchy epic (out of scope: inferring/backfilling links from retired category data); catalogue-demo-reseed repopulates demo tenants.SkillService doesn't extend the generic TaxonomyService<T> base class the other taxonomies use (pre-existing pattern, not introduced by this PR — skill's CRUD was always bespoke to support the service-validation/populate logic).populateServices, validateServiceIds not filtering isActive) mirror gaps that already existed in the pre-PR skillCategory code and are not new regressions.skillCategory model, FK, admin pages (/skills/categories/**), nav entries, and CRUD are gone — verified via repo-wide grep sweep (zero non-migration references) and a clean pnpm --filter web build.SkillService.findActiveByServiceIds plus the serviceId filter on findAll..min(1, ...) in actions.ts + the Mongoose schema validator).skills[] keep resolving — the leads bid-detail populate now nests services instead of skillCategory; verified via pnpm --filter @sustentus/services typecheck.db-migration ships with this run (1782900000000-skill-service-retier.ts) — renamed during Release to dedupe a timestamp collision with the sibling product-service-links migration that merged to main first (1782800000000-product-service-link-indexes.ts); rebased cleanly.