import type { ClassifierContext, ClassifierQuestion, JsonObject, JsonValue } from "@earendil-works/pi-ai"; function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function json(value: unknown, depth = 0): value is JsonValue { if (depth > 64) return false; if (value === null || typeof value === "string" || typeof value === "boolean") return true; if (typeof value === "number") return Number.isFinite(value); if (Array.isArray(value)) return value.every((item) => json(item, depth + 1)); return record(value) && Object.values(value).every((item) => json(item, depth + 1)); } function state(value: unknown): value is JsonObject { return record(value) && json(value); } function strings(value: unknown): value is Record { return record(value) && Object.values(value).every((item) => typeof item === "string"); } /** Convert the unchanged public Jev question envelope to upstream's typed classifier context. */ export function toClassifierContext(payload: unknown): ClassifierContext | undefined { if (!record(payload) || !state(payload.state) || !record(payload.questions)) return undefined; const questions: Record = {}; for (const [id, question] of Object.entries(payload.questions)) { if (!record(question)) return undefined; const raw = question.instructions; const instructions = typeof raw === "string" ? raw : record(raw) && typeof raw.question === "string" ? raw.question + (typeof raw.focus === "string" ? `\nFocus: ${raw.focus}` : "") : undefined; if (instructions === undefined) return undefined; if (question.type === "choice" && strings(question.criteria)) { questions[id] = { type: "choice", instructions, criteria: question.criteria }; } else if (question.type === "score" && Array.isArray(question.criteria) && question.criteria.every((item): item is string => typeof item === "string")) { questions[id] = { type: "score", instructions, criteria: question.criteria }; } else if (question.type === "noul" || question.type === "bool") { const criteria = question.criteria; const yes = record(criteria) ? criteria.true : undefined; const no = record(criteria) ? criteria.false : undefined; if (criteria !== undefined && (!strings(criteria) || typeof yes !== "string" || typeof no !== "string")) return undefined; questions[id] = { type: "bool", instructions, criteria: typeof yes === "string" && typeof no === "string" ? { true: yes, false: no } : { true: "True", false: "False" } }; } else return undefined; } return { state: payload.state, questions }; } /** Deployed TokenIn accepts string state; preserve typed upstream context via onPayload. */ export function tokenInWirePayload(payload: unknown): unknown { if (!record(payload)) throw new Error("TokenIn classifier requires an object payload"); if (typeof payload.state === "string") return payload; if (!state(payload.state)) throw new Error("TokenIn classifier requires JSON state"); return { ...payload, state: JSON.stringify(payload.state) }; } export function fitsClassifierRequest(payload: unknown, maxBytes: number): boolean { try { // Include string-state escaping, not just the smaller in-memory JSON object. const wire = record(payload) && "state" in payload ? tokenInWirePayload(payload) : payload; const text = JSON.stringify(wire); return typeof text === "string" && Number.isFinite(maxBytes) && maxBytes > 0 && Buffer.byteLength(text, "utf8") <= Math.min(maxBytes, 64 * 1024); } catch { return false; } }