fix: keep onboarding qscore baseline at 35
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../db/client.js";
|
import { db } from "../db/client.js";
|
||||||
import { growQscoreLatest, growQscoreProjectionState, growQscoreSignals } from "../db/schema.js";
|
import { growQscoreLatest, growQscoreProjectionState, growQscoreSignals } from "../db/schema.js";
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ export async function ensureOnboardingBaselineQscore(
|
|||||||
const completedAt = onboardingCompletedAt(preferences);
|
const completedAt = onboardingCompletedAt(preferences);
|
||||||
if (!completedAt) return false;
|
if (!completedAt) return false;
|
||||||
|
|
||||||
const [existingSignals] = await db
|
const latestSignals = await db
|
||||||
.select({ count: sql<number>`count(*)::int` })
|
.select({ signalId: growQscoreLatest.signalId, score: growQscoreLatest.score })
|
||||||
.from(growQscoreLatest)
|
.from(growQscoreLatest)
|
||||||
.where(and(eq(growQscoreLatest.userId, userId), eq(growQscoreLatest.present, true)));
|
.where(and(eq(growQscoreLatest.userId, userId), eq(growQscoreLatest.present, true)));
|
||||||
|
|
||||||
@@ -44,11 +44,57 @@ export async function ensureOnboardingBaselineQscore(
|
|||||||
.where(eq(growQscoreProjectionState.userId, userId))
|
.where(eq(growQscoreProjectionState.userId, userId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (Number(existingSignals?.count ?? 0) > 0 || (existingProjection?.score ?? 0) > 0) {
|
const now = new Date();
|
||||||
return false;
|
|
||||||
|
// 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 = {
|
const raw = {
|
||||||
reason: "completed onboarding baseline",
|
reason: "completed onboarding baseline",
|
||||||
completedAt: completedAt.toISOString(),
|
completedAt: completedAt.toISOString(),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ function nestedNumber(record: Record<string, unknown>, keys: string[]): number |
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RESUME_UPLOAD_BASELINE_SCORE = 35;
|
||||||
|
|
||||||
function extractResumeSignals(event: GrowEventRow): QscoreSignal[] {
|
function extractResumeSignals(event: GrowEventRow): QscoreSignal[] {
|
||||||
const payload = event.payload ?? {};
|
const payload = event.payload ?? {};
|
||||||
const analysis = asRecord(payload.analysis ?? payload.result ?? payload);
|
const analysis = asRecord(payload.analysis ?? payload.result ?? payload);
|
||||||
@@ -38,8 +40,11 @@ function extractResumeSignals(event: GrowEventRow): QscoreSignal[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signals: QscoreSignal[] = [];
|
const signals: QscoreSignal[] = [];
|
||||||
if (event.type.includes("uploaded") || event.type.includes("created") || event.type.includes("analysis")) {
|
if (event.type.includes("uploaded") || event.type.includes("created")) {
|
||||||
signals.push(signal("resume.uploaded", 100, { eventId: event.id }));
|
// 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"]);
|
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 }));
|
if (ats !== undefined) signals.push(signal("resume.ats_compatibility", ats, { eventId: event.id }));
|
||||||
|
|||||||
Reference in New Issue
Block a user