diff --git a/src/v1/curator/curator-store.ts b/src/v1/curator/curator-store.ts index 5eccffd..26b251b 100644 --- a/src/v1/curator/curator-store.ts +++ b/src/v1/curator/curator-store.ts @@ -22,7 +22,16 @@ import type { CuratorWeek, } from "./curator-types.js"; import { buildCuratorTaskDeepLink, completionEventsForService, serviceName, serviceToolName } from "./curator-service-links.js"; -import { normalizeActiveCuratorTaskSeed, seed, templateSetFor, type CuratorIcpTemplateSet, type TaskSeed, type WeekTemplate } from "./task-registry.js"; +import { + normalizeActiveCuratorTaskSeed, + seed, + templateSetFor, + trialDaysFor, + trialRecoveryFor, + type CuratorIcpTemplateSet, + type TaskSeed, + type WeekTemplate, +} from "./task-registry.js"; import { kpiDefinitionsForIcp } from "./kpi-registry.js"; import { outcomeDefinitionsForIcp } from "./outcome-report-registry.js"; @@ -61,6 +70,9 @@ const MAX_PLAN_WEEK_COUNT = 6; type PlannedTask = TaskSeed & { weekTheme: string; weekSummary: string; + isTrialTask?: boolean; + isPaidTask?: boolean; + isRecoveryPractice?: boolean; }; type PlanDaySeed = { @@ -74,6 +86,9 @@ type PlanDaySeed = { plannedTasks: PlannedTask[]; generationStatus: "seeded" | "generated" | "adapted"; adaptationReason?: string; + isTrialDay?: boolean; + isPaidDay?: boolean; + hasRecoveryPractice?: boolean; }; type SprintState = { @@ -161,6 +176,31 @@ function makePlannedTask(seedTask: TaskSeed, weekTheme: string, weekSummary: str }; } +function makeTrialTask(seedTask: TaskSeed, weekTheme: string, weekSummary: string): PlannedTask { + return { + ...makePlannedTask(seedTask, weekTheme, weekSummary), + isTrialTask: true, + isPaidTask: false, + }; +} + +function makePaidTask(seedTask: TaskSeed, weekTheme: string, weekSummary: string): PlannedTask { + return { + ...makePlannedTask(seedTask, weekTheme, weekSummary), + isTrialTask: false, + isPaidTask: true, + }; +} + +function makeRecoveryPracticeTask(seedTask: TaskSeed, weekTheme: string, weekSummary: string): PlannedTask { + return { + ...makePlannedTask(seedTask, weekTheme, weekSummary), + isTrialTask: false, + isPaidTask: false, + isRecoveryPractice: true, + }; +} + function hasDirectServiceCompletion(serviceId: CuratorServiceId) { const events = completionEventsForService(serviceId); return !(events.length === 1 && events[0] === "curator.task.completed"); @@ -304,6 +344,31 @@ function fallbackCarryForwardWeek(templateSet: CuratorIcpTemplateSet): WeekTempl }; } +function trialTaskSeedsForVariant(variantId: CuratorIcpId, dayIndex: 1 | 2): TaskSeed[] { + const item = trialDaysFor(variantId)[dayIndex - 1]!; + return [ + seed("proof", "social-branding-service", item.socialTitle, item.socialTask, "10 min", "+1 trial", "Open social proof", ["trial", "social proof", item.socialTitle.toLowerCase()], 10), + seed("measurement", "qscore-service", item.measurementTitle, item.measurementTask, "10 min", "+1 trial", "Open measurement", ["trial", "measurement", item.measurementTitle.toLowerCase()], 12), + seed("proof", "resume-service", item.proofTitle, item.proofTask, "10 min", "+1 trial", "Open proof task", ["trial", "proof", item.proofTitle.toLowerCase()], 15), + seed("practice", item.practiceServiceId, item.practiceTitle, item.practiceTask, "10 min", "+1 trial", "Open practice task", ["trial", "practice", item.practiceTitle.toLowerCase()], 18), + ]; +} + +function trialRecoveryTaskSeed(variantId: CuratorIcpId, missedDayIndex: number): TaskSeed { + const item = trialRecoveryFor(variantId, missedDayIndex); + return seed( + "recovery", + item.serviceId, + item.title, + item.subtitle, + "10 min", + "+1 recovery", + item.serviceId === "interview-service" ? "Open interview preview" : "Open roleplay preview", + ["trial recovery", `missed day ${missedDayIndex}`, item.focus.toLowerCase()], + 10, + ); +} + function planSeedsForVariant(templateSet: CuratorIcpTemplateSet, startDate: string): PlanDaySeed[] { const carryForwardWeek = fallbackCarryForwardWeek(templateSet); const weekTemplates = Array.from({ length: MAX_PLAN_WEEK_COUNT }, (_, index) => ( @@ -317,21 +382,31 @@ function planSeedsForVariant(templateSet: CuratorIcpTemplateSet, startDate: stri const dayOfWeek = (index % DAYS_PER_WEEK) + 1; const weekTemplate = weekTemplates[weekIndex - 1] ?? weekTemplates[0]!; const dayTemplate = weekTemplate.days[dayOfWeek - 1] ?? weekTemplate.days[0]!; + const trialTemplate = dayIndex === 1 || dayIndex === 2 ? trialDaysFor(templateSet.id)[dayIndex - 1] : undefined; + const isTrialDay = Boolean(trialTemplate); + const plannedTasks = trialTemplate + ? trialTaskSeedsForVariant(templateSet.id, dayIndex as 1 | 2).map((task) => makeTrialTask(task, weekTemplate.theme, weekTemplate.summary)) + : [ + makePaidTask(dayTemplate.measurement, weekTemplate.theme, weekTemplate.summary), + makePaidTask(dayTemplate.proof, weekTemplate.theme, weekTemplate.summary), + makePaidTask(dayTemplate.practice, weekTemplate.theme, weekTemplate.summary), + makePaidTask(dayTemplate.roleplay ?? seed("practice", "roleplay-service", "Manager Mode", "Practice one workplace conversation related to today’s sprint focus.", "10 min", "+9 projected", "Open roleplay preview", ["static roleplay", "manager mode"]), weekTemplate.theme, weekTemplate.summary), + ]; planDays.push({ dayIndex, dayIndexInWeek: dayOfWeek, weekIndex, - weekTheme: weekTemplate.theme, - weekSummary: weekTemplate.summary, - focus: (dayTemplate as unknown as { focus?: string }).focus ?? planFocus(weekTemplate.theme, dayTemplate.measurement), - outcome: (dayTemplate as unknown as { outcome?: string }).outcome ?? "Daily outcome captured.", - plannedTasks: [ - makePlannedTask(dayTemplate.measurement, weekTemplate.theme, weekTemplate.summary), - makePlannedTask(dayTemplate.proof, weekTemplate.theme, weekTemplate.summary), - makePlannedTask(dayTemplate.practice, weekTemplate.theme, weekTemplate.summary), - makePlannedTask(dayTemplate.roleplay ?? seed("practice", "roleplay-service", "Manager Mode", "Practice one workplace conversation related to today’s sprint focus.", "10 min", "+9 projected", "Open roleplay preview", ["static roleplay", "manager mode"]), weekTemplate.theme, weekTemplate.summary), - ], + weekTheme: isTrialDay ? "2-Day Trial CareerSprint" : weekTemplate.theme, + weekSummary: isTrialDay + ? "Trial sprint tasks are fixed for the first 48 hours. Paid CareerSprint continuity starts from Day 3." + : weekTemplate.summary, + focus: trialTemplate?.focus ?? (dayTemplate as unknown as { focus?: string }).focus ?? planFocus(weekTemplate.theme, dayTemplate.measurement), + outcome: trialTemplate?.outcome ?? (dayTemplate as unknown as { outcome?: string }).outcome ?? "Daily outcome captured.", + plannedTasks, generationStatus: "seeded", + isTrialDay, + isPaidDay: !isTrialDay, + hasRecoveryPractice: false, }); } @@ -620,8 +695,48 @@ function recoveryTaskSeed(previousDayIndex: number, openedIncompleteCount: numbe ); } +function completedTaskCountForPlanDay( + sprintStartDate: string, + planDay: PlanDaySeed, + completionRows: Awaited>, +) { + return planDay.plannedTasks.filter((task) => ( + taskCompletedByTaskId( + taskIdFor(sprintStartDate, planDay.dayIndex, task.taskType, task.serviceId), + completionEventsForService(task.serviceId), + completionRows, + ) + )).length; +} + +function trialRecoveryPlanDay( + variantId: CuratorIcpId, + current: PlanDaySeed, + missedDayIndex: number, +): PlanDaySeed { + const recovery = trialRecoveryFor(variantId, missedDayIndex); + const recoveryTask = makeRecoveryPracticeTask( + trialRecoveryTaskSeed(variantId, missedDayIndex), + "2-Day Trial Recovery", + `Missed Day ${missedDayIndex} recovery: ${recovery.outcome}`, + ); + return { + ...current, + focus: recovery.focus, + outcome: recovery.outcome, + plannedTasks: [ + recoveryTask, + ...current.plannedTasks.filter((task) => !task.isRecoveryPractice).slice(0, 3), + ], + generationStatus: "adapted", + adaptationReason: `trial day ${missedDayIndex} missed; assigned ${recovery.serviceId} practice recovery`, + hasRecoveryPractice: true, + }; +} + function adaptCurrentDayPlan( sprintStartDate: string, + variantId: CuratorIcpId, focusDayIndex: number, planDays: PlanDaySeed[], completionRows: Awaited>, @@ -633,10 +748,21 @@ function adaptCurrentDayPlan( if (!current) return plan; // Static curator mode: the day-wise task list comes from the task registry. + // Only the explicit 2-day trial recovery rule can modify the active day. + if (focusDayIndex === 2 || focusDayIndex === 3) { + const missedDayIndex = focusDayIndex - 1; + const previous = plan[missedDayIndex - 1]; + if (previous?.isTrialDay && completedTaskCountForPlanDay(sprintStartDate, previous, completionRows) === 0) { + plan[focusDayIndex - 1] = trialRecoveryPlanDay(variantId, current, missedDayIndex); + return plan; + } + } + // Keep the historical adaptive logic below commented out for reference, but - // do not let analytics, recovery, or agent decisions rewrite the static sprint. + // do not let analytics or agent decisions rewrite the static sprint. current.generationStatus = "seeded"; current.adaptationReason = undefined; + current.hasRecoveryPractice = false; plan[focusDayIndex - 1] = current; return plan; @@ -756,13 +882,20 @@ function buildTask( weekIndex: number, weekTheme: string, weekSummary: string, - seedTask: TaskSeed, + seedTask: PlannedTask, completionRows: Awaited>, recentRows: Awaited>, focusDate: string, targetRole?: string, ): CuratorTask { - seedTask = normalizeActiveCuratorTaskSeed(seedTask); + seedTask = { + ...normalizeActiveCuratorTaskSeed(seedTask), + isTrialTask: seedTask.isTrialTask, + isPaidTask: seedTask.isPaidTask, + isRecoveryPractice: seedTask.isRecoveryPractice, + weekTheme: seedTask.weekTheme, + weekSummary: seedTask.weekSummary, + }; const dayOfWeek = dayIndexInWeek(sprintStartDate, dayIndex); const id = taskIdFor(sprintStartDate, dayIndex, seedTask.taskType, seedTask.serviceId); const missionInstanceId = sprintIdFor(sprintStartDate); @@ -805,6 +938,9 @@ function buildTask( signals: seedTask.signals, completionEvents: completionEventsForService(seedTask.serviceId), source: "service-registry", + isTrialTask: Boolean(seedTask.isTrialTask), + isPaidTask: seedTask.isPaidTask !== false, + isRecoveryPractice: Boolean(seedTask.isRecoveryPractice), }; const taskWithRoute = { ...task, @@ -850,6 +986,7 @@ export async function buildCuratorTasks(userId: string, date = todayIso()): Prom const userContext = await buildCuratorUserContext(userId); const adaptedPlanDays = adaptCurrentDayPlan( sprintStartDate, + sprintState.variantId, dayIndex, sprintState.planDays, completionRows, @@ -935,6 +1072,7 @@ async function buildCuratorSprintInternal(userId: string, focusDate = todayIso() ? sprintState.planDays : adaptCurrentDayPlan( sprintStartDate, + sprintState.variantId, activeDayIndex, sprintState.planDays, completionRows, @@ -964,6 +1102,9 @@ async function buildCuratorSprintInternal(userId: string, focusDate = todayIso() plannedServices: planDay.plannedTasks.map((task) => task.serviceId), generationStatus: planDay.generationStatus, adaptationReason: planDay.adaptationReason, + isTrialDay: Boolean(planDay.isTrialDay), + isPaidDay: planDay.isPaidDay !== false, + hasRecoveryPractice: Boolean(planDay.hasRecoveryPractice || planDay.plannedTasks.some((task) => task.isRecoveryPractice)), completedCount, totalCount: planDay.plannedTasks.length, unlockState, diff --git a/src/v1/curator/curator-types.ts b/src/v1/curator/curator-types.ts index 517787b..cc8f902 100644 --- a/src/v1/curator/curator-types.ts +++ b/src/v1/curator/curator-types.ts @@ -61,6 +61,9 @@ export const curatorTaskSchema = z.object({ signals: z.array(z.string()), completionEvents: z.array(z.string()), source: z.enum(["curator-v1", "mission-registry", "service-registry"]), + isTrialTask: z.boolean().default(false), + isPaidTask: z.boolean().default(true), + isRecoveryPractice: z.boolean().default(false), }); export const curatorStreakSchema = z.object({ @@ -81,6 +84,9 @@ export const curatorPlanDaySchema = z.object({ plannedServices: z.array(curatorServiceIdSchema).max(4).default([]), generationStatus: z.enum(["seeded", "generated", "adapted"]).default("seeded"), adaptationReason: z.string().optional(), + isTrialDay: z.boolean().default(false), + isPaidDay: z.boolean().default(true), + hasRecoveryPractice: z.boolean().default(false), completedCount: z.number().int().min(0), totalCount: z.number().int().min(0), unlockState: z.enum(["completed", "active", "upcoming"]), diff --git a/src/v1/curator/task-registry.ts b/src/v1/curator/task-registry.ts index 2615a2c..ddcb664 100644 --- a/src/v1/curator/task-registry.ts +++ b/src/v1/curator/task-registry.ts @@ -34,6 +34,28 @@ export type CuratorIcpTemplateSet = { weeks: WeekTemplate[]; }; +export type TrialDayTemplate = { + focus: string; + outcome: string; + socialTitle: string; + socialTask: string; + measurementTitle: string; + measurementTask: string; + proofTitle: string; + proofTask: string; + practiceTitle: string; + practiceTask: string; + practiceServiceId: Extract; +}; + +export type RecoveryPracticeTemplate = { + focus: string; + outcome: string; + title: string; + subtitle: string; + serviceId: Extract; +}; + function unavailableCta(serviceId: CuratorServiceId, fallback: string) { if ( serviceId === "social-branding-service" || @@ -114,6 +136,68 @@ export const CURATOR_TASK_REGISTRY: Record ]), }; +export const CURATOR_TRIAL_TASK_REGISTRY: Record = { + student_recent_grad: [ + trialDay("Launch Your Journey", "Professional profile created and career readiness baseline established.", "Complete GrowQR Profile", "Complete GrowQR profile and career interests.", "Career Baseline", "Complete Career Assessment and view QX Score.", "First AI Resume", "Create your first AI resume.", "Self-Introduction", "Record a 60-second self-introduction.", "interview-service"), + trialDay("Step Into Opportunity", "Ready to apply for internships with improved confidence.", "Add First Proof", "Add one project or certification.", "Review Progress", "Review Dashboard improvements.", "Internship Cover Letter", "Generate a cover letter for an internship.", "Group Discussion", "Practice a group discussion roleplay.", "roleplay-service"), + ], + intern: [ + trialDay("Build Career Momentum", "Stronger professional positioning and career baseline.", "Internship Profile", "Update internship achievements and LinkedIn headline.", "Career Baseline", "Complete Career Assessment and QX Score.", "Target Role Resume", "Optimize resume for your target role.", "HR Interview", "Complete an HR interview simulation.", "interview-service"), + trialDay("Turn Experience Into Opportunity", "Better interview readiness and stronger recruiter profile.", "Publish Achievement", "Publish one internship or project achievement.", "Review Progress", "Review Dashboard progress.", "Role-Specific Resume", "Create a role-specific resume version.", "Manager Conversation", "Practice a workplace manager conversation.", "roleplay-service"), + ], + fresher_early_professional: [ + trialDay("Get Recruiter Ready", "Resume optimized and recruiter visibility improved.", "Recruiter Profile", "Complete professional profile and LinkedIn summary.", "QX + ATS Baseline", "Complete QX Score and ATS Assessment.", "ATS-Ready Resume", "Generate an ATS-ready resume.", "Recruiter Screening", "Complete a recruiter screening interview.", "interview-service"), + trialDay("Apply With Confidence", "Applications submitted with greater confidence.", "Career Milestone", "Publish a career milestone.", "Review Analytics", "Review Dashboard analytics.", "Personalized Cover Letter", "Generate a personalized cover letter.", "Behavioural Interview", "Practice a behavioural interview.", "roleplay-service"), + ], + experienced_professional: [ + trialDay("Lead Your Next Chapter", "Executive brand established.", "Executive Profile", "Complete executive profile and leadership summary.", "Executive Baseline", "Complete Leadership Assessment and Executive QX Score.", "Executive Resume", "Create an executive resume.", "Executive Introduction", "Practice an executive introduction.", "interview-service"), + trialDay("Influence With Impact", "Leadership readiness strengthened.", "Leadership Insight", "Publish a leadership insight.", "Executive Dashboard", "Review Executive Dashboard.", "Executive Portfolio", "Build an executive portfolio.", "Boardroom Simulation", "Practice a boardroom simulation.", "roleplay-service"), + ], + freelancer: [ + trialDay("Win Your First Client", "Professional freelance identity established.", "Freelancer Profile", "Complete freelancer profile and service catalogue.", "Business Baseline", "Complete Business Readiness Assessment and QX Score.", "Portfolio Upload", "Upload portfolio and service offerings.", "Client Introduction", "Practice a client introduction.", "interview-service"), + trialDay("Build Client Trust", "Client-ready portfolio and stronger business confidence.", "Portfolio Proof", "Publish a portfolio item or testimonial.", "Business Dashboard", "Review Business Dashboard.", "Client Cover Letter", "Generate a client-ready cover letter.", "Client Negotiation", "Practice client negotiation roleplay.", "roleplay-service"), + ], + founder: [ + trialDay("Build Your Venture", "Founder readiness baseline established.", "Founder Profile", "Complete founder profile and startup overview.", "Founder Baseline", "Complete Founder Assessment and QX Score.", "Founder Portfolio", "Create founder portfolio or pitch summary.", "Investor Pitch", "Record an investor pitch.", "interview-service"), + trialDay("Pitch. Validate. Grow.", "Investor-ready profile with validated next steps.", "Startup Vision", "Publish startup vision.", "Founder Dashboard", "Review Founder Dashboard.", "Founder Assets", "Upload resume to update it in different formats.", "Customer Discovery", "Practice customer discovery roleplay.", "roleplay-service"), + ], + enterprise: [ + trialDay("Empower Your Workforce", "Workforce capability baseline established.", "Employer Profile", "Complete employer profile.", "Enterprise Baseline", "Complete Workforce Assessment and Enterprise QX Score.", "Competency Framework", "Upload competency framework.", "Leadership Calibration", "Practice leadership interview calibration.", "interview-service"), + trialDay("Lead Workforce Transformation", "AI-powered workforce capability roadmap initiated.", "Employer Branding", "Publish employer branding update.", "Enterprise Dashboard", "Review Enterprise Dashboard.", "Hiring Toolkit", "Generate hiring competency toolkit.", "Leadership Coaching", "Practice leadership coaching simulation.", "roleplay-service"), + ], +}; + +export const CURATOR_TRIAL_RECOVERY_REGISTRY: Record = { + student_recent_grad: [ + recovery("Own Your Introduction", "Improved confidence in introducing yourself during internships and campus interviews.", "Self-Introduction Interview", "Record a 60-second introduction covering your education, interests and career goals. AI provides instant communication feedback.", "interview-service"), + recovery("Team Player Mode", "Enhanced communication and collaboration readiness for placements.", "Group Discussion Roleplay", "Participate in a simulated campus group discussion, demonstrating teamwork and communication skills.", "roleplay-service"), + ], + intern: [ + recovery("Recruiter Ready", "Improved interview performance and stronger recruiter readiness.", "HR Screening Interview", "Complete a recruiter screening interview focused on internship experience, responsibilities and achievements.", "interview-service"), + recovery("Workplace Ready", "Greater confidence in workplace communication and professional interactions.", "Manager Conversation Roleplay", "Practice discussing work progress, deadlines and feedback with a reporting manager.", "roleplay-service"), + ], + fresher_early_professional: [ + recovery("Interview Recharge", "Better behavioural interview performance and higher callback readiness.", "Behavioural Interview", "Answer behavioural interview questions using structured STAR responses and receive AI feedback.", "interview-service"), + recovery("Think On Your Feet", "Improved problem-solving and collaboration skills.", "Workplace Collaboration Roleplay", "Simulate working with teammates to resolve a workplace challenge.", "roleplay-service"), + ], + experienced_professional: [ + recovery("Executive Presence", "Stronger executive interview readiness and leadership communication.", "Executive Leadership Interview", "Complete a leadership interview focused on strategic thinking, business impact and decision-making.", "interview-service"), + recovery("Lead The Room", "Increased executive presence and stakeholder management skills.", "Boardroom Strategy Roleplay", "Simulate presenting recommendations during a senior leadership meeting.", "roleplay-service"), + ], + freelancer: [ + recovery("Win The Client", "Improved client communication and proposal confidence.", "Client Discovery Interview", "Practice introducing your services, asking discovery questions and understanding client needs.", "interview-service"), + recovery("Handle The Objection", "Greater confidence in closing freelance opportunities.", "Client Negotiation Roleplay", "Simulate handling pricing objections and negotiating project scope professionally.", "roleplay-service"), + ], + founder: [ + recovery("Pitch With Purpose", "Refined startup pitch with stronger clarity and impact.", "Founder Pitch Interview", "Deliver a concise investor or customer pitch highlighting the business problem, solution and vision.", "interview-service"), + recovery("Validate Your Vision", "Better customer validation and product-market fit understanding.", "Customer Discovery Roleplay", "Practice validating customer problems through realistic founder-customer conversations.", "roleplay-service"), + ], + enterprise: [ + recovery("Hire Smarter", "Improved interview quality and standardized hiring practices.", "Structured Hiring Interview", "Conduct an AI-guided interview using competency-based questioning for hiring consistency.", "interview-service"), + recovery("Coach For Growth", "Enhanced leadership coaching and people management capability.", "Leadership Coaching Roleplay", "Simulate a performance coaching conversation with an employee to improve leadership effectiveness.", "roleplay-service"), + ], +}; + export function seed( taskType: CuratorTaskType, serviceId: CuratorServiceId, @@ -180,6 +264,44 @@ function day( }; } +function trialDay( + focus: string, + outcome: string, + socialTitle: string, + socialTask: string, + measurementTitle: string, + measurementTask: string, + proofTitle: string, + proofTask: string, + practiceTitle: string, + practiceTask: string, + practiceServiceId: Extract, +): TrialDayTemplate { + return { + focus, + outcome, + socialTitle, + socialTask, + measurementTitle, + measurementTask, + proofTitle, + proofTask, + practiceTitle, + practiceTask, + practiceServiceId, + }; +} + +function recovery( + focus: string, + outcome: string, + title: string, + subtitle: string, + serviceId: Extract, +): RecoveryPracticeTemplate { + return { focus, outcome, title, subtitle, serviceId }; +} + type QxDimension = "IQ" | "EQ" | "SQ"; type QxKpiWeight = { @@ -421,3 +543,12 @@ export function normalizeActiveCuratorTaskSeed(task: TaskSeed): TaskSeed { export function templateSetFor(variantId: CuratorIcpId): CuratorIcpTemplateSet { return CURATOR_TASK_REGISTRY[variantId] ?? CURATOR_TASK_REGISTRY.fresher_early_professional; } + +export function trialDaysFor(variantId: CuratorIcpId) { + return CURATOR_TRIAL_TASK_REGISTRY[variantId] ?? CURATOR_TRIAL_TASK_REGISTRY.fresher_early_professional; +} + +export function trialRecoveryFor(variantId: CuratorIcpId, missedDayIndex: number) { + const index = missedDayIndex === 2 ? 1 : 0; + return (CURATOR_TRIAL_RECOVERY_REGISTRY[variantId] ?? CURATOR_TRIAL_RECOVERY_REGISTRY.fresher_early_professional)[index]; +}