Verify static curator handoff registry
This commit is contained in:
@@ -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",
|
||||
|
||||
112
scripts/curator-static-registry.test.ts
Normal file
112
scripts/curator-static-registry.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 today’s 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,
|
||||
|
||||
Reference in New Issue
Block a user