153 lines
3.9 KiB
TypeScript
153 lines
3.9 KiB
TypeScript
/**
|
|
* V9 case open request/response contracts.
|
|
*
|
|
* The browser submits only an intent + idempotency key (+ exact sessionId for
|
|
* the session intent). The server owns profile normalization, baseline
|
|
* snapshot/fingerprint generation, candidate range derivation and every
|
|
* disposition decision.
|
|
*/
|
|
import { z } from "zod";
|
|
import {
|
|
isRectificationCaseStatus,
|
|
type RectificationCaseStatus,
|
|
} from "./case-status";
|
|
|
|
export const OPEN_RECTIFICATION_INTENTS = [
|
|
"homepage",
|
|
"session",
|
|
"new",
|
|
] as const;
|
|
|
|
export type OpenRectificationIntent =
|
|
(typeof OPEN_RECTIFICATION_INTENTS)[number];
|
|
|
|
export const openRectificationCaseRequestSchema = z.discriminatedUnion(
|
|
"intent",
|
|
[
|
|
z
|
|
.object({
|
|
intent: z.literal("homepage"),
|
|
requestId: z.string().uuid(),
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
intent: z.literal("session"),
|
|
requestId: z.string().uuid(),
|
|
sessionId: z.string().uuid(),
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
intent: z.literal("new"),
|
|
requestId: z.string().uuid(),
|
|
supersedeActive: z.literal(false).optional(),
|
|
})
|
|
.strict(),
|
|
],
|
|
);
|
|
|
|
export type OpenRectificationCaseRequest = z.infer<
|
|
typeof openRectificationCaseRequestSchema
|
|
>;
|
|
|
|
export const openRectificationDispositions = [
|
|
"created",
|
|
"resumed",
|
|
"readonly",
|
|
] as const;
|
|
|
|
export type OpenRectificationDisposition =
|
|
(typeof openRectificationDispositions)[number];
|
|
|
|
export type OpenRectificationCaseResponse = Readonly<{
|
|
disposition: OpenRectificationDisposition;
|
|
caseId: string;
|
|
sessionId: string;
|
|
status: RectificationCaseStatus;
|
|
shouldStartOpening: boolean;
|
|
skillVersion: string;
|
|
}>;
|
|
|
|
/** Entry-summary contract used by the homepage card. */
|
|
export type RectificationEntrySummary = Readonly<{
|
|
hasResumableCase: boolean;
|
|
hasTerminalCaseWithTime: boolean;
|
|
latestResumable: Readonly<{
|
|
caseId: string;
|
|
status: RectificationCaseStatus;
|
|
lastActivityAt: string;
|
|
}> | null;
|
|
latestTerminal: Readonly<{
|
|
caseId: string;
|
|
status: RectificationCaseStatus;
|
|
hasUsableTime: boolean;
|
|
}> | null;
|
|
}>;
|
|
|
|
export function parseOpenRectificationCaseRequest(
|
|
value: unknown,
|
|
): OpenRectificationCaseRequest | null {
|
|
const parsed = openRectificationCaseRequestSchema.safeParse(value);
|
|
return parsed.success ? parsed.data : null;
|
|
}
|
|
|
|
export function isOpenRectificationDisposition(
|
|
value: unknown,
|
|
): value is OpenRectificationDisposition {
|
|
return (
|
|
typeof value === "string" &&
|
|
(openRectificationDispositions as readonly string[]).includes(value)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* shouldStartOpening is server-owned: true only for a freshly created case
|
|
* that has never started (no turns). Resumed cases and readonly history must
|
|
* never auto-generate an opening.
|
|
*/
|
|
export function shouldStartOpening(
|
|
disposition: OpenRectificationDisposition,
|
|
turnCount: number,
|
|
): boolean {
|
|
if (disposition !== "created") return false;
|
|
return turnCount === 0;
|
|
}
|
|
|
|
export function openResponse(
|
|
value: unknown,
|
|
): OpenRectificationCaseResponse | null {
|
|
if (!value || typeof value !== "object") return null;
|
|
const row = value as Record<string, unknown>;
|
|
const caseId = typeof row.case_id === "string" ? row.case_id : "";
|
|
const sessionId = typeof row.session_id === "string" ? row.session_id : "";
|
|
const skillVersion =
|
|
typeof row.skill_version === "string" ? row.skill_version : "";
|
|
if (
|
|
!caseId ||
|
|
!sessionId ||
|
|
!skillVersion ||
|
|
!isOpenRectificationDisposition(row.disposition) ||
|
|
!isRectificationCaseStatus(row.status)
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
disposition: row.disposition,
|
|
caseId,
|
|
sessionId,
|
|
status: row.status,
|
|
shouldStartOpening: row.should_start_opening === true,
|
|
skillVersion,
|
|
};
|
|
}
|
|
|
|
/** Profile fields the server requires before a case may be opened. */
|
|
export const PROFILE_COMPLETENESS_REQUIREMENTS = [
|
|
"birth_date",
|
|
"latitude",
|
|
"longitude",
|
|
"timezone_offset",
|
|
"birth_time_source",
|
|
] as const;
|