expert-quality-metricsrun.md00_intake/stub.mdThe quality half of the Performance section is dummy: csat, vendorRating, repeatWorkRate,
reworkRate. Coverage is mixed — CSAT already has a model
(csat.ts) and an avg-score helper
(getExpertMatchingContext),
repeat-work is derivable from the expert's leads, but vendor rating and rework rate have no
backing concept at all.
/feature decompose before building, per the epic flag.touches: packages/services/src/db/models (vendor-rating + rework signal),
packages/services/src/db/services/matching/expert-metrics.ts (reuse CSAT),
packages/services/src/server, apps/web/components/dashboard/expert/performance-section.tsx.
</content>
</invoke>01_define/output/spec.mdThe Quality card of the expert workbench's Performance section is hard-coded: csat, vendorRating,
repeatWorkRate, and reworkRate all come from mock data (apps/web/lib/mock/expert.ts). An expert
can't trust the feedback loop that's meant to "support self-improvement" when the numbers are fictional.
This is the last of the expert-dashboard-data epic (8 of 8) and advances Build the Bridge / Q2 2026
Objective 3 — Validate Technical Infrastructure & Payout Flow: real quality signals are what let us
validate that the matching and delivery flow produces trustworthy expert performance data. Coverage is
mixed today — CSAT has a model and an avg-score helper and repeat-work is derivable from leads, but
vendor rating and rework rate have no backing concept at all.
Compute all four quality figures from real data and wire the Quality card to them. Each figure shows an
honest em-dash (—) when it has no data yet, never a fabricated number.
csat model +
getMetricsForExpert in expert-metrics.ts). CSAT is stored on a 1–10 scale and the card displays
/ 5, so map the average to the /5 scale (divide by 2) and round to one decimal. CSAT links to an
expert via its lead.expert (the csat model stores lead/customer/vendor, not expert
directly), so the aggregate joins through leads.getMetricsForExpert: isActiveProject: false, bidPool: false, isBrdApproved: true.)expert-rating model holding a vendor's
1–5 rating of an expert for a given lead (tenantId, vendor, expert, lead, score). Capture is
a minimal 1–5 star control on the lead-detail page (apps/web/app/(app)/service-leads/[id]) shown to
the vendor on a completed lead. The card figure is the average of all of that expert's vendor ratings.isReworked boolean to the lead model.
Capture is a minimal "flag as reworked" control on the lead-detail page shown to SDM/CSM on a
completed lead. Rate = the expert's completed leads flagged reworked ÷ the expert's completed leads,
shown as a percentage.@sustentus/services (extending
ExpertMetrics / the matching service, or a sibling quality function) and wire the Quality card in the
expert dashboard to them, replacing the mock quality object./5 display scale,
or — when the expert has no completed CSAT.—/0% when the expert has no completed leads.expert-rating model.— when
there are none.isReworked on the lead.—/0% when
the expert has no completed leads.expert-earnings-summary feature, 7 of 8).expert-rating model + lead
isReworked flag and their two capture controls (per the epic flag — if either grows, it splits out).expert-workbench-foundation (1 of 8),
which converts the expert dashboard from mock to real server-fetched data. The services, models, and
capture controls here land independently; the final card wiring attaches to whatever data path the
foundation establishes. If the foundation hasn't merged when Build runs, wire the card through an
equivalent server fetch rather than reverting to mock.)02_build/output/notes.md@sustentus/services)db/models/lead.ts — added a minimal isReworked boolean (default false) to the lead
interface + schema (the rework signal).db/models/expert-rating.ts (new) — minimal expertRating model: a vendor's 1–5 score of an
expert for a lead, tenant-scoped, unique per (tenant, lead, vendor). Registered in
db/models/index.ts.db/services/matching/expert-quality.ts (new) — getExpertQualityMetrics(expertId, tenantId)
returns the four real figures, each null when there's no data:overallScore of completed CSATs joined to the expert through their
completed leads (the csat model has no expert field), mapped to /5 via
CsatService.toDisplayScore.isActiveProject:false, bidPool:false, isBrdApproved:true): repeat-customer share and
isReworked share, as percentages.expertRating scores.db/services/matching/index.ts.db/services/expert-rating/{index,instance}.ts (new) — expertRatingService with
findForLeadByVendor (preselect) and upsertForLead (derives the expert from the lead, upserts
one rating per vendor+lead). Exported from db/services/index.ts.apps/web)app/(app)/service-leads/[id]/quality-actions.ts (new) — setExpertVendorRatingAction
(vendor-only) and setLeadReworkAction (csm/sdm-only) using resolveActionContext +
runActionBody, revalidating the lead page.components/service-leads/expert-vendor-rating/expert-vendor-rating.tsx (new) — 1–5 star control.components/service-leads/lead-rework-flag/lead-rework-flag.tsx (new) — flag/clear rework control.app/(app)/service-leads/[id]/page.tsx — renders the vendor-rating control (vendor, completed
lead with an expert) and the rework control (csm/sdm, completed lead with an expert) in the right
column.app/(app)/expert/dashboard/page.tsx — converted to a server component that fetches
getExpertQualityMetrics for the signed-in expert and feeds the Quality card real values; the
other (not-yet-built) sections still read mock. Honest "—" when quality can't be resolved.components/dashboard/expert/performance-section.tsx — Quality card now accepts nullable
csat/vendorRating and renders "—" (no stars, no /5) when absent. No hard-coded quality values
remain in the card path./5-scaled, or "—" — via getExpertQualityMetrics +
CsatService.toDisplayScore, wired into the card.expertRating.isReworked on lead.expert-workbench-foundation (1 of 8) hasn't merged, so the expert
dashboard page was server-converted just for the Quality card (per the spec's sequencing note);
the remaining sections deliberately stay on mock until the foundation lands.csat model stores lead/customer/vendor (not expert), so the aggregate
joins through lead.expert over the expert's completed leads rather than the pre-existing
getMetricsForExpert CSAT.find({ expert }), which matched nothing.03_ship/output/changelog.md03_ship/output/investor-update.mdSustentus experts now see their true quality signals on the go-live workbench — customer satisfaction, platform rating, repeat-work rate, and rework rate — each computed from real delivery data instead of placeholder numbers. This advances Build the Bridge and our Q2 2026 Objective 3: Validate Technical Infrastructure & Payout Flow: trustworthy quality data is what lets us stand behind the experts we match and the deliveries we process end to end.
03_ship/output/pr.mdtechnical/packages/services page documents entrypoints/structure, which are unchanged (a new model + service function doesn't alter the documented altitude).business/feature-role-matrix/experts (view quality metrics), …/vendors (rate expert on a delivered lead), …/leads (flag delivery reworked) in this PR.expert-workbench-foundation's getWorkbench quality slot (foundation now live on main), so the whole workbench reads from one server fetch.03_ship/output/investor-update.md; changelog entry appended to apps/help/app/changelog/page.mdx.expertRating partial-index $or mirrors the existing csat.ts pattern — not a regression.main into the branch (the dependency expert-workbench-foundation #460 had landed). Integrated real quality into the foundation's expertWorkbenchService.getWorkbench quality slot (the slot it left for this feature) instead of a standalone page fetch; ExpertWorkbenchQuality made nullable. Conflicts in the dashboard page, changelog, and services barrel resolved.getExpertQualityMetrics averages completed CSATs (joined via lead.expert) through CsatService.toDisplayScore; card renders "—" when null.expertRating (setExpertVendorRatingAction + expertRatingService.upsertForLead).isReworked on the lead (setLeadReworkAction).