feat(rectification): add v9 skill and domain contracts
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user