/** * V9 deterministic engine client (Python, server-side only). * * The Python service owns Event Contract v2 normalization, candidate ranking, * decision policy and the execution ledger. This client validates and projects * that server contract; it never reconstructs rank, support, ties, confidence, * permissions or executed methods from raw scores or event domains. */ import { isBackgroundEvidenceKind, isEvidenceDomain, isEvidenceKind, type EvidenceKind, } from "./evidence-model"; import { isPublicRectificationMethod, type PublicRectificationMethod, } from "./public-receipt"; export class RectificationEngineError extends Error { readonly code: string; constructor(code: string, message: string) { super(message); this.name = "RectificationEngineError"; this.code = code; } } export type V9EngineEvent = Readonly<{ id: string; domain: string; event_kind: string; date_start: string; date_end: string; precision: "day" | "month" | "quarter" | "year" | "range"; summary: string; date_source?: string | null; date_reliability?: string | null; date_corroboration?: string | null; date_conflict_status?: string | null; source_turn_id?: string | null; subject?: string | null; }>; export type V9EngineCandidate = Readonly<{ candidateId: string; time: string; rank: number; relativeSupport: number; tiedMinuteCount: number; }>; export type V9DecisionReceipt = Readonly>; export type V9ExecutionLedger = readonly Readonly>[]; export type V9EngineScoreResult = Readonly<{ engineResultId: string; algorithmVersion: string; eventContractVersion: string; policyVersion: string; candidateRange: { start_time: string; end_time: string }; candidates: readonly V9EngineCandidate[]; overallConfidence: "low" | "medium" | "high"; marginPercent: number | null; selectionAllowed: boolean; confirmationAllowed: boolean; representativeCandidateId: string | null; representativeTime: string | null; decisionReceipt: V9DecisionReceipt; executionLedger: V9ExecutionLedger; executedMethods: readonly PublicRectificationMethod[]; }>; export type V9EngineDiagnostics = Readonly<{ algorithmVersion: string; engineResultId: string; eventContractVersion: string; policyVersion: string; diagnostics: Readonly>; missingLayers: readonly string[]; canConfirmExactMinute: boolean; decisionReceipt: V9DecisionReceipt; executionLedger: V9ExecutionLedger; executedMethods: readonly PublicRectificationMethod[]; }>; const EVENT_CONTRACT_VERSION = "rectification-event-contract-v2"; const RECEIPT_VERSION = "candidate-decision-receipt-v2"; const EXECUTION_LEDGER_VERSION = "rectification-execution-ledger-v2"; const timePattern = /^(?:[01]\d|2[0-3]):[0-5]\d$/; 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; function record(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function engineNumber(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function clockMinute(value: string): number { const [hour = 0, minute = 0] = value.split(":").map(Number); return hour * 60 + minute; } function timeInRange(time: string, range: { start_time: string; end_time: string }): boolean { const value = clockMinute(time); const start = clockMinute(range.start_time); const end = clockMinute(range.end_time); return end >= start ? value >= start && value <= end : value >= start || value <= end; } /** Map a V9 evidence date precision to the Event Contract v2 vocabulary. */ export function enginePrecision(precision: string): V9EngineEvent["precision"] { if (precision === "day" || precision === "month" || precision === "quarter" || precision === "range") { return precision; } return "year"; } export function toEngineEvents( evidence: readonly Readonly<{ id: string; eventKind: string; domain: string; occurredFrom: string | null; occurredTo: string | null; datePrecision: string; summary: string; dateSource?: string | null; dateReliability?: string | null; dateCorroboration?: string | null; dateConflictStatus?: string | null; sourceTurnId?: string | null; subject?: string | null; }>[], ): V9EngineEvent[] { return evidence.flatMap((item): V9EngineEvent[] => { if (!isEvidenceKind(item.eventKind) || !isEvidenceDomain(item.domain)) return []; if (isBackgroundEvidenceKind(item.eventKind as EvidenceKind)) return []; const start = item.occurredFrom ?? item.occurredTo; const end = item.occurredTo ?? item.occurredFrom; if (!start) return []; return [{ id: item.id, domain: item.domain, event_kind: item.eventKind, date_start: start.slice(0, 10), date_end: (end ?? start).slice(0, 10), precision: enginePrecision(item.datePrecision), summary: item.summary, date_source: item.dateSource ?? null, date_reliability: item.dateReliability ?? null, date_corroboration: item.dateCorroboration ?? null, date_conflict_status: item.dateConflictStatus ?? null, source_turn_id: item.sourceTurnId ?? null, subject: item.subject ?? null, }]; }); } function engineBase(): string { return process.env.JYOTISH_API_BASE?.trim() || "http://127.0.0.1:5200"; } async function postEngine(path: string, body: unknown, timeoutMs = 60_000): Promise> { const response = await fetch(`${engineBase()}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs), }); const data = await response.json().catch(() => null); if (!response.ok) { const message = data && typeof data === "object" ? String((data as Record).error ?? (data as Record).message ?? `Jyotish API ${response.status}`) : `Jyotish API ${response.status}`; throw new RectificationEngineError("engine_request_failed", message); } if (!data || typeof data !== "object") { throw new RectificationEngineError("engine_invalid_response", "Jyotish API returned an invalid response"); } return data as Record; } function readCandidates( value: unknown, range: { start_time: string; end_time: string }, ): V9EngineCandidate[] { if (!Array.isArray(value) || value.length === 0) { throw new RectificationEngineError("engine_invalid_candidate_decisions", "engine candidate decisions are missing"); } const seenIds = new Set(); const seenTimes = new Set(); const candidates: V9EngineCandidate[] = []; for (const item of value) { const row = record(item); const candidateId = row?.candidate_id; const time = row?.time; const rank = row?.rank; const relativeSupport = row?.relative_support; const tiedMinuteCount = row?.tied_minute_count; if ( typeof candidateId !== "string" || !uuidPattern.test(candidateId) || typeof time !== "string" || !timePattern.test(time) || !timeInRange(time, range) || typeof rank !== "number" || !Number.isInteger(rank) || rank < 1 || typeof relativeSupport !== "number" || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100 || typeof tiedMinuteCount !== "number" || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1 || seenIds.has(candidateId) || seenTimes.has(time) ) { throw new RectificationEngineError("engine_invalid_candidate_decisions", "engine candidate decisions are invalid"); } seenIds.add(candidateId); seenTimes.add(time); candidates.push({ candidateId, time, rank, relativeSupport, tiedMinuteCount }); } return candidates; } type ParsedReceipt = Readonly<{ raw: V9DecisionReceipt; eventContractVersion: string; policyVersion: string; selectionAllowed: boolean; confirmationAllowed: boolean; representativeCandidateId: string | null; representativeTime: string | null; overallConfidence: "low" | "medium" | "high"; marginPercent: number | null; }>; function invalidReceipt(): never { throw new RectificationEngineError("engine_invalid_v2_receipt", "engine decision receipt is missing or invalid"); } function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandidate[]): ParsedReceipt { const row = record(value); if (!row) return invalidReceipt(); const representativeCandidateId = row.representative_candidate_id; const representativeTime = row.representative_time; const confidence = row.overall_confidence; const marginPercent = row.margin_percent === null || row.margin_percent === undefined ? null : engineNumber(row.margin_percent); if ( row.receipt_version !== RECEIPT_VERSION || row.contract_version !== "v2" || row.event_contract_version !== EVENT_CONTRACT_VERSION || typeof row.policy_version !== "string" || !row.policy_version || typeof row.decision_policy_version !== "string" || row.decision_policy_version !== row.policy_version || typeof row.display_allowed !== "boolean" || typeof row.selection_allowed !== "boolean" || typeof row.acceptance_allowed !== "boolean" || typeof row.confirmation_allowed !== "boolean" || typeof row.accept_allowed !== "boolean" || typeof row.confirm_allowed !== "boolean" || row.selection_allowed !== row.acceptance_allowed || row.selection_allowed !== row.accept_allowed || row.confirmation_allowed !== row.confirm_allowed || (representativeCandidateId !== null && (typeof representativeCandidateId !== "string" || !uuidPattern.test(representativeCandidateId))) || (representativeTime !== null && (typeof representativeTime !== "string" || !timePattern.test(representativeTime))) || (confidence !== "low" && confidence !== "medium" && confidence !== "high") || (row.margin_percent !== null && row.margin_percent !== undefined && marginPercent === null) ) { return invalidReceipt(); } const representative = representativeCandidateId === null ? null : candidates.find((candidate) => candidate.candidateId === representativeCandidateId) ?? null; if ( (representativeCandidateId === null) !== (representativeTime === null) || (representativeCandidateId !== null && (!representative || representative.time !== representativeTime)) || (row.confirmation_allowed === true && row.selection_allowed !== true) || (row.selection_allowed === true && row.display_allowed !== true) ) { return invalidReceipt(); } return { raw: row, eventContractVersion: EVENT_CONTRACT_VERSION, policyVersion: row.policy_version, selectionAllowed: row.selection_allowed, confirmationAllowed: row.confirmation_allowed, representativeCandidateId, representativeTime, overallConfidence: confidence, marginPercent, }; } function readExecutionLedger(version: unknown, value: unknown): V9ExecutionLedger { if (version !== EXECUTION_LEDGER_VERSION || !Array.isArray(value) || value.length === 0) { throw new RectificationEngineError("engine_invalid_execution_ledger", "engine execution ledger is missing or invalid"); } return value.map((item) => { const row = record(item); if ( !row || row.ledger_version !== EXECUTION_LEDGER_VERSION || typeof row.stage !== "string" || !row.stage || (row.status !== "executed" && row.status !== "not_executed" && row.status !== "retained_not_scored") || typeof row.source !== "string" || !row.source ) { throw new RectificationEngineError("engine_invalid_execution_ledger", "engine execution ledger is missing or invalid"); } return row; }); } function publicMethodFromLedger(value: unknown): PublicRectificationMethod | null { if (isPublicRectificationMethod(value)) return value; if (typeof value !== "string") return null; const normalized = value.toLowerCase(); if (normalized.includes("controlled_transit") || normalized.includes("gochara")) return "gochara"; if (normalized.includes("ashtakavarga")) return "ashtakavarga"; if (normalized.includes("shadbala")) return "shadbala"; if (normalized.includes("arudha")) return "arudha-pada"; if (normalized.includes("functional_benefic") || normalized.includes("functional_malefic")) return "functional-benefic-malefic"; if (normalized.includes("vimshottari") || normalized.includes("vimsottari")) return "vimshottari-dasha"; if (normalized.includes("narayana")) return "narayana-dasha"; return null; } function executedMethods(ledger: V9ExecutionLedger): PublicRectificationMethod[] { const methods = new Set(); for (const entry of ledger) { if (entry.status !== "executed") continue; const direct = publicMethodFromLedger(entry.method); if (direct) methods.add(direct); if (Array.isArray(entry.technique_layers)) { for (const layer of entry.technique_layers) { const method = publicMethodFromLedger(layer); if (method) methods.add(method); } } } return [...methods]; } function engineDiagnostics(data: Record): Readonly> { return record(data.diagnostics) ?? {}; } function engineRequestBody(input: { baselineBirthSnapshot: Readonly>; candidateRange: { start_time: string; end_time: string }; events: readonly V9EngineEvent[]; }): Record { const snapshot = input.baselineBirthSnapshot; const birthDate = String(snapshot.birth_date ?? ""); const lat = engineNumber(snapshot.latitude); const lon = engineNumber(snapshot.longitude); const tz = engineNumber(snapshot.timezone_offset); if (!birthDate || lat === null || lon === null || tz === null) { throw new RectificationEngineError("engine_profile_incomplete", "server profile snapshot is incomplete"); } if (input.events.length === 0) { throw new RectificationEngineError("no_scorable_evidence", "no scorable evidence for the engine"); } return { birth_date: birthDate, start_time: input.candidateRange.start_time, end_time: input.candidateRange.end_time, lat, lon, tz, events: input.events, birth_time_source: snapshot.birth_time_source, timezone_id: snapshot.timezone_id, timezone_source: snapshot.timezone_source, local_time_status: snapshot.local_time_status, }; } export async function runV9CandidateScore(input: { baselineBirthSnapshot: Readonly>; candidateRange: { start_time: string; end_time: string }; events: readonly V9EngineEvent[]; }): Promise { const data = await postEngine("/api/rectification/v5/score", engineRequestBody(input)); const candidates = readCandidates(data.candidate_decisions, input.candidateRange); const receipt = readDecisionReceipt(data.decision_receipt, candidates); const ledger = readExecutionLedger(data.execution_ledger_version, data.execution_ledger); if ( data.event_contract_version !== receipt.eventContractVersion || data.decision_policy_version !== receipt.policyVersion ) { return invalidReceipt(); } return { engineResultId: String(data.result_id ?? ""), algorithmVersion: String(data.algorithm_version ?? ""), eventContractVersion: receipt.eventContractVersion, policyVersion: receipt.policyVersion, candidateRange: input.candidateRange, candidates, overallConfidence: receipt.overallConfidence, marginPercent: receipt.marginPercent, selectionAllowed: receipt.selectionAllowed, confirmationAllowed: receipt.confirmationAllowed, representativeCandidateId: receipt.representativeCandidateId, representativeTime: receipt.representativeTime, decisionReceipt: receipt.raw, executionLedger: ledger, executedMethods: executedMethods(ledger), }; } export async function runV9Diagnostics(input: { baselineBirthSnapshot: Readonly>; candidateRange: { start_time: string; end_time: string }; events: readonly V9EngineEvent[]; }): Promise { const data = await postEngine("/api/rectification/v5/diagnostics", engineRequestBody(input)); const candidates = readCandidates(data.candidate_decisions, input.candidateRange); const receipt = readDecisionReceipt(data.decision_receipt, candidates); const ledger = readExecutionLedger(data.execution_ledger_version, data.execution_ledger); if ( data.event_contract_version !== receipt.eventContractVersion || data.decision_policy_version !== receipt.policyVersion ) { return invalidReceipt(); } const diagnostics = engineDiagnostics(data); const missingLayers = Array.isArray(data.missing_layers) ? data.missing_layers.filter((item): item is string => typeof item === "string") : []; return { algorithmVersion: String(data.algorithm_version ?? ""), engineResultId: String(data.result_id ?? ""), eventContractVersion: receipt.eventContractVersion, policyVersion: receipt.policyVersion, diagnostics: { primary_cluster_retention_rate: diagnostics.primary_cluster_retention_rate, leave_one_event_out_retention_rate: diagnostics.leave_one_event_out_retention_rate, leave_one_domain_out_retention_rate: diagnostics.leave_one_domain_out_retention_rate, date_sensitivity_retention_rate: diagnostics.date_sensitivity_retention_rate, neighbor_support_minutes: diagnostics.neighbor_support_minutes, primary_secondary_margin_percent: diagnostics.primary_secondary_margin_percent, unstable_event_ids: diagnostics.unstable_event_ids, most_discriminating_layers: diagnostics.most_discriminating_layers, candidate_splits: diagnostics.candidate_splits, }, missingLayers, canConfirmExactMinute: receipt.confirmationAllowed, decisionReceipt: receipt.raw, executionLedger: ledger, executedMethods: executedMethods(ledger), }; } export const v9EngineVersion = (): string => process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5";