From 9fd478c095cb2681d3dc930ecb7b723775aa35bf Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:51:22 +0530 Subject: [PATCH] fix: keep onboarding qscore baseline at 35 --- src/events/onboarding-qscore.ts | 58 ++++++++++++++++++++--- src/events/projectors/qscore-projector.ts | 9 +++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/events/onboarding-qscore.ts b/src/events/onboarding-qscore.ts index 48efc13..5ad88ab 100644 --- a/src/events/onboarding-qscore.ts +++ b/src/events/onboarding-qscore.ts @@ -1,4 +1,4 @@ -import { and, eq, sql } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { db } from "../db/client.js"; import { growQscoreLatest, growQscoreProjectionState, growQscoreSignals } from "../db/schema.js"; @@ -33,8 +33,8 @@ export async function ensureOnboardingBaselineQscore( const completedAt = onboardingCompletedAt(preferences); if (!completedAt) return false; - const [existingSignals] = await db - .select({ count: sql`count(*)::int` }) + const latestSignals = await db + .select({ signalId: growQscoreLatest.signalId, score: growQscoreLatest.score }) .from(growQscoreLatest) .where(and(eq(growQscoreLatest.userId, userId), eq(growQscoreLatest.present, true))); @@ -44,11 +44,57 @@ export async function ensureOnboardingBaselineQscore( .where(eq(growQscoreProjectionState.userId, userId)) .limit(1); - if (Number(existingSignals?.count ?? 0) > 0 || (existingProjection?.score ?? 0) > 0) { - return false; + const now = new Date(); + + // Repair users affected by the old resume-upload projector, which treated a + // plain upload as a perfect 100 score. Uploading a resume during onboarding is + // only baseline evidence; parsed resume/interview/roleplay results should be + // what moves the score upward. + if ( + latestSignals.length === 1 && + latestSignals[0]?.signalId === "resume.uploaded" && + latestSignals[0].score > ONBOARDING_BASELINE_QSCORE + ) { + await db + .update(growQscoreLatest) + .set({ + score: ONBOARDING_BASELINE_QSCORE, + raw: { + reason: "resume upload baseline correction", + correctedFrom: latestSignals[0].score, + correctedAt: now.toISOString(), + }, + updatedAt: now, + }) + .where(and(eq(growQscoreLatest.userId, userId), eq(growQscoreLatest.signalId, "resume.uploaded"))); + + await db + .insert(growQscoreProjectionState) + .values({ + userId, + score: ONBOARDING_BASELINE_QSCORE, + signalCount: 1, + dimensions: { baseline: true, latestSignalIds: ["resume.uploaded"], corrected: true }, + summary: "Baseline Q Score from onboarding resume upload.", + updatedAt: now, + }) + .onConflictDoUpdate({ + target: growQscoreProjectionState.userId, + set: { + score: ONBOARDING_BASELINE_QSCORE, + signalCount: 1, + dimensions: { baseline: true, latestSignalIds: ["resume.uploaded"], corrected: true }, + summary: "Baseline Q Score from onboarding resume upload.", + updatedAt: now, + }, + }); + + return true; } - const now = new Date(); + if (latestSignals.length > 0 || (existingProjection?.score ?? 0) > 0) { + return false; + } const raw = { reason: "completed onboarding baseline", completedAt: completedAt.toISOString(), diff --git a/src/events/projectors/qscore-projector.ts b/src/events/projectors/qscore-projector.ts index 48c5f86..84c4e42 100644 --- a/src/events/projectors/qscore-projector.ts +++ b/src/events/projectors/qscore-projector.ts @@ -15,6 +15,8 @@ function nestedNumber(record: Record, keys: string[]): number | return undefined; } +const RESUME_UPLOAD_BASELINE_SCORE = 35; + function extractResumeSignals(event: GrowEventRow): QscoreSignal[] { const payload = event.payload ?? {}; const analysis = asRecord(payload.analysis ?? payload.result ?? payload); @@ -38,8 +40,11 @@ function extractResumeSignals(event: GrowEventRow): QscoreSignal[] { } const signals: QscoreSignal[] = []; - if (event.type.includes("uploaded") || event.type.includes("created") || event.type.includes("analysis")) { - signals.push(signal("resume.uploaded", 100, { eventId: event.id })); + if (event.type.includes("uploaded") || event.type.includes("created")) { + // Uploading a resume is only a baseline readiness signal. The actual Q Score + // should rise from parsed resume/interview/roleplay evidence, not jump to 100 + // immediately after onboarding. + signals.push(signal("resume.uploaded", RESUME_UPLOAD_BASELINE_SCORE, { eventId: event.id })); } const ats = byCategory.get("ATS Compatibility") ?? nestedNumber(analysis, ["ats_score", "ats_compatibility", "atsCompatibility"]); if (ats !== undefined) signals.push(signal("resume.ats_compatibility", ats, { eventId: event.id }));