Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/open-request.ts
T

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;