import { profileForProduct, resolveProductProfile } from "../product/profile.ts"; import { canonicalFactLabel } from "./prompt-policy.ts"; import { factKey, invalidFactValueReason } from "./question-memory.ts"; import { sanitizeSourceAnchorSnapshot } from "./source-anchors.ts"; import { sanitizeToolResultContract } from "./tool-result-contract.ts"; import type { ActiveRun, KdActionCommit, KdContextEntry, KdFact, KdHarnessMode, KdPhase, KdQuestion, KdResumeSnapshot, KdSourceAnchorSnapshot, KdToolResultContract, KdWriteTransaction } from "./types.ts"; import { isKdHarnessMode, isKdPhase, phaseIncludedInRun } from "./types.ts"; import { normalizeWorkingSet } from "./working-set.ts"; import { sanitizeWriteTransaction } from "./write-transaction.ts"; export function hydrateRun(parsed: ActiveRun): ActiveRun | undefined { if (!parsed || typeof parsed !== "object") return undefined; if (typeof parsed.id !== "string" || !parsed.id.trim()) return undefined; if (typeof parsed.phase !== "string" || !isKdPhase(parsed.phase)) return undefined; parsed.id = parsed.id.trim(); parsed.artifacts = isPlainObject(parsed.artifacts) ? sanitizeArtifacts(parsed.artifacts) : {}; parsed.questions = Array.isArray(parsed.questions) ? parsed.questions.map(sanitizeQuestion).filter((question): question is KdQuestion => Boolean(question)) : []; parsed.facts = Array.isArray(parsed.facts) ? parsed.facts.map(sanitizeFact).filter((fact): fact is KdFact => Boolean(fact)) : []; parsed.contextEntries = Array.isArray(parsed.contextEntries) ? parsed.contextEntries.map(sanitizeContextEntry).filter((entry): entry is KdContextEntry => Boolean(entry)) : []; parsed.toolResults = Array.isArray(parsed.toolResults) ? parsed.toolResults.map(sanitizeToolResultContract).filter((result): result is KdToolResultContract => Boolean(result)) : []; parsed.actionCommits = Array.isArray(parsed.actionCommits) ? parsed.actionCommits.map(sanitizeActionCommit).filter((commit): commit is KdActionCommit => Boolean(commit)) : []; parsed.writeTransactions = Array.isArray(parsed.writeTransactions) ? parsed.writeTransactions.map(sanitizeWriteTransaction).filter((transaction): transaction is KdWriteTransaction => Boolean(transaction)) : []; parsed.sourceAnchors = Array.isArray(parsed.sourceAnchors) ? parsed.sourceAnchors.map(sanitizeSourceAnchorSnapshot).filter((snapshot): snapshot is KdSourceAnchorSnapshot => Boolean(snapshot)) : []; parsed.status = parsed.status === "paused" || parsed.status === "done" ? parsed.status : "active"; parsed.goal = typeof parsed.goal === "string" ? parsed.goal : undefined; parsed.version = typeof parsed.version === "string" ? parsed.version : undefined; parsed.createdAt = typeof parsed.createdAt === "string" ? parsed.createdAt : typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined; parsed.updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : parsed.createdAt; const product = typeof parsed.profile?.product === "string" ? parsed.profile.product : typeof parsed.product === "string" ? parsed.product : resolveProductProfile(undefined).product; parsed.profile = profileForProduct(product); parsed.product = parsed.profile.product; parsed.mode = sanitizeHarnessMode(parsed.mode, parsed.phase); parsed.riskAssessment = sanitizeRiskAssessment(parsed.riskAssessment); parsed.repair = sanitizeRepairState(parsed.repair); parsed.gate = sanitizeGate(parsed.gate); parsed.resumeSnapshot = sanitizeResumeSnapshot(parsed.resumeSnapshot); parsed.workingSet = normalizeWorkingSet(parsed.workingSet, parsed); return parsed; } export function normalizeQuestionOptions( options: Array<{ label: string; description?: string }> | undefined, choices: string[] | undefined, ): Array<{ label: string; description?: string }> | undefined { const raw: Array<{ label: string; description?: string }> | undefined = options?.length ? options : choices?.map((choice) => ({ label: choice, })); const normalized = raw ?.map((option) => ({ label: option.label.trim(), description: option.description?.trim() || undefined, })) .filter((option) => option.label); if (!normalized?.length) return undefined; const seen = new Set(); return normalized.filter((option) => { const key = option.label.toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; }); } function sanitizeHarnessMode(value: unknown, phase: KdPhase): KdHarnessMode { if (typeof value !== "string" || !isKdHarnessMode(value)) { throw new Error("Invalid run state: missing or invalid Harness mode"); } const mode = value; if (phaseIncludedInRun({ mode }, phase)) return mode; throw new Error(`Invalid run state: phase ${phase} is not included in Harness mode ${mode}`); } function sanitizeArtifacts(value: Record): Record { return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string")); } function sanitizeQuestion(value: unknown): KdQuestion | undefined { if (!isPlainObject(value)) return undefined; const phase = typeof value.phase === "string" && isKdPhase(value.phase) ? value.phase : undefined; const question = typeof value.question === "string" ? value.question.trim() : ""; if (!phase || !question) return undefined; const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : createQuestionId(1); const rawFactLabel = typeof value.factLabel === "string" && value.factLabel.trim() ? value.factLabel.trim() : undefined; const factLabel = rawFactLabel ? canonicalFactLabel(rawFactLabel) : undefined; const options = normalizeQuestionOptions(readQuestionOptions(value.options), readQuestionChoices(value.choices)); return { id, phase, question, reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined, contextSummary: typeof value.contextSummary === "string" && value.contextSummary.trim() ? value.contextSummary.trim() : undefined, sourceRefs: readStringList(value.sourceRefs), factLabel, proposedFactValue: typeof value.proposedFactValue === "string" && value.proposedFactValue.trim() ? value.proposedFactValue.trim() : undefined, options, choices: options?.map((option) => option.label), multiple: value.multiple === true, customAnswer: value.customAnswer !== false, blocking: value.blocking !== false, status: value.status === "answered" ? "answered" : "open", answer: typeof value.answer === "string" ? value.answer : undefined, createdAt: typeof value.createdAt === "string" ? value.createdAt : new Date().toISOString(), answeredAt: typeof value.answeredAt === "string" ? value.answeredAt : undefined, }; } function sanitizeFact(value: unknown): KdFact | undefined { if (!isPlainObject(value)) return undefined; const label = typeof value.label === "string" ? value.label.trim() : ""; const factValue = typeof value.value === "string" ? value.value.trim() : ""; if (!label || !factValue) return undefined; const canonicalLabel = canonicalFactLabel(label); if (!canonicalLabel) return undefined; if (invalidFactValueReason(factValue)) return undefined; const status = value.status === "superseded" || value.status === "rejected" ? value.status : "current"; const source = value.source === "manual" ? "manual" : "question"; const now = new Date().toISOString(); return { key: factKey(canonicalLabel), label: canonicalLabel, value: factValue, status, source, sourceQuestionId: typeof value.sourceQuestionId === "string" && value.sourceQuestionId.trim() ? value.sourceQuestionId.trim() : undefined, reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined, createdAt: typeof value.createdAt === "string" ? value.createdAt : now, updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : now, supersededBy: typeof value.supersededBy === "string" && value.supersededBy.trim() ? value.supersededBy.trim() : undefined, }; } function sanitizeContextEntry(value: unknown): KdContextEntry | undefined { if (!isPlainObject(value)) return undefined; const phase = typeof value.phase === "string" && isKdPhase(value.phase) ? value.phase : undefined; const text = typeof value.text === "string" ? value.text.trim() : ""; if (!phase || !text) return undefined; const kind = value.kind === "decision" || value.kind === "file-finding" || value.kind === "constraint" || value.kind === "requirement" || value.kind === "risk" || value.kind === "next-action" ? value.kind : "user-input"; return { id: typeof value.id === "string" && value.id.trim() ? value.id.trim() : createContextEntryId(1), phase, kind, text, sourceRefs: readStringList(value.sourceRefs), createdAt: typeof value.createdAt === "string" ? value.createdAt : new Date().toISOString(), }; } function sanitizeActionCommit(value: unknown): KdActionCommit | undefined { if (!isPlainObject(value)) return undefined; const id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : undefined; const phase = typeof value.phase === "string" && isKdPhase(value.phase) ? value.phase : undefined; const focus = typeof value.focus === "string" && value.focus.trim() ? value.focus.trim() : undefined; const intent = typeof value.intent === "string" && value.intent.trim() ? value.intent.trim() : undefined; const reason = typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined; const requiredAction = typeof value.requiredAction === "string" && value.requiredAction.trim() ? value.requiredAction.trim() : undefined; if (!id || !phase || !focus || !intent || !reason || !requiredAction) return undefined; return { id, phase, focus, intent, allowed: value.allowed === true, reason, requiredAction, source: value.source === "command" || value.source === "tool" || value.source === "system" ? value.source : "input", createdAt: typeof value.createdAt === "string" ? value.createdAt : new Date().toISOString(), }; } function sanitizeRiskAssessment(value: unknown): ActiveRun["riskAssessment"] { if (!isPlainObject(value)) return undefined; const level = value.level; if (level !== "low" && level !== "medium" && level !== "high") return undefined; return { level, reason: typeof value.reason === "string" ? value.reason : "", source: value.source === "verify" || value.source === "ship" ? value.source : "manual", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(), }; } function sanitizeRepairState(value: unknown): ActiveRun["repair"] { if (!isPlainObject(value)) return undefined; const attempts = typeof value.attempts === "number" && Number.isFinite(value.attempts) ? Math.max(0, Math.trunc(value.attempts)) : 0; const maxAttempts = typeof value.maxAttempts === "number" && Number.isFinite(value.maxAttempts) ? Math.max(1, Math.trunc(value.maxAttempts)) : 3; return { attempts, maxAttempts, lastFailureEvidence: typeof value.lastFailureEvidence === "string" ? value.lastFailureEvidence : undefined, lastFailureSignature: typeof value.lastFailureSignature === "string" ? value.lastFailureSignature : undefined, status: value.status === "repairing" || value.status === "blocked" ? value.status : "idle", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(), }; } function sanitizeGate(value: unknown): ActiveRun["gate"] { if (!isPlainObject(value)) return { passed: false, checkedAt: new Date().toISOString() }; return { passed: value.passed === true, reason: typeof value.reason === "string" ? value.reason : undefined, checkedAt: typeof value.checkedAt === "string" ? value.checkedAt : new Date().toISOString(), }; } function sanitizeResumeSnapshot(value: unknown): KdResumeSnapshot | undefined { if (!isPlainObject(value)) return undefined; const phase = typeof value.phase === "string" && isKdPhase(value.phase) ? value.phase : undefined; const contextSummary = typeof value.contextSummary === "string" ? value.contextSummary.trim() : ""; const nextAction = typeof value.nextAction === "string" ? value.nextAction.trim() : ""; if (!phase || !contextSummary || !nextAction) return undefined; return { phase, contextSummary, sourceRefs: readStringList(value.sourceRefs), gateReason: typeof value.gateReason === "string" && value.gateReason.trim() ? value.gateReason.trim() : undefined, nextAction, pendingQuestionId: typeof value.pendingQuestionId === "string" && value.pendingQuestionId.trim() ? value.pendingQuestionId.trim() : undefined, status: value.status === "answered" || value.status === "archived" ? value.status : "open", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(), }; } function readQuestionOptions(value: unknown): Array<{ label: string; description?: string }> | undefined { if (!Array.isArray(value)) return undefined; return value.flatMap((item): Array<{ label: string; description?: string }> => { if (!isPlainObject(item) || typeof item.label !== "string" || !item.label.trim()) return []; return [ { label: item.label.trim(), description: typeof item.description === "string" && item.description.trim() ? item.description.trim() : undefined, }, ]; }); } function readQuestionChoices(value: unknown): string[] | undefined { return readStringList(value); } function readStringList(value: unknown): string[] | undefined { if (!Array.isArray(value)) return undefined; const list = value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())).map((item) => item.trim()); return list.length ? list : undefined; } function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function createQuestionId(sequence: number): string { return `Q-${String(sequence).padStart(3, "0")}`; } function createContextEntryId(sequence: number): string { return `C-${String(sequence).padStart(3, "0")}`; }