From 750a6ab03b65a1b4b46b08fc8d6e353b52e64702 Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:30:00 +0530 Subject: [PATCH] puter fix: service registry and curator onboaridng data fix, home feed route fixes --- scripts/service-registry-acceptance.mjs | 3 +- scripts/service-registry-smoke.mjs | 1 - src/home/home-feed-agent.ts | 19 ++- src/home/home-feed.ts | 14 +- src/home/seed-demo-home.ts | 10 +- src/home/types.ts | 10 +- src/routes/logs.ts | 1 - src/services/service-registry.ts | 168 ++++++---------------- src/v1/curator/curator-onboarding-loop.ts | 32 ++++- src/v1/curator/curator-tools.ts | 7 +- src/v1/curator/curator-types.ts | 1 - 11 files changed, 107 insertions(+), 159 deletions(-) diff --git a/scripts/service-registry-acceptance.mjs b/scripts/service-registry-acceptance.mjs index b5c00b9..88b8146 100755 --- a/scripts/service-registry-acceptance.mjs +++ b/scripts/service-registry-acceptance.mjs @@ -15,10 +15,10 @@ const requiredServices = [ "courses-service", "assessment-service", "matchmaking-service", - "pathways-service", "resume-service", "cover-letter-service", "qscore-service", + "social-branding-service", ]; const registry = await import("../dist/services/service-registry.js"); @@ -95,7 +95,6 @@ for (const id of requiredServices) { } assert(registry.getService("jobs-service")?.id === "matchmaking-service", "matchmaking alias failed"); -assert(registry.getService("career-pathways-service")?.id === "pathways-service", "pathways alias failed"); assert(registry.getService("coverletter-service")?.id === "cover-letter-service", "cover-letter alias failed"); assert(registry.getService("q-score-service")?.id === "qscore-service", "qscore alias failed"); assert(registry.getService("social-service")?.id === "social-branding-service", "social alias failed"); diff --git a/scripts/service-registry-smoke.mjs b/scripts/service-registry-smoke.mjs index bc450e3..0cd986c 100644 --- a/scripts/service-registry-smoke.mjs +++ b/scripts/service-registry-smoke.mjs @@ -27,7 +27,6 @@ const requiredServices = [ "courses-service", "assessment-service", "matchmaking-service", - "pathways-service", "qscore-service", "social-branding-service", ]; diff --git a/src/home/home-feed-agent.ts b/src/home/home-feed-agent.ts index 99bc039..b064015 100644 --- a/src/home/home-feed-agent.ts +++ b/src/home/home-feed-agent.ts @@ -52,8 +52,8 @@ Every notification must point to one of these real dashboard routes: - /agents/roleplay for recruiter/manager/salary/stakeholder roleplay - /agents/qscore for Q Score/readiness explanations - /missions for mission progress, approvals, artifacts, next stages -- /social for LinkedIn/social branding -- /pathways for locked/coming-soon pathways +- /agents/social-branding for LinkedIn/social branding +- /agents/matchmaking for Scout/opportunity matching - /rewards for locked/coming-soon rewards - /suggestions for broad onboarding/profile suggestions Every notification object must include: @@ -65,11 +65,10 @@ Use urgency truthfully: now = needs immediate user action, today = useful today, function sanitizeHref(href: string, moduleId: HomeModuleId) { if (isAllowedNotificationHref(href)) return href; if (href.startsWith("/missions")) return "/missions/active"; - if (href.startsWith("/social")) return "/social"; - if (href.startsWith("/pathways")) return "/pathways"; + if (href.startsWith("/agents/social-branding")) return "/agents/social-branding"; + if (href.startsWith("/agents/matchmaking")) return "/agents/matchmaking"; if (href.startsWith("/rewards")) return "/rewards"; - if (href.startsWith("/productivity")) return "/productivity"; - return moduleId === "productivity" ? "/productivity" : `/${moduleId}`; + return moduleId === "productivity" ? "/agents" : `/${moduleId}`; } function stableId(prefix: string, index: number) { @@ -82,8 +81,8 @@ function sourceFromHref(href: string) { if (href.startsWith("/agents/roleplay")) return "roleplay"; if (href.startsWith("/agents/qscore")) return "qscore"; if (href.startsWith("/missions")) return "mission"; - if (href.startsWith("/social")) return "social"; - if (href.startsWith("/pathways")) return "pathways"; + if (href.startsWith("/agents/social-branding")) return "social"; + if (href.startsWith("/agents/matchmaking")) return "pathways"; if (href.startsWith("/rewards")) return "rewards"; return "system"; } @@ -115,8 +114,8 @@ function defaultHrefForSource(source: NonNullable & { moduleId: HomeModuleId; priority: number }; diff --git a/src/home/seed-demo-home.ts b/src/home/seed-demo-home.ts index 9044da4..06dc4c4 100644 --- a/src/home/seed-demo-home.ts +++ b/src/home/seed-demo-home.ts @@ -25,12 +25,12 @@ const demoNotifications: DemoNotification[] = [ { moduleId: "missions", title: "2 approvals pending", subtitle: "Resume v6 and Mock #4 feedback need your confirmation.", tag: "Action", urgency: "now", href: "/missions", source: "mission", priority: 113 }, { moduleId: "missions", title: "Final readiness unlock", subtitle: "Complete one roleplay recovery drill to generate the final checklist.", tag: "Next", urgency: "soon", href: "/missions", source: "mission", priority: 106 }, - { moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/social", source: "social", priority: 104 }, - { moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/social", source: "social", priority: 100 }, - { moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/social", source: "social", priority: 96 }, + { moduleId: "social", title: "LinkedIn headline v3 ready", subtitle: "Clearer target: Product Intern · FinTech · Growth Systems.", tag: "Ready", urgency: "today", href: "/agents/social-branding", source: "social", priority: 104 }, + { moduleId: "social", title: "Featured section has 3 proof pins", subtitle: "Use resume scan, mock review, and Q Score delta as credibility blocks.", tag: "Proof", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 100 }, + { moduleId: "social", title: "Banner options queued", subtitle: "Three calm orange/blue layouts are waiting for review.", tag: "Brand", urgency: "soon", href: "/agents/social-branding", source: "social", priority: 96 }, - { moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/pathways", source: "pathways", priority: 70 }, - { moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/pathways", source: "pathways", priority: 68 }, + { moduleId: "pathways", title: "Pathways stay locked for demo", subtitle: "Resume + interview data is enough; pathway service is not enabled yet.", tag: "Soon", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 70 }, + { moduleId: "pathways", title: "PM SaaS route predicted", subtitle: "Directional only until the pathways service is connected.", tag: "Preview", urgency: "calm", href: "/agents/matchmaking", source: "pathways", priority: 68 }, { moduleId: "productivity", title: "Resume ATS 86", subtitle: "Keyword relevance +9 after JD tailoring. Open Resume Builder.", tag: "Resume", urgency: "today", href: "/agents/resume", source: "resume", priority: 118 }, { moduleId: "productivity", title: "Interview review is ready", subtitle: "Overall 82 · storytelling and concise examples need one more drill.", tag: "Mock", urgency: "now", href: "/agents/interview", source: "interview", priority: 117 }, diff --git a/src/home/types.ts b/src/home/types.ts index 43c4115..dd0eb8d 100644 --- a/src/home/types.ts +++ b/src/home/types.ts @@ -42,8 +42,8 @@ export type HomeFeedResponse = { export const MODULE_META: Record> = { suggestions: { id: "suggestions", label: "Today's Queue", href: "/suggestions", accent: "orange" }, missions: { id: "missions", label: "Missions", href: "/missions", accent: "orange" }, - social: { id: "social", label: "Social Branding", href: "/social", accent: "blue" }, - pathways: { id: "pathways", label: "Pathways", href: "/pathways", accent: "teal" }, + social: { id: "social", label: "Social Branding", href: "/agents/social-branding", accent: "blue" }, + pathways: { id: "pathways", label: "Pathways", href: "/agents/matchmaking", accent: "teal" }, productivity: { id: "productivity", label: "Interview · Roleplay · Resume", href: "/agents", accent: "orange" }, rewards: { id: "rewards", label: "Rewards", href: "/rewards", accent: "amber" }, }; @@ -62,9 +62,9 @@ export const ALLOWED_NOTIFICATION_HREFS = new Set([ "/missions", "/missions/active", "/missions/available", - "/social", - "/pathways", - "/productivity", + "/agents/social-branding", + "/agents/matchmaking", + "/agents", "/rewards", "/agents/resume", "/agents/interview", diff --git a/src/routes/logs.ts b/src/routes/logs.ts index 3bc1e2a..6713124 100644 --- a/src/routes/logs.ts +++ b/src/routes/logs.ts @@ -10,7 +10,6 @@ const LOG_CONTAINERS = { interview: "interview-service-api-1", roleplay: "roleplay-service-api-1", social: "growqr_social_api", - pathways: "pathways-service-api-1", courses: "courses_service-api-1", assessment: "assessment-service-api-1", matchmaking: "matchmaking-service-api-1", diff --git a/src/services/service-registry.ts b/src/services/service-registry.ts index 3c0486d..2a67196 100644 --- a/src/services/service-registry.ts +++ b/src/services/service-registry.ts @@ -10,7 +10,6 @@ export type ServiceId = | "courses-service" | "assessment-service" | "matchmaking-service" - | "pathways-service" | "resume-service" | "cover-letter-service" | "qscore-service" @@ -155,30 +154,27 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { discovery: { - path: "/upskilling/interview", - aliases: ["/agents/interview"], + path: "/agents/interview", queryParams: ["fresh"], usage: "Entry screen for role selection and resume of in-progress interview work.", }, setup: { - path: "/upskilling/interview/setup", - aliases: ["/agents/interview/setup"], - queryParams: ["role", "type", "from_assignment"], + path: "/agents/interview/setup", + queryParams: ["role", "type", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Collects interview role, type, duration, persona, media mode, and personalization consent.", }, preview: { - path: "/upskilling/interview/preview", - aliases: ["/agents/interview/preview"], + path: "/agents/interview/preview", queryParams: ["role", "type", "persona", "duration", "difficulty", "media", "vip", "from_assignment", "personalize", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Curator default handoff. The page configures the session and opens the launch overlay.", }, feedback: { - path: "/upskilling/interview/feedback", - queryParams: ["sessionId"], + path: "/agents/interview/feedback", + queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Opens feedback/review for a completed or processing session.", }, session: { - path: "/v2/service-sessions/interview", + path: "/agents/interview/preview", queryParams: ["session_id", "goal", "role", "type"], usage: "Legacy service-session launcher used by service agent results.", }, @@ -244,30 +240,27 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { discovery: { - path: "/upskilling/roleplay", - aliases: ["/agents/roleplay"], + path: "/agents/roleplay", queryParams: ["fresh"], usage: "Entry screen for scenario discovery and resume of in-progress roleplay work.", }, setup: { - path: "/upskilling/roleplay/setup", - aliases: ["/agents/roleplay/setup"], - queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment"], + path: "/agents/roleplay/setup", + queryParams: ["scenario", "scenario_text", "scenario_name", "from_assignment", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Collects roleplay scenario details and stores configure parameters for the builder.", }, builder: { - path: "/upskilling/roleplay/builder", - aliases: ["/agents/roleplay/builder"], - queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief"], + path: "/agents/roleplay/builder", + queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "role", "persona", "duration", "mode", "brief", "vip", "from_assignment_id"], usage: "Curator default handoff for generating or resuming a roleplay plan.", }, feedback: { - path: "/upskilling/roleplay/feedback", - queryParams: ["sessionId"], + path: "/agents/roleplay/feedback", + queryParams: ["sessionId", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Opens feedback/review for a completed or processing session.", }, session: { - path: "/v2/service-sessions/roleplay", + path: "/agents/roleplay/builder", queryParams: ["session_id", "goal", "role", "type"], usage: "Legacy service-session launcher used by service agent results.", }, @@ -330,23 +323,22 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { workspace: { - path: "/opportunities/resume", - aliases: ["/agents/resume"], + path: "/agents/resume", queryParams: ["tab", "section", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Resume hub. Use tab=resumes by default and section to deep-link editor panels.", }, editor: { - path: "/opportunities/resume/:resumeId", + path: "/agents/resume/:resumeId", queryParams: ["section"], usage: "Resume editor for a known resume.", }, templates: { - path: "/opportunities/resume/templates", + path: "/agents/resume/templates", queryParams: [], usage: "Template gallery.", }, session: { - path: "/v2/service-sessions/resume", + path: "/agents/resume", queryParams: ["goal", "role"], usage: "Legacy service-session launcher used by service agent results.", }, @@ -404,17 +396,17 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { workspace: { - path: "/opportunities/resume", + path: "/agents/resume", queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Open with tab=cover-letters to land in the cover-letter list.", }, generator: { - path: "/opportunities/cover-letter", - queryParams: [], - usage: "Standalone generation entry point.", + path: "/agents/resume", + queryParams: ["tab", "source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], + usage: "Cover-letter generation currently lands in the shared resume workspace with tab=cover-letters.", }, editor: { - path: "/opportunities/resume/cover-letters/:coverLetterId", + path: "/agents/resume/cover-letters/:coverLetterId", queryParams: [], usage: "Cover-letter editor for a known coverLetterId.", }, @@ -444,7 +436,7 @@ const serviceRegistry: ServiceRecord[] = [ { id: "courses-service", label: "Courses", - description: "Create, list, and open upskilling courses.", + description: "Create, list, and open learning courses.", category: "learning", enabled: Boolean(config.coursesServiceUrl), featureId: "courses", @@ -466,9 +458,19 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { catalog: { - path: "/upskilling/course", + path: "/agents/courses", queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Course catalog and upskilling entry point.", + usage: "Course catalog and learning entry point.", + }, + assigned: { + path: "/agents/courses/assigned", + queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], + usage: "Assigned course rail.", + }, + lesson: { + path: "/agents/courses/assigned/:assignmentCode/lesson/:lessonId", + queryParams: [], + usage: "Assigned course lesson player.", }, }, usage: "Open catalog for general learning handoffs.", @@ -481,7 +483,7 @@ const serviceRegistry: ServiceRecord[] = [ }, toolName: "prepare_course_handoff", completionEvents: ["course.started", "course.completed"], - qscoreSignals: ["skills.learning", "readiness.upskilling"], + qscoreSignals: ["skills.learning", "readiness.course_progress"], usage: "Use for skill-gap tasks that should become learning work.", }, usageDocs: ["Call buildServiceLink('courses-service', 'catalog', state) for course handoffs."], @@ -511,7 +513,7 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { assessment: { - path: "/upskilling/assessment", + path: "/agents/assessments", queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Assessment landing and active assessment surface.", }, @@ -560,18 +562,12 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { jobs: { - path: "/opportunities/job-matching", - aliases: ["/pathways"], + path: "/agents/matchmaking", queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId", "query", "role", "location"], usage: "Job/opportunity matching dashboard.", }, - pathways: { - path: "/career-pathways", - queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Career pathways dashboard and pathway list.", - }, }, - usage: "Use jobs for immediate matching and pathways for career-plan handoffs.", + usage: "Use matchmaking for immediate opportunity matching and Scout-style recommendations.", }, curator: { defaultPage: "jobs", @@ -587,79 +583,9 @@ const serviceRegistry: ServiceRecord[] = [ }, usageDocs: [ "Call buildServiceLink('matchmaking-service', 'jobs', state) for job matching.", - "Use pathways-service for generated career pathway plans.", + "Use the canonical /agents/matchmaking page for Scout-style opportunity review.", ], }, - { - id: "pathways-service", - label: "Pathways", - description: "Generate, activate, and report on personalized career pathways.", - category: "opportunity", - enabled: Boolean(config.pathwaysServiceUrl), - featureId: "pathways", - promptModulePath: "agents/pathways.md", - aliases: ["career-pathways-service"], - backend: { - baseUrl: config.pathwaysServiceUrl, - publicUrl: config.pathwaysPublicUrl, - healthPath: "/api/v1/health", - endpoints: { - health: endpoint("GET", "/api/v1/health", "Readiness probe.", "Check service availability."), - state: endpoint("GET", "/api/state/:clerkId", "Reads pathway state.", "Hydrate pathway dashboard."), - profileIngest: endpoint("POST", "/api/v1/profiles/ingest", "Ingests a profile for pathway generation.", "Profile setup."), - questionnaire: endpoint("GET", "/api/v1/questionnaires/:userId", "Reads pathway questionnaire.", "Questionnaire resume."), - createQuestionnaire: endpoint("POST", "/api/v1/questionnaires", "Creates questionnaire answers.", "Pathway onboarding."), - generatePathway: endpoint("POST", "/api/v1/pathways/generate", "Generates a pathway.", "Career-plan generation."), - activatePathway: endpoint("POST", "/api/v1/pathways/:pathwayId/activate", "Activates a pathway.", "Commit chosen pathway."), - getPathway: endpoint("GET", "/api/v1/pathways/:pathwayId", "Reads pathway details.", "Pathway detail."), - weeklyPlan: endpoint("GET", "/api/v1/pathways/:pathwayId/weekly-plan", "Reads weekly plan.", "Planner UI."), - report: endpoint("GET", "/api/v1/pathways/:pathwayId/report", "Reads pathway report.", "Progress report."), - opportunities: endpoint("GET", "/api/v1/pathways/:pathwayId/opportunities", "Reads pathway opportunities.", "Pathway opportunity recommendations."), - a2aTask: endpoint("POST", "/a2a/tasks", "Runs pathway agent tasks.", "Agent/curator orchestrated work."), - }, - usage: "Use for generated pathway plans and recommendation context. The live container is currently healthchecked separately from matchmaking.", - }, - frontend: { - baseUrl: frontendBaseUrl, - pages: { - dashboard: { - path: "/career-pathways", - aliases: ["/pathways"], - queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Pathway list/dashboard.", - }, - generate: { - path: "/career-pathways/generate", - queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Pathway generation flow.", - }, - questionnaire: { - path: "/career-pathways/questionnaire", - queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Pathway questionnaire flow.", - }, - detail: { - path: "/career-pathways/dashboard/:pathwayId", - queryParams: [], - usage: "Pathway detail dashboard.", - }, - }, - usage: "Use dashboard for general pathway handoffs and generate/questionnaire for guided setup.", - }, - curator: { - defaultPage: "dashboard", - defaultActionLabel: "Open pathways", - actionLabels: { - start: "Build pathway", - review: "Review pathway", - }, - toolName: "prepare_pathways_handoff", - completionEvents: ["pathway.generated", "pathway.activated", "pathway.report_viewed"], - qscoreSignals: ["readiness.pathway", "market.plan"], - usage: "Use for strategic career pathway planning rather than immediate job matching.", - }, - usageDocs: ["Call buildServiceLink('pathways-service', 'dashboard', state) for pathway handoffs."], - }, { id: "qscore-service", label: "QScore", @@ -685,13 +611,12 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { dashboard: { - path: "/home", - aliases: ["/analytics"], + path: "/agents/qscore", queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], - usage: "Home dashboard with QScore panel.", + usage: "Dedicated Q Score service page with live readiness signals.", }, }, - usage: "Open home dashboard for QScore review until a dedicated analytics route exists.", + usage: "Open the dedicated Q Score service page for score review.", }, curator: { defaultPage: "dashboard", @@ -732,8 +657,7 @@ const serviceRegistry: ServiceRecord[] = [ baseUrl: frontendBaseUrl, pages: { profile: { - path: "/opportunities/social-media", - aliases: ["/social"], + path: "/agents/social-branding", queryParams: ["source", "missionInstanceId", "missionId", "stageId", "curatorTaskId"], usage: "Social/profile improvement page.", }, diff --git a/src/v1/curator/curator-onboarding-loop.ts b/src/v1/curator/curator-onboarding-loop.ts index 37a072f..fe9e946 100644 --- a/src/v1/curator/curator-onboarding-loop.ts +++ b/src/v1/curator/curator-onboarding-loop.ts @@ -1,4 +1,4 @@ -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, inArray } from "drizzle-orm"; import { db } from "../../db/client.js"; import { growEvents, growHomeNotifications, type GrowEventRow } from "../../db/schema.js"; import { asRecord, getString } from "../../events/envelope.js"; @@ -44,6 +44,17 @@ function parseCompletedAt(value: unknown): string | undefined { return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString(); } +function onboardingContextFromPayload(payload: Record) { + const preferences = asRecord(payload.preferences); + const onboarding = asRecord(payload.onboarding ?? preferences.onboarding); + if (!Object.keys(onboarding).length && !Object.keys(preferences).length) return undefined; + return { + onboarding, + preferences: Object.keys(preferences).length ? preferences : { onboarding }, + source: "grow_events", + }; +} + export function onboardingCompletedAtFromPreferences(preferences: Record | undefined) { const onboarding = asRecord(preferences?.onboarding); return parseCompletedAt(onboarding.completed_at ?? onboarding.completedAt); @@ -123,6 +134,22 @@ async function recordOnboardingContextSnapshot(input: { }, { userId: input.userId, source: input.source ?? "onboarding" }); } +async function findLatestOnboardingContext(userId: string) { + const [event] = await db + .select({ id: growEvents.id, payload: growEvents.payload, occurredAt: growEvents.occurredAt, source: growEvents.source, type: growEvents.type }) + .from(growEvents) + .where(and( + eq(growEvents.userId, userId), + inArray(growEvents.type, ["onboarding.snapshot.saved", "onboarding.completed", "user.onboarding.completed", "profile.onboarding.completed"]), + )) + .orderBy(desc(growEvents.occurredAt)) + .limit(1); + + if (!event?.payload) return undefined; + const context = onboardingContextFromPayload(event.payload); + return context ? { ...context, sourceEventId: event.id, sourceEventType: event.type, sourceEventSource: event.source } : undefined; +} + async function fetchUserServiceContext(userId: string): Promise | undefined> { const token = config.serviceToken || (config.nodeEnv !== "production" ? config.a2aAllowedKey : ""); if (!token) return undefined; @@ -228,7 +255,7 @@ export async function runCuratorOnboardingLoop(input: OnboardingLoopInput): Prom } const startDate = isoDateFrom(input.completedAt); - const context = input.context ?? await fetchUserServiceContext(userId); + const context = input.context ?? await findLatestOnboardingContext(userId) ?? await fetchUserServiceContext(userId); await recordOnboardingContextSnapshot({ userId, startDate, @@ -272,6 +299,7 @@ export async function runCuratorOnboardingLoopForEvent(event: GrowEventRow): Pro completedAt: onboardingCompletedAtFromEvent(event), sourceEventId: event.id, source: event.source, + context: onboardingContextFromPayload(event.payload ?? {}), }); } diff --git a/src/v1/curator/curator-tools.ts b/src/v1/curator/curator-tools.ts index 04c77c0..8e26853 100644 --- a/src/v1/curator/curator-tools.ts +++ b/src/v1/curator/curator-tools.ts @@ -4,6 +4,7 @@ import { eq, desc, and, inArray } from "drizzle-orm"; import { db } from "../../db/client.js"; import { growEvents, growQscoreLatest, growQscoreProjectionState } from "../../db/schema.js"; import { interviewService, resumeService, roleplayService } from "../../services/product-service-clients.js"; +import { listServices } from "../../services/service-registry.js"; import { createMissionAction, listMissionActions } from "../../missions/actions.js"; import { listActiveMissionsPg, listMessagesPg } from "../../grow/persistence.js"; import { buildCuratorStreak, buildCuratorTasks, listCuratorRegistryCapabilities } from "./curator-store.js"; @@ -276,9 +277,9 @@ export function buildCuratorTools(ctx: { userId: string; date: string; conversat execute: async ({ taskId }) => { const task = taskId ? await findTask(ctx.userId, taskId, ctx.date) : null; return { - routes: ["interview-service", "resume-service", "roleplay-service", "qscore-service"].map((serviceId) => ({ - serviceId, - route: serviceRoute({ serviceId: serviceId as any, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }), + routes: listServices().map((service) => ({ + serviceId: service.id, + route: serviceRoute({ serviceId: service.id, missionId: task?.missionId, missionInstanceId: task?.missionInstanceId, stageId: task?.stageId, taskId: task?.id }), })), }; }, diff --git a/src/v1/curator/curator-types.ts b/src/v1/curator/curator-types.ts index e3cac58..e6d5f46 100644 --- a/src/v1/curator/curator-types.ts +++ b/src/v1/curator/curator-types.ts @@ -10,7 +10,6 @@ export const curatorServiceIdSchema = z.enum([ "qscore-service", "social-branding-service", "matchmaking-service", - "pathways-service", ]); export type CuratorServiceId = z.infer;