Implement ICP-driven curator sprint flow
This commit is contained in:
@@ -9,6 +9,7 @@ import { createMissionAction, listMissionActions } from "../../missions/actions.
|
||||
import { getActiveMissionPg, listActiveMissionsPg, listMissionSuggestionsPg } from "../../grow/persistence.js";
|
||||
import { listServiceCapabilities } from "../../workflows/service-capabilities.js";
|
||||
import { getSubAgentModules } from "../../lib/prompt-loader.js";
|
||||
import { buildMissionServiceRoute } from "../../services/service-registry.js";
|
||||
|
||||
const SYSTEM_PROMPT = `You are the GrowQR conversation agent.
|
||||
Keep answers concise, practical, and focused on the user's active mission.
|
||||
@@ -92,16 +93,7 @@ function serviceHref(input: {
|
||||
stageId?: string;
|
||||
goal?: string;
|
||||
}) {
|
||||
const params = new URLSearchParams({
|
||||
source: "mission",
|
||||
missionInstanceId: input.missionInstanceId,
|
||||
missionId: input.missionId,
|
||||
});
|
||||
if (input.stageId) params.set("stageId", input.stageId);
|
||||
if (input.goal) params.set("goal", input.goal);
|
||||
if (input.serviceId === "interview-service") return `/agents/interview/setup?${params.toString()}`;
|
||||
if (input.serviceId === "roleplay-service") return `/agents/roleplay/setup?${params.toString()}`;
|
||||
return `/agents/resume?${params.toString()}`;
|
||||
return buildMissionServiceRoute(input);
|
||||
}
|
||||
|
||||
function buildConversationTools(ctx: ConversationRuntimeContext = {}) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { config } from "../config.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { buildServiceSessionPath } from "./service-registry.js";
|
||||
|
||||
// Lightweight agent reference (works with both old AgentProfile and new SubAgentModule).
|
||||
export type ServiceAgentRef = {
|
||||
@@ -28,32 +29,17 @@ export function buildServiceSessionUrl(
|
||||
detail: Record<string, unknown> | undefined,
|
||||
goal?: string,
|
||||
): string | undefined {
|
||||
const base = config.workflowsDashboardUrl.replace(/\/$/, "");
|
||||
const sessionId = detail?.session_id ?? detail?.sessionId;
|
||||
const params = new URLSearchParams();
|
||||
if (sessionId && typeof sessionId === "string") params.set("session_id", sessionId);
|
||||
if (goal) params.set("goal", goal);
|
||||
|
||||
if (service === "interview-service") {
|
||||
if (!sessionId || typeof sessionId !== "string") return undefined;
|
||||
params.set("role", String(detail?.target_role ?? goal ?? "Interview practice"));
|
||||
params.set("type", String(detail?.interview_type ?? "behavioral"));
|
||||
return `${base}/v2/service-sessions/interview?${params.toString()}`;
|
||||
if (
|
||||
service !== "interview-service" &&
|
||||
service !== "roleplay-service" &&
|
||||
service !== "resume-service"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (service === "roleplay-service") {
|
||||
if (!sessionId || typeof sessionId !== "string") return undefined;
|
||||
params.set("role", String(detail?.target_role ?? goal ?? "Roleplay practice"));
|
||||
params.set("type", String(detail?.roleplay_type ?? "custom"));
|
||||
return `${base}/v2/service-sessions/roleplay?${params.toString()}`;
|
||||
}
|
||||
|
||||
if (service === "resume-service") {
|
||||
if (goal) params.set("role", goal);
|
||||
return `${base}/v2/service-sessions/resume${params.size ? `?${params.toString()}` : ""}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
const path = buildServiceSessionPath(service, detail, goal);
|
||||
if (!path) return undefined;
|
||||
return `${config.workflowsDashboardUrl.replace(/\/$/, "")}${path}`;
|
||||
}
|
||||
|
||||
function stableUuid(input: string): string {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import type { CuratorServiceId, CuratorTask } from "../v1/curator/curator-types.js";
|
||||
|
||||
type QueryValue = string | number | undefined | null;
|
||||
|
||||
type MissionServiceId = Extract<CuratorServiceId, "interview-service" | "roleplay-service" | "resume-service">;
|
||||
|
||||
type MissionRouteInput = {
|
||||
serviceId: MissionServiceId;
|
||||
missionInstanceId: string;
|
||||
missionId: string;
|
||||
stageId?: string;
|
||||
goal?: string;
|
||||
};
|
||||
|
||||
type CuratorRouteInput = {
|
||||
serviceId?: CuratorServiceId;
|
||||
missionInstanceId?: string;
|
||||
missionId?: string;
|
||||
stageId?: string;
|
||||
taskId?: string;
|
||||
targetRole?: string;
|
||||
durationMinutes?: number;
|
||||
difficulty?: string;
|
||||
personaId?: string;
|
||||
requestedMode?: string;
|
||||
roleplayBrief?: string;
|
||||
};
|
||||
|
||||
function appendQuery(
|
||||
pathname: string,
|
||||
params: Record<string, QueryValue>,
|
||||
) {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
search.set(key, String(value));
|
||||
}
|
||||
const query = search.toString();
|
||||
return query ? `${pathname}?${query}` : pathname;
|
||||
}
|
||||
|
||||
function getString(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function getSessionId(detail?: Record<string, unknown>) {
|
||||
return getString(detail?.session_id ?? detail?.sessionId ?? detail?.id);
|
||||
}
|
||||
|
||||
export function buildServiceSessionPath(
|
||||
serviceId: MissionServiceId,
|
||||
detail?: Record<string, unknown>,
|
||||
goal?: string,
|
||||
) {
|
||||
const sessionId = getSessionId(detail);
|
||||
|
||||
if (serviceId === "interview-service") {
|
||||
if (!sessionId) return undefined;
|
||||
return appendQuery("/v2/service-sessions/interview", {
|
||||
session_id: sessionId,
|
||||
goal,
|
||||
role: getString(detail?.target_role) ?? goal ?? "Interview practice",
|
||||
type: getString(detail?.interview_type) ?? "behavioral",
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceId === "roleplay-service") {
|
||||
if (!sessionId) return undefined;
|
||||
return appendQuery("/v2/service-sessions/roleplay", {
|
||||
session_id: sessionId,
|
||||
goal,
|
||||
role: getString(detail?.target_role) ?? goal ?? "Roleplay practice",
|
||||
type: getString(detail?.roleplay_type) ?? "custom",
|
||||
});
|
||||
}
|
||||
|
||||
return appendQuery("/v2/service-sessions/resume", {
|
||||
goal,
|
||||
role: goal,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildMissionServiceRoute(input: MissionRouteInput) {
|
||||
const baseParams = {
|
||||
source: "mission",
|
||||
missionInstanceId: input.missionInstanceId,
|
||||
missionId: input.missionId,
|
||||
stageId: input.stageId,
|
||||
goal: input.goal,
|
||||
};
|
||||
|
||||
if (input.serviceId === "interview-service") {
|
||||
return appendQuery("/agents/interview/setup", baseParams);
|
||||
}
|
||||
|
||||
if (input.serviceId === "roleplay-service") {
|
||||
return appendQuery("/agents/roleplay/setup", baseParams);
|
||||
}
|
||||
|
||||
return appendQuery("/agents/resume", baseParams);
|
||||
}
|
||||
|
||||
function curatorBaseParams(input: CuratorRouteInput) {
|
||||
return {
|
||||
source: "curator-v1",
|
||||
missionInstanceId: input.missionInstanceId,
|
||||
missionId: input.missionId,
|
||||
stageId: input.stageId,
|
||||
curatorTaskId: input.taskId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCuratorServiceRoute(input: CuratorRouteInput) {
|
||||
if (input.serviceId === "interview-service") {
|
||||
return appendQuery("/agents/interview/preview", {
|
||||
...curatorBaseParams(input),
|
||||
role: input.targetRole?.trim() || "Product Manager",
|
||||
type: "behavioral",
|
||||
persona: input.personaId ?? "payal",
|
||||
duration: input.durationMinutes ?? 5,
|
||||
difficulty: input.difficulty ?? "medium",
|
||||
media: input.requestedMode ?? "video",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.serviceId === "roleplay-service") {
|
||||
return appendQuery("/agents/roleplay/builder", {
|
||||
...curatorBaseParams(input),
|
||||
role: input.targetRole?.trim() || "Professional",
|
||||
persona: input.personaId ?? "emma",
|
||||
duration: input.durationMinutes ?? 5,
|
||||
mode: input.requestedMode ?? "video",
|
||||
brief: input.roleplayBrief,
|
||||
});
|
||||
}
|
||||
|
||||
if (input.serviceId === "resume-service") {
|
||||
return appendQuery("/agents/resume", curatorBaseParams(input));
|
||||
}
|
||||
if (input.serviceId === "qscore-service") {
|
||||
return appendQuery("/analytics", curatorBaseParams(input));
|
||||
}
|
||||
if (input.serviceId === "social-branding-service") {
|
||||
return appendQuery("/social", curatorBaseParams(input));
|
||||
}
|
||||
if (input.serviceId === "matchmaking-service") {
|
||||
return appendQuery("/pathways", curatorBaseParams(input));
|
||||
}
|
||||
|
||||
return input.missionInstanceId
|
||||
? appendQuery("/missions/active", { missionInstanceId: input.missionInstanceId })
|
||||
: "/missions/active";
|
||||
}
|
||||
|
||||
export function getServiceDisplayName(serviceId?: CuratorServiceId, fallback = "Mission planner") {
|
||||
if (serviceId === "interview-service") return "Interview service";
|
||||
if (serviceId === "roleplay-service") return "Roleplay service";
|
||||
if (serviceId === "resume-service") return "Resume service";
|
||||
if (serviceId === "qscore-service") return "Q Score service";
|
||||
if (serviceId === "social-branding-service") return "Social branding service";
|
||||
if (serviceId === "matchmaking-service") return "Pathways service";
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function getServiceToolName(serviceId?: CuratorServiceId) {
|
||||
if (serviceId === "interview-service") return "prepare_interview_preview";
|
||||
if (serviceId === "roleplay-service") return "prepare_roleplay_preview";
|
||||
if (serviceId === "resume-service") return "prepare_resume_upload";
|
||||
if (serviceId === "qscore-service") return "prepare_qscore_review";
|
||||
return "prepare_mission_step";
|
||||
}
|
||||
|
||||
export function getServiceCompletionEvents(serviceId?: CuratorServiceId) {
|
||||
if (serviceId === "interview-service") {
|
||||
return ["interview.configured", "interview.review_completed", "interview.completed"];
|
||||
}
|
||||
if (serviceId === "roleplay-service") {
|
||||
return ["roleplay.configured", "roleplay.review_completed", "roleplay.completed"];
|
||||
}
|
||||
if (serviceId === "resume-service") {
|
||||
return ["resume.analysis_completed", "resume.parsed", "resume.updated"];
|
||||
}
|
||||
if (serviceId === "qscore-service") {
|
||||
return ["qscore.updated", "qscore.signal_projected"];
|
||||
}
|
||||
return ["curator.task.completed"];
|
||||
}
|
||||
|
||||
export function getServiceActionLabel(task: CuratorTask) {
|
||||
if (task.serviceId === "interview-service") return "Open interview preview";
|
||||
if (task.serviceId === "roleplay-service") return "Open roleplay preview";
|
||||
if (task.serviceId === "resume-service") return "Open resume workspace";
|
||||
if (task.serviceId === "qscore-service") return "Review Q Score";
|
||||
return task.cta || "Open";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildCuratorPlan, buildCuratorStreak, buildCuratorTasks, todayIsoDate } from "./curator-store.js";
|
||||
import { curatorPlanSchema, type CuratorImprovementSignal } from "./curator-types.js";
|
||||
import { buildCuratorPlan, buildCuratorSprint, buildCuratorStreak, buildCuratorTasks, todayIsoDate } from "./curator-store.js";
|
||||
import { curatorPlanSchema, curatorSprintResponseSchema, type CuratorImprovementSignal } from "./curator-types.js";
|
||||
import { emitCuratorEvent } from "./curator-events.js";
|
||||
import { runCuratorChat } from "./curator-agent.js";
|
||||
import { prepareHandoffForTask } from "./curator-tools.js";
|
||||
@@ -19,20 +19,27 @@ export const curatorActor = {
|
||||
|
||||
async getToday(input: { userId: string; date?: string }) {
|
||||
const date = input.date ?? todayIsoDate();
|
||||
const plan = curatorPlanSchema.parse(await buildCuratorPlan(input.userId, { startDate: date, endDate: date }));
|
||||
const tasks = plan.days[0]?.tasks ?? await buildCuratorTasks(input.userId, date);
|
||||
const sprint = curatorSprintResponseSchema.parse(await buildCuratorSprint(input.userId, date));
|
||||
await emitCuratorEvent({ userId: input.userId, type: "curator.day.opened", payload: { date } });
|
||||
return {
|
||||
date,
|
||||
plan,
|
||||
tasks,
|
||||
streak: plan.streak,
|
||||
completedCount: tasks.filter((task) => task.status === "completed").length,
|
||||
totalCount: tasks.length,
|
||||
plan: sprint.plan,
|
||||
tasks: sprint.todayTasks,
|
||||
streak: sprint.streak,
|
||||
completedCount: sprint.completedCount,
|
||||
totalCount: sprint.totalCount,
|
||||
sprint,
|
||||
source: "curator-v1" as const,
|
||||
};
|
||||
},
|
||||
|
||||
async getSprint(input: { userId: string; date?: string }) {
|
||||
const date = input.date ?? todayIsoDate();
|
||||
const sprint = curatorSprintResponseSchema.parse(await buildCuratorSprint(input.userId, date));
|
||||
await emitCuratorEvent({ userId: input.userId, type: "curator.day.opened", payload: { date, sprintId: sprint.sprintId } });
|
||||
return sprint;
|
||||
},
|
||||
|
||||
async chat(input: { userId: string; conversationId?: string; date?: string; taskId?: string; subtaskIndex?: number; subtask?: string; messages: Array<{ role: "user" | "assistant"; content: string }> }) {
|
||||
return runCuratorChat(input);
|
||||
},
|
||||
@@ -78,18 +85,20 @@ export const curatorActor = {
|
||||
|
||||
async recordServiceImpact(input: { userId: string; eventId: string }) {
|
||||
const streak = await buildCuratorStreak(input.userId);
|
||||
return { matched: true, completedTasks: await buildCuratorTasks(input.userId, todayIsoDate()), streak };
|
||||
const sprint = await buildCuratorSprint(input.userId, todayIsoDate());
|
||||
return { matched: true, completedTasks: sprint.todayTasks, streak, sprint };
|
||||
},
|
||||
|
||||
async applyImprovementSignals(input: { userId: string; date: string; signals: CuratorImprovementSignal[] }) {
|
||||
for (const signal of input.signals) {
|
||||
await emitCuratorEvent({ userId: input.userId, type: "curator.improvement_signal.applied", payload: { signal } });
|
||||
}
|
||||
const plan = await buildCuratorPlan(input.userId, { startDate: input.date, endDate: input.date });
|
||||
return { applied: input.signals.length, plan };
|
||||
const sprint = await buildCuratorSprint(input.userId, input.date);
|
||||
return { applied: input.signals.length, plan: sprint.plan, sprint };
|
||||
},
|
||||
|
||||
async getState(input: { userId: string }) {
|
||||
return { tasks: await buildCuratorTasks(input.userId, todayIsoDate()), streak: await buildCuratorStreak(input.userId) };
|
||||
const sprint = await buildCuratorSprint(input.userId, todayIsoDate());
|
||||
return { tasks: sprint.todayTasks, streak: sprint.streak, sprint };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,6 +41,11 @@ export function v1CuratorRoutes() {
|
||||
return c.json(await curatorActor.getToday({ userId, date: c.req.query("date") }));
|
||||
});
|
||||
|
||||
app.get("/sprint", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
return c.json(await curatorActor.getSprint({ userId, date: c.req.query("date") }));
|
||||
});
|
||||
|
||||
app.post("/chat", async (c) => {
|
||||
const userId = c.get("userId");
|
||||
const body = chatSchema.parse(await c.req.json());
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
buildCuratorServiceRoute,
|
||||
getServiceActionLabel,
|
||||
getServiceCompletionEvents,
|
||||
getServiceDisplayName,
|
||||
getServiceToolName,
|
||||
} from "../../services/service-registry.js";
|
||||
import type { CuratorServiceId, CuratorTask } from "./curator-types.js";
|
||||
|
||||
type ServiceRouteInput = {
|
||||
@@ -14,75 +21,22 @@ type ServiceRouteInput = {
|
||||
roleplayBrief?: string;
|
||||
};
|
||||
|
||||
function baseParams(input: ServiceRouteInput) {
|
||||
const params = new URLSearchParams({ source: "curator-v1" });
|
||||
if (input.missionInstanceId) params.set("missionInstanceId", input.missionInstanceId);
|
||||
if (input.missionId) params.set("missionId", input.missionId);
|
||||
if (input.stageId) params.set("stageId", input.stageId);
|
||||
if (input.taskId) params.set("curatorTaskId", input.taskId);
|
||||
return params;
|
||||
}
|
||||
|
||||
export function serviceRoute(input: ServiceRouteInput) {
|
||||
if (input.serviceId === "interview-service") {
|
||||
const params = baseParams(input);
|
||||
params.set("role", input.targetRole?.trim() || "Product Manager");
|
||||
params.set("type", "behavioral");
|
||||
params.set("persona", input.personaId ?? "payal");
|
||||
params.set("duration", String(input.durationMinutes ?? 5));
|
||||
params.set("difficulty", input.difficulty ?? "medium");
|
||||
params.set("media", input.requestedMode ?? "video");
|
||||
return `/agents/interview/preview?${params.toString()}`;
|
||||
}
|
||||
|
||||
if (input.serviceId === "roleplay-service") {
|
||||
const params = baseParams(input);
|
||||
params.set("role", input.targetRole?.trim() || "Professional");
|
||||
params.set("persona", input.personaId ?? "emma");
|
||||
params.set("duration", String(input.durationMinutes ?? 5));
|
||||
params.set("mode", input.requestedMode ?? "video");
|
||||
if (input.roleplayBrief) params.set("brief", input.roleplayBrief);
|
||||
return `/agents/roleplay/builder?${params.toString()}`;
|
||||
}
|
||||
|
||||
const params = baseParams(input);
|
||||
if (input.serviceId === "resume-service") return `/agents/resume?${params.toString()}`;
|
||||
if (input.serviceId === "qscore-service") return `/analytics?${params.toString()}`;
|
||||
if (input.serviceId === "social-branding-service") return `/social?${params.toString()}`;
|
||||
if (input.serviceId === "matchmaking-service") return `/pathways?${params.toString()}`;
|
||||
return `/missions/active${input.missionInstanceId ? `?missionInstanceId=${encodeURIComponent(input.missionInstanceId)}` : ""}`;
|
||||
return buildCuratorServiceRoute(input);
|
||||
}
|
||||
|
||||
export function serviceName(serviceId?: CuratorServiceId, fallback = "Mission planner") {
|
||||
if (serviceId === "interview-service") return "Interview service";
|
||||
if (serviceId === "roleplay-service") return "Roleplay service";
|
||||
if (serviceId === "resume-service") return "Resume service";
|
||||
if (serviceId === "qscore-service") return "Q Score service";
|
||||
if (serviceId === "social-branding-service") return "Social branding service";
|
||||
if (serviceId === "matchmaking-service") return "Pathways service";
|
||||
return fallback;
|
||||
return getServiceDisplayName(serviceId, fallback);
|
||||
}
|
||||
|
||||
export function serviceToolName(serviceId?: CuratorServiceId) {
|
||||
if (serviceId === "interview-service") return "prepare_interview_preview";
|
||||
if (serviceId === "roleplay-service") return "prepare_roleplay_preview";
|
||||
if (serviceId === "resume-service") return "prepare_resume_upload";
|
||||
if (serviceId === "qscore-service") return "prepare_qscore_review";
|
||||
return "prepare_mission_step";
|
||||
return getServiceToolName(serviceId);
|
||||
}
|
||||
|
||||
export function completionEventsForService(serviceId?: CuratorServiceId) {
|
||||
if (serviceId === "interview-service") return ["interview.configured", "interview.review_completed", "interview.completed"];
|
||||
if (serviceId === "roleplay-service") return ["roleplay.configured", "roleplay.review_completed", "roleplay.completed"];
|
||||
if (serviceId === "resume-service") return ["resume.analysis_completed", "resume.parsed", "resume.updated"];
|
||||
if (serviceId === "qscore-service") return ["qscore.updated", "qscore.signal_projected"];
|
||||
return ["curator.task.completed"];
|
||||
return getServiceCompletionEvents(serviceId);
|
||||
}
|
||||
|
||||
export function actionLabel(task: CuratorTask) {
|
||||
if (task.serviceId === "interview-service") return "Open interview preview";
|
||||
if (task.serviceId === "roleplay-service") return "Open roleplay preview";
|
||||
if (task.serviceId === "resume-service") return "Open resume workspace";
|
||||
if (task.serviceId === "qscore-service") return "Review Q Score";
|
||||
return task.cta || "Open";
|
||||
return getServiceActionLabel(task);
|
||||
}
|
||||
|
||||
+1059
-414
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ export async function prepareHandoffForTask(
|
||||
});
|
||||
|
||||
let actionId: string | undefined;
|
||||
if (task.missionInstanceId) {
|
||||
if (task.missionInstanceId && task.missionId !== "curator-sprint") {
|
||||
const action = await createMissionAction({
|
||||
userId,
|
||||
missionInstanceId: task.missionInstanceId,
|
||||
|
||||
@@ -11,6 +11,9 @@ export const curatorServiceIdSchema = z.enum([
|
||||
|
||||
export type CuratorServiceId = z.infer<typeof curatorServiceIdSchema>;
|
||||
|
||||
export const curatorTaskTypeSchema = z.enum(["measurement", "proof", "practice"]);
|
||||
export type CuratorTaskType = z.infer<typeof curatorTaskTypeSchema>;
|
||||
|
||||
export const curatorTaskStatusSchema = z.enum([
|
||||
"ready",
|
||||
"started",
|
||||
@@ -19,9 +22,16 @@ export const curatorTaskStatusSchema = z.enum([
|
||||
"blocked",
|
||||
]);
|
||||
|
||||
export const curatorWeekLifecycleSchema = z.enum(["done", "active", "upcoming"]);
|
||||
export const curatorWeekPerformanceSchema = z.enum(["Missed", "Okayish", "Avg", "Excelling"]);
|
||||
|
||||
export const curatorTaskSchema = z.object({
|
||||
id: z.string(),
|
||||
date: z.string(),
|
||||
dayIndex: z.number().int().min(1).max(28),
|
||||
dayIndexInWeek: z.number().int().min(1).max(7),
|
||||
weekIndex: z.number().int().min(1).max(4),
|
||||
taskType: curatorTaskTypeSchema,
|
||||
title: z.string(),
|
||||
subtitle: z.string(),
|
||||
missionId: z.string(),
|
||||
@@ -39,7 +49,7 @@ export const curatorTaskSchema = z.object({
|
||||
cta: z.string(),
|
||||
context: z.array(z.object({ label: z.string(), value: z.string() })),
|
||||
contextNarrative: z.string(),
|
||||
subtasks: z.array(z.string()).min(1),
|
||||
subtasks: z.array(z.string()).length(3),
|
||||
signals: z.array(z.string()),
|
||||
completionEvents: z.array(z.string()),
|
||||
source: z.enum(["curator-v1", "mission-registry", "service-registry"]),
|
||||
@@ -51,6 +61,34 @@ export const curatorStreakSchema = z.object({
|
||||
lastCompletedDate: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const curatorPlanDaySchema = z.object({
|
||||
date: z.string(),
|
||||
dayIndex: z.number().int().min(1).max(28),
|
||||
dayIndexInWeek: z.number().int().min(1).max(7),
|
||||
weekIndex: z.number().int().min(1).max(4),
|
||||
weekTheme: z.string(),
|
||||
weekSummary: z.string(),
|
||||
completedCount: z.number().int().min(0),
|
||||
totalCount: z.number().int().min(0),
|
||||
unlockState: z.enum(["completed", "active", "upcoming"]),
|
||||
tasks: z.array(curatorTaskSchema),
|
||||
});
|
||||
|
||||
export const curatorWeekSchema = z.object({
|
||||
weekIndex: z.number().int().min(1).max(4),
|
||||
title: z.string(),
|
||||
theme: z.string(),
|
||||
summary: z.string(),
|
||||
startDayIndex: z.number().int().min(1).max(28),
|
||||
endDayIndex: z.number().int().min(1).max(28),
|
||||
lifecycle: curatorWeekLifecycleSchema,
|
||||
performance: curatorWeekPerformanceSchema,
|
||||
completedTaskCount: z.number().int().min(0),
|
||||
totalTaskCount: z.number().int().min(0),
|
||||
completionPercent: z.number().min(0).max(100),
|
||||
days: z.array(curatorPlanDaySchema).length(7),
|
||||
});
|
||||
|
||||
export const curatorPlanSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
@@ -58,12 +96,9 @@ export const curatorPlanSchema = z.object({
|
||||
endDate: z.string(),
|
||||
goals: z.array(z.string()),
|
||||
generatedAt: z.string(),
|
||||
days: z.array(z.object({
|
||||
date: z.string(),
|
||||
dayIndex: z.number().int().min(1),
|
||||
theme: z.string(),
|
||||
tasks: z.array(curatorTaskSchema),
|
||||
})),
|
||||
durationDays: z.literal(28),
|
||||
weeks: z.array(curatorWeekSchema).length(4),
|
||||
days: z.array(curatorPlanDaySchema).length(28),
|
||||
streak: curatorStreakSchema,
|
||||
source: z.literal("curator-v1"),
|
||||
});
|
||||
@@ -81,10 +116,29 @@ export const curatorImprovementSignalSchema = z.object({
|
||||
status: z.enum(["created", "applied", "skipped"]).default("created"),
|
||||
});
|
||||
|
||||
export const curatorSprintResponseSchema = z.object({
|
||||
date: z.string(),
|
||||
sprintId: z.string(),
|
||||
plan: curatorPlanSchema,
|
||||
activeWeek: curatorWeekSchema,
|
||||
activeWeekIndex: z.number().int().min(1).max(4),
|
||||
activeDay: curatorPlanDaySchema,
|
||||
activeDayIndex: z.number().int().min(1).max(28),
|
||||
todayTasks: z.array(curatorTaskSchema).length(3),
|
||||
streak: curatorStreakSchema,
|
||||
completedCount: z.number().int().min(0),
|
||||
totalCount: z.number().int().min(0),
|
||||
overallProgressPercent: z.number().min(0).max(100),
|
||||
source: z.literal("curator-v1"),
|
||||
});
|
||||
|
||||
export type CuratorTask = z.infer<typeof curatorTaskSchema>;
|
||||
export type CuratorPlanDay = z.infer<typeof curatorPlanDaySchema>;
|
||||
export type CuratorWeek = z.infer<typeof curatorWeekSchema>;
|
||||
export type CuratorPlan = z.infer<typeof curatorPlanSchema>;
|
||||
export type CuratorStreak = z.infer<typeof curatorStreakSchema>;
|
||||
export type CuratorImprovementSignal = z.infer<typeof curatorImprovementSignalSchema>;
|
||||
export type CuratorSprintResponse = z.infer<typeof curatorSprintResponseSchema>;
|
||||
|
||||
export type CuratorTodayResponse = {
|
||||
date: string;
|
||||
|
||||
Reference in New Issue
Block a user