Verify static curator handoff registry

This commit is contained in:
Sai-karthik
2026-07-02 13:18:49 +00:00
parent 837a36276a
commit 0394dfbca0
5 changed files with 189 additions and 54 deletions

View File

@@ -9,6 +9,7 @@
"test:onboarding": "tsx scripts/onboarding-ledger.test.ts",
"test:missions": "tsx scripts/mission-lifecycle.test.ts",
"test:passive-actions": "tsx scripts/passive-actions.test.ts",
"test:curator-static": "tsx scripts/curator-static-registry.test.ts",
"start": "node dist/index.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
"workflows:smoke": "tsx src/workflows/smoke-test.ts",

View File

@@ -0,0 +1,112 @@
import { buildServiceCurationPreview } from "../src/v1/curator/curator-store.js";
import type { CuratorIcpId } from "../src/v1/curator/curator-icp-playbooks.js";
const ICP_IDS: CuratorIcpId[] = [
"student_recent_grad",
"intern",
"fresher_early_professional",
"experienced_professional",
"freelancer",
"founder",
"enterprise",
];
const EXPECTED_SERVICES = new Set([
"courses-service",
"resume-service",
"interview-service",
"roleplay-service",
]);
const EXPECTED_ROUTE_PREFIX: Record<string, string> = {
"courses-service": "/agents/courses",
"resume-service": "/agents/resume",
"interview-service": "/agents/interview/preview",
"roleplay-service": "/agents/roleplay/builder",
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function routeFor(route: string) {
return new URL(route, "https://app.sai-onchain.me");
}
async function main() {
const startDate = "2026-07-02";
const failures: string[] = [];
for (const icpId of ICP_IDS) {
const preview = await buildServiceCurationPreview({
userId: `curator-static-registry-test-${icpId}`,
startDate,
icpId,
userContext: { targetRole: "Product Manager" },
});
try {
assert(preview.version === "icp-v7-static", `${icpId}: expected static plan version`);
assert(preview.idempotency.promptHash === "static-task-registry", `${icpId}: prompt hash should prove static registry mode`);
assert(preview.plan.days.length === 30, `${icpId}: expected 30 generated days`);
assert(preview.plan.calendarWeeks.length === 5, `${icpId}: expected 5 sprint-relative calendar weeks`);
const firstSevenFocus = preview.plan.days.slice(0, 7).map((day) => day.focus);
const secondSevenFocus = preview.plan.days.slice(7, 14).map((day) => day.focus);
assert(JSON.stringify(firstSevenFocus) === JSON.stringify(secondSevenFocus), `${icpId}: day-wise static sprint should repeat predictably after day 7`);
for (const day of preview.plan.days) {
assert(day.generationStatus === "seeded", `${icpId} day ${day.dayIndex}: generation status must stay seeded`);
assert(!day.adaptationReason, `${icpId} day ${day.dayIndex}: adaptation reason should be absent`);
assert(day.tasks.length === 4, `${icpId} day ${day.dayIndex}: expected exactly 4 task objects`);
const services = new Set(day.tasks.map((task) => task.serviceId));
for (const serviceId of EXPECTED_SERVICES) {
assert(services.has(serviceId), `${icpId} day ${day.dayIndex}: missing ${serviceId}`);
}
assert(services.size === 4, `${icpId} day ${day.dayIndex}: expected exactly four unique services`);
const taskIds = new Set(day.tasks.map((task) => task.id));
const stageIds = new Set(day.tasks.map((task) => task.stageId));
assert(taskIds.size === 4, `${icpId} day ${day.dayIndex}: task IDs must be unique`);
assert(stageIds.size === 4, `${icpId} day ${day.dayIndex}: stage IDs must be unique`);
for (const task of day.tasks) {
assert(task.missionId === "curator-sprint", `${icpId} ${task.id}: missionId mismatch`);
assert(task.missionInstanceId?.startsWith("curator-sprint:icp-v7-static:"), `${icpId} ${task.id}: missionInstanceId should use static version`);
assert(task.stageId?.includes(`${task.serviceId?.replace("-service", "")}`), `${icpId} ${task.id}: stageId should include service segment`);
assert(task.id.includes(`${task.serviceId?.replace("-service", "")}`), `${icpId} ${task.id}: taskId should include service segment`);
assert(task.actorName !== "Vera" && task.actorName !== "Kai" && task.actorName !== "Mira" && task.actorName !== "Lyra", `${icpId} ${task.id}: agent persona leaked into actorName`);
const url = routeFor(task.route);
const expectedPrefix = task.serviceId ? EXPECTED_ROUTE_PREFIX[task.serviceId] : undefined;
assert(expectedPrefix && url.pathname === expectedPrefix, `${icpId} ${task.id}: route ${url.pathname} does not match ${expectedPrefix}`);
assert(url.searchParams.get("source") === "curator-v1", `${icpId} ${task.id}: route missing curator source`);
assert(url.searchParams.get("curatorTaskId") === task.id, `${icpId} ${task.id}: route curatorTaskId mismatch`);
assert(url.searchParams.get("missionId") === task.missionId, `${icpId} ${task.id}: route missionId mismatch`);
assert(url.searchParams.get("missionInstanceId") === task.missionInstanceId, `${icpId} ${task.id}: route missionInstanceId mismatch`);
assert(url.searchParams.get("stageId") === task.stageId, `${icpId} ${task.id}: route stageId mismatch`);
if (task.serviceId === "interview-service" || task.serviceId === "roleplay-service") {
assert(url.searchParams.get("role") === "Product Manager", `${icpId} ${task.id}: practice route should preserve target role`);
assert(url.searchParams.get("duration") === "5", `${icpId} ${task.id}: practice route should carry duration`);
}
}
}
} catch (err) {
failures.push(err instanceof Error ? err.message : String(err));
}
}
if (failures.length) {
console.error(failures.join("\n"));
process.exit(1);
}
console.log(`curator-static-registry tests passed for ${ICP_IDS.length} ICPs, 30 days each, 4 service handoffs per day`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -25,7 +25,7 @@ function addDaysIsoLocal(startDate: string, days: number) {
}
function dateFromTaskId(taskId: string) {
const match = /:icp-v\d+:(\d{4}-\d{2}-\d{2}):day-(\d+):/.exec(taskId);
const match = /:icp-v[^:]+:(\d{4}-\d{2}-\d{2}):day-(\d+):/.exec(taskId);
if (!match) return undefined;
const startDate = match[1];
const daySegment = match[2];

View File

@@ -4,6 +4,7 @@ import { createClient, type Client } from "rivetkit/client";
import { requireUser, type AuthContext } from "../../auth/clerk.js";
import { config } from "../../config.js";
import type { Registry } from "../../actors/registry.js";
import { curatorService } from "./curator-actor.js";
let _client: Client<Registry> | null = null;
function getClient(): Client<Registry> {
@@ -25,7 +26,7 @@ const chatSchema = z.object({
const curationPreviewSchema = z.object({
startDate: z.string().optional(),
icpId: z.enum(["student_recent_grad", "intern", "fresher_early_professional", "experienced_professional"]).optional(),
icpId: z.enum(["student_recent_grad", "intern", "fresher_early_professional", "experienced_professional", "freelancer", "founder", "enterprise"]).optional(),
goals: z.array(z.string()).optional(),
userContext: z.record(z.unknown()).optional(),
});
@@ -67,7 +68,7 @@ export function v1CuratorRoutes() {
app.post("/curation/preview", async (c) => {
const userId = c.get("userId");
const body = curationPreviewSchema.parse(await c.req.json().catch(() => ({})));
return c.json(await getCuratorActor(userId).previewCuration({ userId, ...body }));
return c.json(await curatorService.previewCuration({ userId, ...body }));
});
app.post("/onboarding/run", async (c) => {

View File

@@ -70,6 +70,7 @@ type WeekTemplate = {
measurement: TaskSeed;
proof: TaskSeed;
practice: TaskSeed;
roleplay?: TaskSeed;
}>;
};
@@ -946,8 +947,10 @@ type StaticSprintDay = {
outcome: string;
resumeTitle: string;
resumeTask: string;
practiceTitle: string;
practiceTask: string;
interviewTitle: string;
interviewTask: string;
roleplayTitle: string;
roleplayTask: string;
learningTitle: string;
learningTask: string;
};
@@ -957,34 +960,39 @@ function day(
outcome: string,
resumeTitle: string,
resumeTask: string,
practiceTitle: string,
practiceTask: string,
learningTitle: string,
learningTask: string,
interviewTitle: string,
interviewTask: string,
roleplayTitleOrLearningTitle: string,
roleplayTaskOrLearningTask: string,
learningTitle?: string,
learningTask?: string,
): StaticSprintDay {
return { focus, outcome, resumeTitle, resumeTask, practiceTitle, practiceTask, learningTitle, learningTask };
}
function practiceServiceFor(title: string, task: string): CuratorServiceId {
const text = `${title} ${task}`.toLowerCase();
if (
text.includes("manager") ||
text.includes("client") ||
text.includes("conflict") ||
text.includes("negotiat") ||
text.includes("team") ||
text.includes("lead the room") ||
text.includes("high stakes") ||
text.includes("workplace") ||
text.includes("customer") ||
text.includes("stakeholder") ||
text.includes("boardroom") ||
text.includes("pricing") ||
text.includes("contract")
) {
return "roleplay-service";
if (learningTitle && learningTask) {
return {
focus,
outcome,
resumeTitle,
resumeTask,
interviewTitle,
interviewTask,
roleplayTitle: roleplayTitleOrLearningTitle,
roleplayTask: roleplayTaskOrLearningTask,
learningTitle,
learningTask,
};
}
return "interview-service";
return {
focus,
outcome,
resumeTitle,
resumeTask,
interviewTitle,
interviewTask,
roleplayTitle: "Manager Mode",
roleplayTask: "Practice one workplace conversation related to today's sprint focus.",
learningTitle: roleplayTitleOrLearningTitle,
learningTask: roleplayTaskOrLearningTask,
};
}
function staticWeekTemplate(days: StaticSprintDay[]): WeekTemplate {
@@ -992,7 +1000,6 @@ function staticWeekTemplate(days: StaticSprintDay[]): WeekTemplate {
theme: "Static 7-Day CareerSprint",
summary: "Static day-wise sprint tasks sourced from the GrowQR task library and 7-day sprint document.",
days: days.map((item) => {
const practiceService = practiceServiceFor(item.practiceTitle, item.practiceTask);
return {
measurement: seed(
"measurement",
@@ -1016,13 +1023,23 @@ function staticWeekTemplate(days: StaticSprintDay[]): WeekTemplate {
),
practice: seed(
"practice",
practiceService,
item.practiceTitle,
item.practiceTask,
"interview-service",
item.interviewTitle,
item.interviewTask,
"10 min",
practiceService === "roleplay-service" ? "+9 projected" : "+10 projected",
practiceService === "roleplay-service" ? "Open roleplay preview" : "Open interview preview",
["static practice", item.practiceTitle.toLowerCase()],
"+10 projected",
"Open interview preview",
["static interview", item.interviewTitle.toLowerCase()],
),
roleplay: seed(
"practice",
"roleplay-service",
item.roleplayTitle,
item.roleplayTask,
"10 min",
"+9 projected",
"Open roleplay preview",
["static roleplay", item.roleplayTitle.toLowerCase()],
),
};
}),
@@ -1136,12 +1153,14 @@ function sprintIdFor(startDate: string) {
return `curator-sprint:${CURATOR_PLAN_VERSION}:${startDate}`;
}
function taskIdFor(startDate: string, dayIndex: number, taskType: CuratorTaskType) {
return `${sprintIdFor(startDate)}:day-${dayIndex}:${taskType}`;
function taskIdFor(startDate: string, dayIndex: number, taskType: CuratorTaskType, serviceId?: CuratorServiceId) {
const serviceSegment = serviceId ? `:${serviceId.replace("-service", "")}` : "";
return `${sprintIdFor(startDate)}:day-${dayIndex}:${taskType}${serviceSegment}`;
}
function stageIdFor(dayIndex: number, weekIndex: number, taskType: CuratorTaskType) {
return `wk-${weekIndex}:day-${dayIndex}:${taskType}`;
function stageIdFor(dayIndex: number, weekIndex: number, taskType: CuratorTaskType, serviceId?: CuratorServiceId) {
const serviceSegment = serviceId ? `:${serviceId.replace("-service", "")}` : "";
return `wk-${weekIndex}:day-${dayIndex}:${taskType}${serviceSegment}`;
}
function dateFromIso(value: string) {
@@ -1311,11 +1330,13 @@ function fallbackCarryForwardWeek(templateSet: CuratorIcpTemplateSet): WeekTempl
measurement: seed("measurement", "qscore-service", "Review your 30-day movement", "Check which readiness signals moved most over the sprint and what still needs work.", "5 min", "+5 projected", "Review Q Score", ["30 day review", "signal movement"]),
proof: seed("proof", "resume-service", "Package your strongest proof into one asset", "Turn the best sprint outcome into a reusable bullet, story, or artifact.", "10 min", "+7 projected", "Open resume workspace", ["proof packaging", "story asset"]),
practice: seed("practice", "interview-service", "Run a final closeout interview rep", "Use one realistic question to prove the sprint improved your clarity and confidence.", "10 min", "+10 projected", "Open interview preview", ["closeout rep", "confidence"]),
roleplay: seed("practice", "roleplay-service", "Run a final conversation rep", "Use one realistic conversation to prove the sprint improved your confidence under pressure.", "10 min", "+9 projected", "Open roleplay preview", ["closeout roleplay", "confidence"]),
},
{
measurement: seed("measurement", "qscore-service", "Choose the next readiness driver", "Pick the one signal that should define the next 30-day push.", "5 min", "+5 projected", "Review Q Score", ["next sprint", "readiness driver"]),
proof: seed("proof", "social-branding-service", "Turn sprint movement into a visible update", "Package your strongest improvement into a public-safe profile or positioning update.", "10 min", "+6 projected", "Open social profile flow", ["visibility update", "carry forward"]),
practice: seed("practice", "matchmaking-service", "Review the next opportunities to act on", "Use the stronger profile and practice signals to shortlist the next roles or pathways.", "10 min", "+5 projected", "Open pathways", ["next opportunities", "carry forward"]),
roleplay: seed("practice", "roleplay-service", "Practice the next high-value conversation", "Rehearse the conversation most likely to unlock the next sprint move.", "10 min", "+9 projected", "Open roleplay preview", ["next conversation", "carry forward"]),
},
...Array.from({ length: 5 }, () => lastWeek.days[lastWeek.days.length - 1] ?? lastWeek.days[0]!),
],
@@ -1324,10 +1345,9 @@ function fallbackCarryForwardWeek(templateSet: CuratorIcpTemplateSet): WeekTempl
function planSeedsForVariant(templateSet: CuratorIcpTemplateSet, startDate: string): PlanDaySeed[] {
const carryForwardWeek = fallbackCarryForwardWeek(templateSet);
const weekTemplates = [
...templateSet.weeks,
...Array.from({ length: MAX_PLAN_WEEK_COUNT - templateSet.weeks.length }, () => carryForwardWeek),
];
const weekTemplates = Array.from({ length: MAX_PLAN_WEEK_COUNT }, (_, index) => (
templateSet.weeks[index % templateSet.weeks.length] ?? carryForwardWeek
));
const planDays: PlanDaySeed[] = [];
for (let index = 0; index < SPRINT_DURATION_DAYS; index += 1) {
@@ -1347,6 +1367,7 @@ function planSeedsForVariant(templateSet: CuratorIcpTemplateSet, startDate: stri
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 todays sprint focus.", "10 min", "+9 projected", "Open roleplay preview", ["static roleplay", "manager mode"]), weekTemplate.theme, weekTemplate.summary),
],
generationStatus: "seeded",
});
@@ -1696,16 +1717,16 @@ function adaptCurrentDayPlan(
return plan;
}
const previousCompleted = previous.plannedTasks.filter((task) => (
taskCompletedByTaskId(
taskIdFor(sprintStartDate, previous.dayIndex, task.taskType),
completionEventsForService(task.serviceId),
completionRows,
)
taskCompletedByTaskId(
taskIdFor(sprintStartDate, previous.dayIndex, task.taskType, task.serviceId),
completionEventsForService(task.serviceId),
completionRows,
)
)).length;
if (previousCompleted < 3) {
const previousClassifications = previous.plannedTasks.map((task) => {
const taskId = taskIdFor(sprintStartDate, previous.dayIndex, task.taskType);
const taskId = taskIdFor(sprintStartDate, previous.dayIndex, task.taskType, task.serviceId);
const taskRows = rowsForTask(taskId, recentRows);
return {
taskId,
@@ -1784,9 +1805,9 @@ function buildTask(
): CuratorTask {
seedTask = normalizeActiveCuratorTaskSeed(seedTask);
const dayOfWeek = dayIndexInWeek(sprintStartDate, dayIndex);
const id = taskIdFor(sprintStartDate, dayIndex, seedTask.taskType);
const id = taskIdFor(sprintStartDate, dayIndex, seedTask.taskType, seedTask.serviceId);
const missionInstanceId = sprintIdFor(sprintStartDate);
const stageId = stageIdFor(dayIndex, weekIndex, seedTask.taskType);
const stageId = stageIdFor(dayIndex, weekIndex, seedTask.taskType, seedTask.serviceId);
const task: CuratorTask = {
id,
date,