/** * V9 rectification tool service. * * Server-owned fact layer behind the ten `rectification-*` tools. Every tool * call authenticates through the Case row, validates ownership/status/skill * version, reads the durable evidence ledger, derives canonical fingerprints, * invokes the deterministic Python engine when evidence effectively changed, * persists a candidate snapshot or receipt, and returns a compact safe * projection. The model never supplies userId, birth data, candidate ranges, * event arrays, scores or permission decisions. */ import { createHash } from "node:crypto"; import { EVIDENCE_KINDS, type EvidenceKind, } from "./evidence-model"; import { isPublicRectificationMethod, isPublicRectificationPhase, isPublicRectificationTool, type PublicRectificationMethod, type PublicRectificationPhase, type PublicRectificationTool, } from "./public-receipt"; import { RECTIFICATION_SKILL_VERSION } from "./case-status"; import { asInferenceState, type InferenceTransitionSnapshot, } from "../core/compose-receipt"; import { INFERENCE_ALGORITHM_VERSION } from "../core/types"; import { decisionStateFingerprint } from "../core/decision-fingerprint"; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** * Minimal structural RPC client. SupabaseClient satisfies this; tests can * pass a fake with a single rpc() method. */ export type RectificationRpcClient = { rpc( fn: string, args: Record, ): PromiseLike<{ data: unknown; error: { message: string } | null }>; }; export type AccountingClient = RectificationRpcClient; export class RectificationToolServiceError extends Error { readonly code: string; constructor(code: string) { super(`Rectification tool service error: ${code}`); this.name = "RectificationToolServiceError"; this.code = code; } } function first(value: unknown): unknown { if (Array.isArray(value)) return value[0] ?? null; if (value && typeof value === "object" && "value" in value) { return (value as { value?: unknown }).value; } return value; } function rpc( accounting: AccountingClient, fn: string, args: Record, ): Promise { return Promise.resolve(accounting.rpc(fn, args)).then(({ data, error }) => { if (error) throw new RectificationToolServiceError(error.message); return first(data) as T; }); } export type ConversationFocusStatus = | "active" | "resolved" | "declined" | "skipped" | "superseded"; export type ConversationFocus = Readonly<{ id: string; caseId: string; questionId: string; intent: string; targetEvidenceId: string | null; targetDomain: string | null; targetKind: string | null; expectedAnswerSchema: Readonly>; status: ConversationFocusStatus; askedAt: string; resolvedAt: string | null; }>; export type CaseConversationSummary = Readonly<{ confirmedEvidenceSummary: readonly Readonly>[]; pendingRevisions: readonly Readonly>[]; activeFocus: ConversationFocus | null; declinedSkippedTopics: readonly Readonly>[]; candidateDivergenceSummary: Readonly> | null; missingEvidenceCategories: readonly string[]; lastResultPolicy: Readonly> | null; summaryVersion: number; updatedAt: string; }>; export type V9CaseDossier = Readonly<{ case: Readonly<{ caseId: string; sessionId: string; status: string; skillName: string; skillVersion: string; candidateRange: { start_time: string; end_time: string } | null; acceptedTime: string | null; confirmedTime: string | null; completedAt: string | null; closedReason: string | null; lastActivityAt: string; evidenceCount: number; turnCount: number; }>; turns: readonly Readonly<{ id: string; role: "user" | "assistant"; text: string | null; status: string; createdAt: string; }>[]; evidence: readonly Readonly<{ id: string; sourceTurnId: string; subject: string; eventKind: string; domain: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; status: string; supersedesEvidenceId: string | null; createdAt: string; dateSource?: string | null; dateReliability?: string | null; dateCorroboration?: string | null; dateConflictStatus?: string | null; }>[]; conversationSummary: CaseConversationSummary; latestResult: V9CandidateSnapshot | null; }>; export type RectificationCaseSkillIdentityStatus = Readonly<{ name: string; version: string; sha256: string | null; sourceCommit: string | null; status: "verified" | "legacy_unverifiable"; requiresSkillAdoption: boolean; }>; export type RectificationCaseSkillIdentity = Readonly<{ name: string; version: string; sha256: string; sourceCommit: string | null; }>; export async function loadV9CaseSkillIdentityStatus( accounting: AccountingClient, userId: string, caseId: string, ): Promise { const value = await rpc>( accounting, "get_agentic_rectification_skill_identity_status", { p_user_id: userId, p_case_id: caseId }, ); const name = rowText(value?.skill_name); const version = rowText(value?.skill_version); const sha256 = value?.skill_sha256 === null ? null : rowText(value?.skill_sha256); const sourceCommit = value?.skill_source_commit === null ? null : rowText(value?.skill_source_commit); const status = value?.skill_identity_status; if (!name || !version || (status !== "verified" && status !== "legacy_unverifiable") || (sha256 !== null && (!sha256 || !/^[0-9a-f]{64}$/.test(sha256))) || (sourceCommit !== null && (!sourceCommit || !/^[0-9a-f]{40}$/.test(sourceCommit)))) { throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity"); } if (status === "verified" && sha256 === null) { throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity"); } if (status === "legacy_unverifiable" && sha256 !== null) { throw new RectificationToolServiceError("agentic_rectification_invalid_skill_identity"); } return { name, version, sha256, sourceCommit, status, requiresSkillAdoption: value?.requires_skill_adoption === true, }; } export async function loadV9CaseSkillIdentity( accounting: AccountingClient, userId: string, caseId: string, ): Promise { let value: Record; try { value = await rpc>( accounting, "get_agentic_rectification_skill_identity", { p_user_id: userId, p_case_id: caseId }, ); } catch (error) { if (error instanceof RectificationToolServiceError && error.code.includes("agentic_rectification_legacy_skill_identity_unverifiable")) { throw error; } throw error; } const name = rowText(value?.skill_name); const version = rowText(value?.skill_version); const sha256 = rowText(value?.skill_sha256); const sourceCommit = rowText(value?.skill_source_commit); if (!name || !version || !sha256 || !/^[0-9a-f]{64}$/.test(sha256) || (sourceCommit !== null && !/^[0-9a-f]{40}$/.test(sourceCommit))) { throw new RectificationToolServiceError("agentic_rectification_skill_identity_missing"); } return { name, version, sha256, sourceCommit }; } export async function insertV9SkillRunReceipt( accounting: AccountingClient, userId: string, caseId: string, turnId: string, requestId: string, runKind: "turn" | "regeneration", identity: RectificationCaseSkillIdentity, ): Promise { await rpc( accounting, "insert_agentic_rectification_skill_run_receipt", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_request_id: requestId, p_run_kind: runKind, p_skill_name: identity.name, p_skill_version: identity.version, p_skill_sha256: identity.sha256, p_source_commit: identity.sourceCommit, }, ); } export type V9Candidate = Readonly<{ candidateId: string; time: string; rank: number; relativeSupport: number; tiedMinuteCount: number; }>; export type V9CandidateSnapshot = Readonly<{ resultId: string; candidates: readonly V9Candidate[]; overallConfidence: "low" | "medium" | "high"; selectionAllowed: boolean; confirmationAllowed: boolean; representativeTime: string | null; selectedTime: string | null; selectionKind: string | null; evidenceLedgerFingerprint: string | null; candidateRangeFingerprint: string | null; skillVersion: string | null; algorithmVersion: string | null; eventContractVersion: string | null; policyVersion: string | null; decisionReceipt: Readonly> | null; executionLedger: readonly Readonly>[] | null; createdAt: string; invalidatedAt: string | null; }>; function timeValue(value: unknown): string | null { const time = typeof value === "string" ? value.slice(0, 5) : ""; return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time) ? time : null; } function rowText(value: unknown): string | null { return typeof value === "string" ? value : null; } function rowNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function rowBoolean(value: unknown): boolean { return value === true; } function rowArray(value: unknown): unknown[] { return Array.isArray(value) ? value : []; } function rowObject(value: unknown): Readonly> | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Readonly> : null; } function parseConversationFocus(value: unknown): ConversationFocus | null { const row = rowObject(value); if (!row || typeof row.id !== "string" || typeof row.case_id !== "string" || typeof row.question_id !== "string" || typeof row.intent !== "string") return null; const status = row.status; if (status !== "active" && status !== "resolved" && status !== "declined" && status !== "skipped" && status !== "superseded") return null; return { id: row.id, caseId: row.case_id, questionId: row.question_id, intent: row.intent, targetEvidenceId: rowText(row.target_evidence_id), targetDomain: rowText(row.target_domain), targetKind: rowText(row.target_kind), expectedAnswerSchema: rowObject(row.expected_answer_schema) ?? {}, status, askedAt: String(row.asked_at ?? ""), resolvedAt: rowText(row.resolved_at), }; } function parseConversationSummary(value: unknown): CaseConversationSummary { const row = rowObject(value) ?? {}; return { confirmedEvidenceSummary: rowArray(row.confirmed_evidence_summary).flatMap((item) => { const parsed = rowObject(item); return parsed ? [parsed] : []; }), pendingRevisions: rowArray(row.pending_revisions).flatMap((item) => { const parsed = rowObject(item); return parsed ? [parsed] : []; }), activeFocus: parseConversationFocus(row.active_focus), declinedSkippedTopics: rowArray(row.declined_skipped_topics).flatMap((item) => { const parsed = rowObject(item); return parsed ? [parsed] : []; }), candidateDivergenceSummary: rowObject(row.candidate_divergence_summary), missingEvidenceCategories: rowArray(row.missing_evidence_categories) .filter((item): item is string => typeof item === "string"), lastResultPolicy: rowObject(row.last_result_policy), summaryVersion: rowNumber(row.summary_version) ?? 1, updatedAt: String(row.updated_at ?? ""), }; } export function parseV9CaseDossier(value: unknown): V9CaseDossier | null { if (!value || typeof value !== "object") return null; const root = value as Record; const caseRow = root.case && typeof root.case === "object" ? root.case as Record : null; if (!caseRow || typeof caseRow.case_id !== "string") return null; const range = caseRow.candidate_range && typeof caseRow.candidate_range === "object" ? caseRow.candidate_range as { start_time?: unknown; end_time?: unknown } : null; const candidateRange = range && typeof range.start_time === "string" && typeof range.end_time === "string" ? { start_time: range.start_time, end_time: range.end_time } : null; const turns = rowArray(root.turns).flatMap((item): V9CaseDossier["turns"] => { if (!item || typeof item !== "object") return []; const turn = item as Record; if (typeof turn.id !== "string") return []; return [{ id: turn.id, role: turn.role === "user" ? "user" : "assistant", text: rowText(turn.text), status: String(turn.status ?? ""), createdAt: String(turn.created_at ?? ""), }]; }); const evidence = rowArray(root.evidence).flatMap((item): V9CaseDossier["evidence"] => { if (!item || typeof item !== "object") return []; const row = item as Record; if (typeof row.id !== "string") return []; return [{ id: row.id, sourceTurnId: String(row.source_turn_id ?? ""), subject: String(row.subject ?? ""), eventKind: String(row.event_kind ?? ""), domain: String(row.domain ?? ""), occurredFrom: rowText(row.occurred_from), occurredTo: rowText(row.occurred_to), datePrecision: String(row.date_precision ?? ""), summary: String(row.summary ?? ""), status: String(row.status ?? ""), supersedesEvidenceId: rowText(row.supersedes_evidence_id), createdAt: String(row.created_at ?? ""), dateSource: rowText(row.date_source), dateReliability: rowText(row.date_reliability), dateCorroboration: rowText(row.date_corroboration), dateConflictStatus: rowText(row.date_conflict_status), }]; }); const latestResult = parseV9CandidateSnapshot(root.latest_result); const conversationSummary = parseConversationSummary(root.conversation_summary); return { case: { caseId: caseRow.case_id, sessionId: String(caseRow.session_id ?? ""), status: String(caseRow.status ?? ""), skillName: String(caseRow.skill_name ?? ""), skillVersion: String(caseRow.skill_version ?? ""), candidateRange, acceptedTime: timeValue(caseRow.accepted_time), confirmedTime: timeValue(caseRow.confirmed_time), completedAt: rowText(caseRow.completed_at), closedReason: rowText(caseRow.closed_reason), lastActivityAt: String(caseRow.last_activity_at ?? ""), evidenceCount: rowNumber(caseRow.evidence_count) ?? 0, turnCount: rowNumber(caseRow.turn_count) ?? 0, }, turns, evidence, conversationSummary, latestResult, }; } export function parseV9CandidateSnapshot(value: unknown): V9CandidateSnapshot | null { const row = rowObject(value); const resultId = rowText(row?.result_id); if (!row || !resultId || !Array.isArray(row.candidates) || row.candidates.length === 0) return null; const candidates: V9Candidate[] = []; const seenIds = new Set(); for (const item of row.candidates) { const candidate = rowObject(item); const candidateId = rowText(candidate?.candidate_id); const candidateTime = timeValue(candidate?.time); const rank = rowNumber(candidate?.rank); const relativeSupport = rowNumber(candidate?.relative_support); const tiedMinuteCount = rowNumber(candidate?.tied_minute_count); if ( !candidate || !candidateId || !uuidPattern.test(candidateId) || seenIds.has(candidateId) || !candidateTime || rank === null || !Number.isInteger(rank) || rank < 1 || relativeSupport === null || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100 || tiedMinuteCount === null || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1 ) return null; seenIds.add(candidateId); candidates.push({ candidateId, time: candidateTime, rank, relativeSupport, tiedMinuteCount }); } const decisionReceipt = rowObject(row.decision_receipt); const executionLedger = Array.isArray(row.execution_ledger) && row.execution_ledger.every((item) => rowObject(item) !== null) ? row.execution_ledger as readonly Readonly>[] : null; return { resultId, candidates, overallConfidence: row.overall_confidence === "high" || row.overall_confidence === "medium" ? row.overall_confidence : "low", selectionAllowed: rowBoolean(row.selection_allowed), confirmationAllowed: rowBoolean(row.confirmation_allowed), representativeTime: timeValue(row.representative_time), selectedTime: timeValue(row.selected_time), selectionKind: rowText(row.selection_kind), evidenceLedgerFingerprint: rowText(row.evidence_ledger_fingerprint), candidateRangeFingerprint: rowText(row.candidate_range_fingerprint), skillVersion: rowText(row.skill_version), algorithmVersion: rowText(row.algorithm_version), eventContractVersion: rowText(row.event_contract_version), policyVersion: rowText(row.decision_policy_version) ?? rowText(decisionReceipt?.policy_version), decisionReceipt, executionLedger, createdAt: String(row.created_at ?? ""), invalidatedAt: rowText(row.invalidated_at), }; } export type V9ComputeProjection = Readonly<{ caseId: string; skillVersion: string; baselineProfileFingerprint: string; baselineBirthSnapshot: Readonly>; candidateRange: { start_time: string; end_time: string }; }>; export function parseV9ComputeProjection(value: unknown): V9ComputeProjection | null { if (!value || typeof value !== "object") return null; const row = value as Record; const range = row.candidate_range && typeof row.candidate_range === "object" ? row.candidate_range as { start_time?: unknown; end_time?: unknown } : null; const snapshot = row.baseline_birth_snapshot && typeof row.baseline_birth_snapshot === "object" ? row.baseline_birth_snapshot as Record : null; if ( typeof row.case_id !== "string" || typeof row.skill_version !== "string" || typeof row.baseline_profile_fingerprint !== "string" || !snapshot || !range || typeof range.start_time !== "string" || typeof range.end_time !== "string" ) { return null; } return { caseId: row.case_id, skillVersion: row.skill_version, baselineProfileFingerprint: row.baseline_profile_fingerprint, baselineBirthSnapshot: snapshot, candidateRange: { start_time: range.start_time, end_time: range.end_time }, }; } export async function loadV9CaseDossier( accounting: AccountingClient, userId: string, caseId: string, ): Promise { const row = await rpc( accounting, "get_agentic_rectification_case_dossier", { p_user_id: userId, p_case_id: caseId }, ); const dossier = parseV9CaseDossier(row); if (!dossier) throw new RectificationToolServiceError("invalid_case_dossier"); return dossier; } export async function loadV9CaseCompute( accounting: AccountingClient, userId: string, caseId: string, ): Promise { const row = await rpc( accounting, "get_agentic_rectification_case_compute", { p_user_id: userId, p_case_id: caseId }, ); const projection = parseV9ComputeProjection(row); if (!projection) throw new RectificationToolServiceError("invalid_case_compute"); return projection; } /** Evidence rows that actually carry a scorable date. */ export function scorableEvidence( evidence: V9CaseDossier["evidence"], ): V9CaseDossier["evidence"] { return evidence.filter( (item) => item.status === "confirmed" && item.datePrecision !== "unknown" && (item.occurredFrom || item.occurredTo), ); } function hashParts(parts: readonly string[]): string { return createHash("sha256").update(parts.join("|")).digest("hex"); } /** Canonical evidence ledger fingerprint over scorable evidence rows. */ export function evidenceLedgerFingerprint( evidence: V9CaseDossier["evidence"], ): string { const rows = [...scorableEvidence(evidence)] .sort((left, right) => left.id.localeCompare(right.id)) .map((item) => [ item.id, item.eventKind, item.domain, item.occurredFrom ?? "", item.occurredTo ?? "", item.datePrecision, item.summary, ].join("::"), ); return hashParts(["v9-evidence-ledger-v1", ...rows]); } /** Candidate range fingerprint: range + baseline fingerprint. */ export function candidateRangeFingerprint( range: { start_time: string; end_time: string }, baselineProfileFingerprint: string, ): string { return hashParts([ "v9-candidate-range-v1", range.start_time, range.end_time, baselineProfileFingerprint, ]); } /** Canonical tool input fingerprint for receipts. */ export function canonicalToolInputFingerprint( toolName: string, args: Readonly>, ): string { const safe = Object.fromEntries( Object.entries(args) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => [key, typeof value === "string" ? value : JSON.stringify(value)]), ); return hashParts([`v9-tool:${toolName}`, JSON.stringify(safe)]); } export type V10RunAttemptStatus = "completed" | "failed" | "retryable" | "aborted"; export type V10RunAttemptClaim = Readonly<{ attemptId: string; status: string; shouldExecute: boolean; alreadyInProgress: boolean; }>; export async function createV10RunAttempt( accounting: AccountingClient, userId: string, caseId: string, turnId: string, attemptNumber: number, ): Promise { const row = await rpc<{ attempt_id?: unknown; status?: unknown; should_execute?: unknown; already_in_progress?: unknown; }>( accounting, "create_agentic_rectification_run_attempt", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_attempt_number: attemptNumber, }, ); const attemptId = rowText(row?.attempt_id); const status = rowText(row?.status) ?? "started"; if (!attemptId) throw new RectificationToolServiceError("invalid_attempt_id"); const legacyIdempotent = rowBoolean((row as { idempotent?: unknown } | null)?.idempotent); return { attemptId, status, shouldExecute: row?.should_execute === undefined ? !legacyIdempotent : rowBoolean(row.should_execute), alreadyInProgress: row?.already_in_progress === undefined ? legacyIdempotent && status === "started" : rowBoolean(row.already_in_progress), }; } export async function finalizeV10RunAttempt( accounting: AccountingClient, userId: string, caseId: string, turnId: string, attemptId: string, status: V10RunAttemptStatus, errorCode: string | null, usage: Readonly>, ): Promise { await rpc( accounting, "finalize_agentic_rectification_run_attempt", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_attempt_id: attemptId, p_status: status, p_error_code: errorCode, p_usage: usage, }, ); } export type V9TurnAppendResult = Readonly<{ turnId: string }>; export async function appendV9Turn( accounting: AccountingClient, userId: string, caseId: string, input: { userMessage: string | null; modelName: string; modelVersion?: string; }, ): Promise { const row = await rpc<{ turn_id?: unknown }>( accounting, "append_agentic_rectification_turn", { p_user_id: userId, p_case_id: caseId, p_user_message: input.userMessage, p_assistant_message: null, p_model_name: input.modelName, p_model_version: input.modelVersion ?? null, p_status: "pending", }, ); const turnId = typeof row?.turn_id === "string" ? row.turn_id : ""; if (!turnId) throw new RectificationToolServiceError("invalid_turn_id"); return { turnId }; } export async function finalizeV9Turn( accounting: AccountingClient, userId: string, caseId: string, turnId: string, status: "completed" | "failed" | "retryable", assistantMessage: string | null, ): Promise { await rpc( accounting, "finalize_agentic_rectification_turn", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_status: status, p_assistant_message: assistantMessage, }, ); } export async function insertV9ToolReceipt( accounting: AccountingClient, userId: string, caseId: string, turnId: string, input: { toolName: string; publicPhase: string; status: "started" | "completed" | "failed" | "skipped"; inputFingerprint?: string | null; resultFingerprint?: string | null; engineVersion?: string | null; safeErrorCode?: string | null; executedMethods?: readonly PublicRectificationMethod[]; attemptId?: string | null; }, ): Promise { if (!isPublicRectificationTool(input.toolName)) { throw new RectificationToolServiceError("tool_not_allowlisted"); } if (!isPublicRectificationPhase(input.publicPhase)) { throw new RectificationToolServiceError("phase_not_allowlisted"); } const executedMethods = [...new Set(input.executedMethods ?? [])]; if (!executedMethods.every(isPublicRectificationMethod)) { throw new RectificationToolServiceError("method_not_allowlisted"); } await rpc( accounting, "insert_agentic_rectification_tool_receipt", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_tool_name: input.toolName, p_public_phase: input.publicPhase, p_status: input.status, p_input_fingerprint: input.inputFingerprint ?? null, p_result_fingerprint: input.resultFingerprint ?? null, p_engine_version: input.engineVersion ?? null, p_safe_error_code: input.safeErrorCode ?? null, p_executed_methods: executedMethods, p_attempt_id: input.attemptId ?? null, }, ); } export async function insertV9RunPhase( accounting: AccountingClient, userId: string, caseId: string, turnId: string, phase: string, toolName: string | null, sequence: number, attemptId?: string | null, ): Promise { if (!isPublicRectificationPhase(phase)) { throw new RectificationToolServiceError("phase_not_allowlisted"); } await rpc( accounting, "insert_agentic_rectification_run_phase", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId, p_phase: phase, p_tool_name: toolName, p_sequence: sequence, p_attempt_id: attemptId ?? null, }, ); } export type V9TurnReceipt = Readonly<{ turnId: string; attemptId: string | null; status: string; skillName: string; skillVersion: string; engineVersion: string | null; phases: readonly Readonly<{ phase: string; tool: string | null }>[]; toolActivities: readonly Readonly<{ tool: PublicRectificationTool; status: "completed" | "failed"; methods: readonly PublicRectificationMethod[]; }>[]; tools: readonly string[]; methods: readonly PublicRectificationMethod[]; startedAt: string; completedAt: string | null; }>; export async function loadV9TurnReceipt( accounting: AccountingClient, userId: string, caseId: string, turnId: string, ): Promise { const row = await rpc>( accounting, "get_agentic_rectification_turn_receipt", { p_user_id: userId, p_case_id: caseId, p_turn_id: turnId }, ); if (!row || typeof row.turn_id !== "string") return null; const phases = rowArray(row.phases).flatMap((item) => { if (!item || typeof item !== "object") return []; const phaseRow = item as Record; return [{ phase: String(phaseRow.phase ?? ""), tool: rowText(phaseRow.tool) }]; }); const toolActivities = rowArray(row.tool_activities) .flatMap((item) => { const activity = rowObject(item); if (!activity) return []; const tool = activity.tool; const status = activity.status; if (!isPublicRectificationTool(tool) || (status !== "completed" && status !== "failed")) return []; return [{ tool, status, methods: status === "completed" ? rowArray(activity.methods).filter(isPublicRectificationMethod) : [], }]; }); return { turnId: row.turn_id, attemptId: rowText(row.attempt_id), status: String(row.status ?? ""), skillName: String(row.skill_name ?? ""), skillVersion: String(row.skill_version ?? ""), engineVersion: rowText(row.engine_version), phases, toolActivities, tools: rowArray(row.tools).map((item) => String(item)), methods: rowArray(row.methods).filter(isPublicRectificationMethod), startedAt: String(row.started_at ?? ""), completedAt: rowText(row.completed_at), }; } /** Map a persisted turn status to the public receipt status vocabulary. */ export function receiptStatusFromTurn(status: string): "completed" | "degraded" | "blocked" | "failed" { if (status === "completed") return "completed"; if (status === "retryable") return "degraded"; return "failed"; } export type ProposeEvidenceResult = Readonly<{ evidenceId: string | null; idempotent: boolean; outcome: "accepted" | "rejected"; errorCode: string | null; status: string; }>; export async function proposeV9Evidence( accounting: AccountingClient, userId: string, caseId: string, input: { sourceTurnId: string; quote: string; subject: "self" | "family" | "other"; eventKind: EvidenceKind; domain: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; }, ): Promise { const row = await rpc<{ evidence_id?: unknown; idempotent?: unknown; outcome?: unknown; error_code?: unknown; status?: unknown; }>( accounting, "propose_agentic_rectification_evidence", { p_user_id: userId, p_case_id: caseId, p_source_turn_id: input.sourceTurnId, p_user_quote: input.quote, p_subject: input.subject, p_event_kind: input.eventKind, p_domain: input.domain, p_occurred_from: input.occurredFrom, p_occurred_to: input.occurredTo, p_date_precision: input.datePrecision, p_summary: input.summary, }, ); const errorCode = rowText(row?.error_code); const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : null; if (errorCode || !evidenceId) { return { evidenceId: null, idempotent: false, outcome: "rejected", errorCode: errorCode ?? "invalid_item", status: String(row?.status ?? "rejected"), }; } return { evidenceId, idempotent: row?.idempotent === true, outcome: "accepted", errorCode: null, status: String(row?.status ?? "draft"), }; } export async function confirmV9Evidence( accounting: AccountingClient, userId: string, caseId: string, evidenceId: string, ): Promise> { const row = await rpc<{ evidence_id?: unknown; status?: unknown; idempotent?: unknown }>( accounting, "confirm_agentic_rectification_evidence", { p_user_id: userId, p_case_id: caseId, p_evidence_id: evidenceId }, ); const confirmedId = typeof row?.evidence_id === "string" ? row.evidence_id : ""; if (!confirmedId) throw new RectificationToolServiceError("invalid_evidence_id"); return { evidenceId: confirmedId, status: String(row?.status ?? "confirmed"), idempotent: row?.idempotent === true, }; } export async function reviseV9Evidence( accounting: AccountingClient, userId: string, caseId: string, input: { evidenceId: string; quote: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; }, ): Promise> { const row = await rpc<{ evidence_id?: unknown; supersedes_evidence_id?: unknown; idempotent?: unknown }>( accounting, "revise_agentic_rectification_evidence", { p_user_id: userId, p_case_id: caseId, p_evidence_id: input.evidenceId, p_user_quote: input.quote, p_occurred_from: input.occurredFrom, p_occurred_to: input.occurredTo, p_date_precision: input.datePrecision, p_summary: input.summary, }, ); const evidenceId = typeof row?.evidence_id === "string" ? row.evidence_id : ""; const supersedes = typeof row?.supersedes_evidence_id === "string" ? row.supersedes_evidence_id : ""; if (!evidenceId) throw new RectificationToolServiceError("invalid_evidence_id"); return { evidenceId, supersedesEvidenceId: supersedes, idempotent: row?.idempotent === true }; } export async function setV10ConversationFocus( accounting: AccountingClient, userId: string, caseId: string, input: { questionId: string; intent: string; targetEvidenceId?: string | null; targetDomain?: string | null; targetKind?: string | null; expectedAnswerSchema?: Readonly>; }, ): Promise> { const row = await rpc>( accounting, "set_agentic_rectification_conversation_focus", { p_user_id: userId, p_case_id: caseId, p_question_id: input.questionId, p_intent: input.intent, p_target_evidence_id: input.targetEvidenceId ?? null, p_target_domain: input.targetDomain ?? null, p_target_kind: input.targetKind ?? null, p_expected_answer_schema: input.expectedAnswerSchema ?? {}, }, ); const focus = parseConversationFocus(row.focus ?? row); if (!focus) throw new RectificationToolServiceError("invalid_focus"); return { focus, idempotent: row.idempotent === true }; } export async function resolveV10ConversationFocus( accounting: AccountingClient, userId: string, caseId: string, input: { focusId: string; status: "resolved" | "declined" | "skipped"; evidenceId?: string | null; }, ): Promise> { const row = await rpc>( accounting, "resolve_agentic_rectification_conversation_focus", { p_user_id: userId, p_case_id: caseId, p_focus_id: input.focusId, p_status: input.status, p_evidence_id: input.evidenceId ?? null, }, ); const focusId = rowText(row.focus_id); if (!focusId) throw new RectificationToolServiceError("invalid_focus"); return { focusId, status: String(row.status ?? input.status), evidenceId: rowText(row.evidence_id), idempotent: row.idempotent === true, }; } export async function confirmV10Evidence( accounting: AccountingClient, userId: string, caseId: string, focusId: string | null, evidenceId: string, ): Promise> { const row = await rpc>( accounting, "confirm_agentic_rectification_evidence_v10", { p_user_id: userId, p_case_id: caseId, p_focus_id: focusId, p_evidence_id: evidenceId, }, ); const confirmedId = rowText(row.evidence_id); if (!confirmedId) throw new RectificationToolServiceError("invalid_evidence_id"); return { focusId: rowText(row.focus_id), evidenceId: confirmedId, status: String(row.status ?? "confirmed"), idempotent: row.idempotent === true, }; } export async function reviseV10Evidence( accounting: AccountingClient, userId: string, caseId: string, input: { focusId: string; evidenceId: string; quote: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; }, ): Promise> { const row = await rpc>( accounting, "revise_agentic_rectification_evidence_v10", { p_user_id: userId, p_case_id: caseId, p_focus_id: input.focusId, p_evidence_id: input.evidenceId, p_user_quote: input.quote, p_occurred_from: input.occurredFrom, p_occurred_to: input.occurredTo, p_date_precision: input.datePrecision, p_summary: input.summary, }, ); const evidenceId = rowText(row.evidence_id); const supersedesEvidenceId = rowText(row.supersedes_evidence_id); if (!evidenceId || !supersedesEvidenceId) { throw new RectificationToolServiceError("invalid_evidence_id"); } return { focusId: rowText(row.focus_id) ?? input.focusId, evidenceId, supersedesEvidenceId, idempotent: row.idempotent === true, }; } export type V10EvidenceBatchItem = Readonly<{ quote: string; subject: "self" | "family" | "other"; eventKind: EvidenceKind; domain: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; }>; export type V10EvidenceBatchResult = Readonly<{ items: readonly Readonly<{ index: number; outcome: "accepted" | "needs_clarification" | "rejected"; evidenceId: string | null; status: string; idempotent: boolean; clarificationFields: readonly string[]; errorCode: string | null; }>[]; acceptedCount: number; needsClarificationCount: number; rejectedCount: number; focusId: string | null; }>; export async function recordV10EvidenceBatch( accounting: AccountingClient, userId: string, caseId: string, sourceTurnId: string, focusId: string | null, items: readonly V10EvidenceBatchItem[], ): Promise { const rpcItems = items.map((item) => ({ idempotency_key: hashParts([ "v10-evidence-item-v1", caseId, sourceTurnId, JSON.stringify({ quote: item.quote, subject: item.subject, event_kind: item.eventKind, domain: item.domain, occurred_from: item.occurredFrom, occurred_to: item.occurredTo, date_precision: item.datePrecision, summary: item.summary, }), ]), quote: item.quote, subject: item.subject, event_kind: item.eventKind, domain: item.domain, occurred_from: item.occurredFrom, occurred_to: item.occurredTo, date_precision: item.datePrecision, summary: item.summary, })); const row = await rpc>( accounting, "record_agentic_rectification_evidence_batch", { p_user_id: userId, p_case_id: caseId, p_source_turn_id: sourceTurnId, p_focus_id: focusId, p_items: rpcItems, }, ); const results = rowArray(row.items).flatMap((value) => { const item = rowObject(value); if (!item) return []; const outcome = item.outcome; if (outcome !== "accepted" && outcome !== "needs_clarification" && outcome !== "rejected") return []; return [{ index: rowNumber(item.index) ?? 0, outcome: outcome as V10EvidenceBatchResult["items"][number]["outcome"], evidenceId: rowText(item.evidence_id), status: String(item.status ?? ""), idempotent: item.idempotent === true, clarificationFields: rowArray(item.clarification_fields) .filter((field): field is string => typeof field === "string"), errorCode: rowText(item.error_code), }]; }); return { items: results, acceptedCount: rowNumber(row.accepted_count) ?? results.filter((item) => item.outcome === "accepted").length, needsClarificationCount: rowNumber(row.needs_clarification_count) ?? results.filter((item) => item.outcome === "needs_clarification").length, rejectedCount: rowNumber(row.rejected_count) ?? results.filter((item) => item.outcome === "rejected").length, focusId: rowText(row.focus_id), }; } export async function transitionV9CaseStatus( accounting: AccountingClient, userId: string, caseId: string, status: "draft" | "collecting_evidence" | "candidate_ready" | "candidate_accepted" | "needs_rebaseline" | "paused", ): Promise> { const row = await rpc<{ status?: unknown; idempotent?: unknown }>( accounting, "transition_agentic_rectification_case_status", { p_user_id: userId, p_case_id: caseId, p_status: status }, ); return { status: String(row?.status ?? status), idempotent: row?.idempotent === true }; } export type V9PersistedCandidate = Readonly<{ resultId: string; cached: boolean; candidates: V9CandidateSnapshot["candidates"]; overallConfidence: "low" | "medium" | "high"; selectionAllowed: boolean; confirmationAllowed: boolean; representativeTime: string | null; algorithmVersion: string | null; eventContractVersion: string | null; policyVersion: string | null; decisionReceipt: Readonly>; executionLedger: readonly Readonly>[]; }>; export async function persistV9Candidate( accounting: AccountingClient, userId: string, caseId: string, input: { engineResultId: string; algorithmVersion: string; evidenceFingerprint: string; rangeFingerprint: string; skillVersion: string; eventContractVersion: string; policyVersion: string; candidateRange: { start_time: string; end_time: string }; candidates: V9CandidateSnapshot["candidates"]; decisionReceipt: Readonly>; executionLedger: readonly Readonly>[]; }, ): Promise { const row = await rpc>( accounting, "persist_agentic_rectification_candidate_v2", { p_user_id: userId, p_case_id: caseId, p_engine_result_id: input.engineResultId, p_evidence_ledger_fingerprint: input.evidenceFingerprint, p_candidate_range_fingerprint: input.rangeFingerprint, p_skill_version: input.skillVersion, p_algorithm_version: input.algorithmVersion, p_event_contract_version: input.eventContractVersion, p_decision_policy_version: input.policyVersion, p_candidate_range: input.candidateRange, p_candidates: input.candidates.map((candidate) => ({ candidate_id: candidate.candidateId, time: candidate.time, rank: candidate.rank, relative_support: candidate.relativeSupport, tied_minute_count: candidate.tiedMinuteCount, })), p_decision_receipt: input.decisionReceipt, p_execution_ledger: input.executionLedger, }, ); const snapshot = parseV9CandidateSnapshot(row); if (!snapshot) throw new RectificationToolServiceError("invalid_candidate_result"); return { resultId: snapshot.resultId, cached: row.cached === true, candidates: snapshot.candidates, overallConfidence: snapshot.overallConfidence, selectionAllowed: snapshot.selectionAllowed, confirmationAllowed: snapshot.confirmationAllowed, representativeTime: snapshot.representativeTime, algorithmVersion: snapshot.algorithmVersion ?? input.algorithmVersion, eventContractVersion: snapshot.eventContractVersion ?? input.eventContractVersion, policyVersion: snapshot.policyVersion ?? input.policyVersion, decisionReceipt: snapshot.decisionReceipt ?? input.decisionReceipt, executionLedger: snapshot.executionLedger ?? input.executionLedger, }; } function parseTransitionSnapshot(value: unknown): InferenceTransitionSnapshot | null { const row = rowObject(value); const inference = asInferenceState(row?.inference_state); const resultId = rowText(row?.result_id); const fingerprint = rowText(row?.decision_state_fingerprint); const revision = rowNumber(row?.revision); if (!row || !inference || !resultId || !fingerprint || revision === null) return null; return { id: rowText(row.id) ?? undefined, resultId, revision, probeId: rowText(row.probe_id), reason: String(row.reason ?? "choice"), idempotent: row.idempotent === true, decisionStateFingerprint: fingerprint, inferenceState: inference, posteriorBefore: rowObject(row.posterior_before) as Record ?? {}, posteriorAfter: rowObject(row.posterior_after) as Record ?? {}, scoreDeltas: rowObject(row.score_deltas) as Record ?? {}, }; } export async function loadLatestInferenceTransition( accounting: AccountingClient, userId: string, caseId: string, ): Promise { const row = await rpc( accounting, "get_agentic_rectification_latest_inference_transition", { p_user_id: userId, p_case_id: caseId }, ); return parseTransitionSnapshot(row); } export type PersistInferenceTransitionInput = Readonly<{ expectedRevision: number; probeId: string; openProbeId: string; semanticKey: string; candidateSplitHash: string; answerClass: string; rawAnswer: string; inferenceState: Readonly>; posteriorBefore: Readonly>; posteriorAfter: Readonly>; scoreDeltas: Readonly>; decisionStateFingerprint: string; reason: "choice" | "supersede" | "already_answered"; idempotencyKey: string; candidateSetId: string; }>; export async function persistV9InferenceState( accounting: AccountingClient, userId: string, caseId: string, input: PersistInferenceTransitionInput, ): Promise>; decisionStateFingerprint: string; }>> { const row = await rpc>( accounting, "append_agentic_rectification_inference_transition", { p_user_id: userId, p_case_id: caseId, p_expected_revision: input.expectedRevision, p_probe_id: input.probeId, p_open_probe_id: input.openProbeId, p_semantic_key: input.semanticKey, p_candidate_split_hash: input.candidateSplitHash, p_answer_class: input.answerClass, p_raw_answer: input.rawAnswer, p_inference_state: input.inferenceState, p_posterior_before: input.posteriorBefore, p_posterior_after: input.posteriorAfter, p_score_deltas: input.scoreDeltas, p_decision_state_fingerprint: input.decisionStateFingerprint, p_reason: input.reason, p_idempotency_key: input.idempotencyKey, p_candidate_set_id: input.candidateSetId, }, ); const resultId = rowText(row.result_id); const decisionReceipt = rowObject(row.decision_receipt); const revision = rowNumber(row.revision); const fingerprint = rowText(row.decision_state_fingerprint) ?? input.decisionStateFingerprint; if (!resultId || !decisionReceipt || revision === null) { throw new RectificationToolServiceError("invalid_inference_transition"); } return { resultId, revision, idempotent: row.idempotent === true, decisionReceipt, decisionStateFingerprint: fingerprint, }; } export function inferenceFingerprintForState( caseId: string, evidenceLedgerFingerprint: string, state: { candidate_set_id: string; revision: number; answered_probes: readonly { probe_id: string }[] }, scoringPolicyVersion = INFERENCE_ALGORITHM_VERSION, ): string { return decisionStateFingerprint({ caseId, evidenceLedgerFingerprint, candidateSetId: state.candidate_set_id, inferenceRevision: state.revision, answeredProbeIds: state.answered_probes.map((item) => item.probe_id), scoringPolicyVersion, }); } export type V9AcceptResult = Readonly<{ success: boolean; savedTime: string; status: "accepted"; resultId: string; caseStatus: string; idempotent: boolean; }>; export async function acceptV9Candidate( accounting: AccountingClient, userId: string, caseId: string, resultId: string, candidateId: string, requestId: string, ): Promise { if (!uuidPattern.test(candidateId) || !uuidPattern.test(requestId)) { throw new RectificationToolServiceError("invalid_candidate_ref"); } const row = await rpc>( accounting, "accept_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: resultId, p_candidate_id: candidateId, p_request_id: requestId, }, ); const savedTime = timeValue(row.saved_time); if (row.success !== true || row.status !== "accepted" || !savedTime) { throw new RectificationToolServiceError("invalid_accept_result"); } return { success: true, savedTime, status: "accepted", resultId: String(row.result_id ?? resultId), caseStatus: String(row.case_status ?? "candidate_accepted"), idempotent: row.idempotent === true, }; } export type V9ConfirmResult = Readonly<{ success: boolean; savedTime: string; status: "confirmed"; resultId: string; caseStatus: string; idempotent: boolean; }>; export async function confirmV9BirthTime( accounting: AccountingClient, userId: string, caseId: string, input: { resultId: string; candidateId: string; requestId: string; consentQuote: string; sourceTurnId: string; }, ): Promise { if (!uuidPattern.test(input.candidateId) || !uuidPattern.test(input.requestId)) { throw new RectificationToolServiceError("invalid_candidate_ref"); } const row = await rpc>( accounting, "confirm_agentic_rectification_candidate_for_case_v2", { p_user_id: userId, p_case_id: caseId, p_result_id: input.resultId, p_candidate_id: input.candidateId, p_request_id: input.requestId, p_consent_quote: input.consentQuote, p_source_turn_id: input.sourceTurnId, }, ); const savedTime = timeValue(row.saved_time); if (row.success !== true || row.status !== "confirmed" || !savedTime) { throw new RectificationToolServiceError("invalid_confirm_result"); } return { success: true, savedTime, status: "confirmed", resultId: String(row.result_id ?? input.resultId), caseStatus: String(row.case_status ?? "confirmed"), idempotent: row.idempotent === true, }; } export async function closeV9Case( accounting: AccountingClient, userId: string, caseId: string, reason: "completed_by_user" | "abandoned_by_user" | "other", ): Promise> { const row = await rpc>( accounting, "close_agentic_rectification_case", { p_user_id: userId, p_case_id: caseId, p_reason: reason }, ); if (row?.success !== true) throw new RectificationToolServiceError("invalid_close_result"); return { success: true, caseId: String(row?.case_id ?? caseId), status: String(row?.status ?? "closed"), idempotent: row?.idempotent === true, }; } /** Map an RPC error message back to a safe tool error code. */ export function safeToolErrorCode(error: unknown): string { const message = error instanceof Error ? error.message : String(error); const known = [ "quote_not_grounded", "consent_not_grounded", "evidence_not_found", "evidence_not_confirmable", "evidence_not_revisable", "case_terminal", "case_not_found", "candidate_not_found", "candidate_expired", "candidate_selection_blocked", "candidate_time_not_allowed", "candidate_already_selected", "candidate_superseded", "candidate_profile_changed", "confirmation_blocked", "confirm_time_mismatch", "case_already_confirmed", "legacy_skill_identity_unverifiable", "skill_identity_missing", "skill_identity_mismatch", "invalid_skill_identity", "skill_version_mismatch", "turn_not_found", "focus_not_found", "focus_not_active", "focus_target_mismatch", "focus_idempotency_conflict", "attempt_not_found", "attempt_already_finalized", "attempt_not_successful", "idempotency_conflict", "invalid_input", "invalid_choice_copy", "precision_downgrade", "offer_not_allowed", "no_candidate_result", "stale_probe", "revision_conflict", "inference_patch_retired", ]; if (error instanceof RectificationToolServiceError && known.includes(error.code)) { return error.code; } for (const code of known) { if (message.includes(`agentic_rectification_${code}`)) return code; } return "tool_failed"; } export const V9_EVIDENCE_KINDS = EVIDENCE_KINDS; export const V9_SKILL_VERSION = RECTIFICATION_SKILL_VERSION; export type V9PublicTool = PublicRectificationTool; export type V9PublicPhase = PublicRectificationPhase;