/** * Wire → domain normalization for the conversation transcript read (LS7). * * Mirrors the LS6 backend DTO (`crates/app-tauri/src/dto.rs`): `TurnPageDto` * (camelCase `turns` / `hasMore` / `nextAnchor`), `TurnViewDto` * (`id` / `atMs` / `role` / `source` / `text` / `textLen`) and the serde-tagged * `TurnSourceDto` (`{ kind: "human" }` | `{ kind: "agent", agentId }`). The * `source` mapping is calqued on `workStateNormalization.ts`. */ import type { TurnPage, TurnRole, TurnSource, TurnView } from "@/domain"; type RecordLike = Record; function isRecord(value: unknown): value is RecordLike { return value !== null && typeof value === "object" && !Array.isArray(value); } function stringValue(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; } function numberValue(value: unknown, fallback = 0): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function arrayValue(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } /** Mirror of LS6 `TurnSourceDto` (serde tag `kind`, camelCase `agentId`). */ function normalizeSource(value: unknown): TurnSource { if (!isRecord(value)) return { kind: "human" }; if (value.kind === "agent") { return { kind: "agent", agentId: stringValue(value.agentId, "unknown") }; } return { kind: "human" }; } /** Mirror of the camelCase-serialized `TurnRole` (`prompt`/`response`/`toolActivity`). */ function normalizeRole(value: unknown): TurnRole { return value === "response" || value === "toolActivity" ? value : "prompt"; } function normalizeTurn(value: unknown, index: number): TurnView { const turn = isRecord(value) ? value : {}; const text = stringValue(turn.text); return { id: stringValue(turn.id, `turn-${index}`), atMs: numberValue(turn.atMs, index), role: normalizeRole(turn.role), source: normalizeSource(turn.source), text, textLen: numberValue(turn.textLen, text.length), }; } /** Maps a raw `TurnPageDto` payload to the domain {@link TurnPage}. */ export function normalizeTurnPage(value: unknown): TurnPage { const page = isRecord(value) ? value : {}; const nextAnchor = page.nextAnchor; return { turns: arrayValue(page.turns).map(normalizeTurn), hasMore: page.hasMore === true, ...(typeof nextAnchor === "string" ? { nextAnchor } : {}), }; }