fix(curator): gate profile and day progression

This commit is contained in:
-Puter
2026-07-15 15:36:15 +05:30
parent d6f583f238
commit e8c6665ca0
4 changed files with 73 additions and 10 deletions
@@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import { resolveProfileTaskPolicy } from "../src/v1/curator/streak-utils.js";
assert.deepEqual(
resolveProfileTaskPolicy({ resumeAvailable: true, linkedinAvailable: false }),
{ complete: false, serviceId: "social-branding-service" },
"a resume upload must not complete or reroute the LinkedIn profile task",
);
assert.deepEqual(
resolveProfileTaskPolicy({ resumeAvailable: false, linkedinAvailable: true }),
{ complete: true, serviceId: "social-branding-service" },
"an existing LinkedIn connection completes the task on the next sprint read",
);
assert.deepEqual(
resolveProfileTaskPolicy({ resumeAvailable: true, linkedinAvailable: true }),
{ complete: true, serviceId: "social-branding-service" },
"LinkedIn remains the completion signal when both profile assets exist",
);
console.log("curator profile-task policy regression: ok");
+15 -2
View File
@@ -5,8 +5,21 @@ import {
resolveAgedTrialReuse,
} from "../src/v1/curator/streak-utils.js";
assert.equal(curatorLogicalDate(new Date("2026-07-14T18:29:59.999Z")), "2026-07-14");
assert.equal(curatorLogicalDate(new Date("2026-07-14T18:30:00.000Z")), "2026-07-15");
assert.equal(
curatorLogicalDate(new Date("2026-07-15T03:29:59.999Z")),
"2026-07-14",
"the previous Curator day remains active until 09:00 Asia/Kolkata",
);
assert.equal(
curatorLogicalDate(new Date("2026-07-15T03:30:00.000Z")),
"2026-07-15",
"the next Curator day starts at 09:00 Asia/Kolkata",
);
assert.equal(
curatorLogicalDate(new Date("2026-07-15T02:30:00.000Z"), 8),
"2026-07-15",
"deployments may explicitly configure a different business-morning hour",
);
const reuse = resolveAgedTrialReuse({
existing: { startDate: "2026-07-14", version: "icp-v10-static", durationDays: 2, access: "trial" },
desired: { durationDays: 2, access: "trial" },
+7 -5
View File
@@ -34,7 +34,7 @@ import {
} from "./task-registry.js";
import { kpiDefinitionsForIcp } from "./kpi-registry.js";
import { outcomeDefinitionsForIcp } from "./outcome-report-registry.js";
import { activeCuratorDayIndex, addDaysIso, composeTrialDay2, computeStreakFromDays, curatorLogicalDate, resolveAgedTrialReuse, todayIso } from "./streak-utils.js";
import { activeCuratorDayIndex, addDaysIso, composeTrialDay2, computeStreakFromDays, curatorLogicalDate, resolveAgedTrialReuse, resolveProfileTaskPolicy, todayIso } from "./streak-utils.js";
// A streak day represents meaningful correlated activity, not necessarily a
// terminal artifact. Keep this intentionally broader than full task completion.
@@ -1224,14 +1224,16 @@ function buildTask(
isPaidTask: seedTask.isPaidTask !== false,
isRecoveryPractice: Boolean(seedTask.isRecoveryPractice),
};
const profileComplete = isProfileTask && (userContext.profile.resumeAvailable || userContext.profile.linkedinAvailable);
const routeTask = isProfileTask && userContext.profile.linkedinAvailable && !userContext.profile.resumeAvailable
? { ...task, serviceId: "resume-service" as const }
const profilePolicy = isProfileTask
? resolveProfileTaskPolicy(userContext.profile)
: undefined;
const routeTask = profilePolicy
? { ...task, serviceId: profilePolicy.serviceId }
: task;
const taskWithRoute = {
...task,
route: buildCuratorTaskDeepLink(routeTask, targetRole),
status: profileComplete ? "completed" as const : classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
status: profilePolicy?.complete ? "completed" as const : classifyTaskStatus({ task, focusDate, completionRows, recentRows }),
} satisfies CuratorTask;
return taskWithRoute;
}
+31 -3
View File
@@ -17,9 +17,37 @@ export function indiaTodayIso(date = new Date()): string {
}).format(date);
}
// Timestamps stay UTC; only the product-day boundary is interpreted for India.
export function curatorLogicalDate(date = new Date()): string {
return indiaTodayIso(date);
const CURATOR_BUSINESS_DAY_START_HOUR = 9;
// Timestamps stay UTC; the Curator product day rolls over at the next
// business morning in India rather than at calendar midnight.
export function curatorLogicalDate(date = new Date(), businessDayStartHour = CURATOR_BUSINESS_DAY_START_HOUR): string {
const indiaParts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Kolkata",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const part = (type: Intl.DateTimeFormatPartTypes) => indiaParts.find((item) => item.type === type)?.value ?? "";
const indiaDate = `${part("year")}-${part("month")}-${part("day")}`;
return Number(part("hour")) < businessDayStartHour
? addDaysIso(indiaDate, -1)
: indiaDate;
}
export type ProfileTaskPolicyInput = {
resumeAvailable: boolean;
linkedinAvailable: boolean;
};
export function resolveProfileTaskPolicy(input: ProfileTaskPolicyInput) {
void input.resumeAvailable;
return {
complete: input.linkedinAvailable,
serviceId: "social-branding-service" as const,
};
}
export function addDaysIso(startDate: string, days: number): string {