/** * 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 { evidenceSubjectForDomain, isBackgroundEvidenceKind, isEvidenceDomain, isEvidenceKind, type EvidenceKind, } from "./evidence-model"; import { isPublicRectificationMethod, type PublicRectificationMethod, } from "./public-receipt"; import { parseWindowScan, type WindowScan, } from "./varga-observations"; import { questionContractVersionIsCompatible } from "./probe-question-contract"; import { resolveAyanamsa } from "../../ayanamsa.ts"; import { candidateSegmentMetadata } from "./segment-contract.ts"; import type { AscendantSignature } from "../core/segment-summary.ts"; import { isDatedScoringAlgorithmVersion, requestCandidateIntervals, readCandidatePosition, type CandidatePosition, type DatedCandidateRange } from "../core/candidate-window.ts"; import { parseBlockScanPayload, type BlockScanBlock } from "./block-scan.ts"; import { recordEngineCallTiming, reportTurnProgress } from "./turn-instrumentation.ts"; export type EngineCallFailureKind = "busy" | "http_error" | "timeout" | "bad_payload"; export const RECTIFICATION_ENGINE_BUSY_RETRY_LIMIT = 2; export const RECTIFICATION_ENGINE_BUSY_BACKOFF_BUDGET_MS = 6_000; export class RectificationEngineError extends Error { readonly code: string; readonly kind?: EngineCallFailureKind; readonly httpStatus?: number; readonly retryAfterSeconds?: number; readonly elapsedMs?: number; readonly path?: string; readonly errorName?: string; constructor( code: string, message: string, extras?: Readonly<{ kind?: EngineCallFailureKind; httpStatus?: number; retryAfterSeconds?: number; elapsedMs?: number; path?: string; errorName?: string; }>, ) { super(message); this.name = "RectificationEngineError"; this.code = code; this.kind = extras?.kind; this.httpStatus = extras?.httpStatus; this.retryAfterSeconds = extras?.retryAfterSeconds; this.elapsedMs = extras?.elapsedMs; this.path = extras?.path; this.errorName = extras?.errorName; } } export function engineFailureKind(error: unknown): EngineCallFailureKind | null { if (error instanceof RectificationEngineError && error.kind) return error.kind; if (error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) { return "timeout"; } if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) { return "timeout"; } return null; } export function parseEngineRetryAfterSeconds(value: string | null | undefined): number | null { if (!value) return null; const trimmed = value.trim(); if (/^\d+(?:\.\d+)?$/.test(trimmed)) { const seconds = Number(trimmed); return Number.isFinite(seconds) && seconds >= 0 ? seconds : null; } const at = Date.parse(trimmed); if (!Number.isFinite(at)) return null; const seconds = (at - Date.now()) / 1000; return seconds > 0 ? seconds : 0; } export function nextEngineBusyBackoffMs(input: { retryAfterSeconds?: number | null; retriesUsed: number; backoffUsedMs: number; }): number | null { if (input.retriesUsed >= RECTIFICATION_ENGINE_BUSY_RETRY_LIMIT) return null; const remaining = RECTIFICATION_ENGINE_BUSY_BACKOFF_BUDGET_MS - input.backoffUsedMs; if (remaining < 0) return null; const requested = Math.max(0, Math.round((input.retryAfterSeconds ?? 1) * 1000)); if (requested === 0) return 0; if (remaining <= 0) return null; return Math.min(requested, remaining); } const defaultEngineBusyWait = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); let engineBusyWait = defaultEngineBusyWait; export function setEngineBusyWaitForTests(wait: ((ms: number) => Promise) | null): void { engineBusyWait = wait ?? defaultEngineBusyWait; } function logEngineCallFailure(input: { path: string; kind: EngineCallFailureKind; elapsedMs: number; httpStatus?: number; errorName?: string; }): void { console.warn("rectification_engine_failed", { path: input.path, status: input.kind, httpStatus: input.httpStatus ?? null, errorName: input.errorName ?? null, elapsedMs: input.elapsedMs, }); } function persistenceCodeForKind(kind: EngineCallFailureKind): string { if (kind === "timeout") return "timeout"; if (kind === "bad_payload") return "engine_invalid_response"; return "engine_request_failed"; } 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 = CandidatePosition & Readonly<{ candidateId: string; time: string; rank: number; relativeSupport: number; tiedMinuteCount: number; signature?: AscendantSignature; rawScore?: number; clusterTimes?: readonly string[]; clusterStart?: string; clusterEnd?: string; }>; export type V9DecisionReceipt = Readonly>; export type V9ExecutionLedger = readonly Readonly>[]; export type V9EngineScoreResult = Readonly<{ engineResultId: string; algorithmVersion: string; eventContractVersion: string; policyVersion: string; candidateRange: DatedCandidateRange; candidates: readonly V9EngineCandidate[]; overallConfidence: "low" | "medium" | "high"; marginPercent: number | null; acceptanceAllowed: boolean; selectionAllowed: boolean; proposeAllowed: boolean; confirmationAllowed: boolean; representativeCandidateId: string | null; representativeTime: string | null; decisionReceipt: V9DecisionReceipt; executionLedger: V9ExecutionLedger; executedMethods: readonly PublicRectificationMethod[]; windowScan: WindowScan | null; }>; 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) && item.eventKind !== "horary_query") 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: evidenceSubjectForDomain(item.domain, item.subject), }]; }); } function engineBase(): string { return process.env.JYOTISH_API_BASE?.trim() || "http://127.0.0.1:5200"; } function retryAfterFromHeaders(headers: { get?(name: string): string | null } | undefined): number | null { if (!headers || typeof headers.get !== "function") return null; return parseEngineRetryAfterSeconds(headers.get("retry-after") ?? headers.get("Retry-After")); } /** Engine paths that recompare candidates against the chart (BUG-1047 stage). */ const RESCORING_ENGINE_PATHS = new Set([ "/api/rectification/v5/score", "/api/rectification/v5/block_scan", ]); async function readEngineJson(path: string, init: RequestInit, timeoutMs: number): Promise> { if (RESCORING_ENGINE_PATHS.has(path)) reportTurnProgress("rescoring"); const started = Date.now(); try { return await readEngineJsonTimed(path, init, timeoutMs, started); } catch (error) { recordEngineCallTiming({ path, ms: Date.now() - started, outcome: error instanceof RectificationEngineError && error.kind ? error.kind : "http_error", }); throw error; } } async function readEngineJsonTimed( path: string, init: RequestInit, timeoutMs: number, started: number, ): Promise> { try { const response = await fetch(`${engineBase()}${path}`, { ...init, signal: AbortSignal.timeout(timeoutMs), }); const elapsedMs = Date.now() - started; const data = await response.json().catch(() => null); if (!response.ok) { const errorText = data && typeof data === "object" ? String((data as Record).error ?? (data as Record).message ?? "") : ""; const errorCode = data && typeof data === "object" ? String((data as Record).error_code ?? "") : ""; const tooManyCandidates = response.status === 400 && (errorCode === "too_many_candidates" || errorText.includes("too_many_candidates")); const kind: EngineCallFailureKind = response.status === 429 ? "busy" : tooManyCandidates ? "bad_payload" : "http_error"; logEngineCallFailure({ path, kind, elapsedMs, httpStatus: response.status, }); const message = errorText || `Jyotish API ${response.status}`; throw new RectificationEngineError(tooManyCandidates ? "too_many_candidates" : persistenceCodeForKind(kind), message, { kind, httpStatus: response.status, retryAfterSeconds: retryAfterFromHeaders(response.headers) ?? undefined, elapsedMs, path, }); } if (!data || typeof data !== "object") { logEngineCallFailure({ path, kind: "bad_payload", elapsedMs, httpStatus: response.status, }); throw new RectificationEngineError("engine_invalid_response", "Jyotish API returned an invalid response", { kind: "bad_payload", httpStatus: response.status, elapsedMs, path, }); } recordEngineCallTiming({ path, ms: Date.now() - started, outcome: "ok" }); return data as Record; } catch (error) { if (error instanceof RectificationEngineError) throw error; const elapsedMs = Date.now() - started; const errorName = error instanceof Error ? error.name : "Error"; const kind: EngineCallFailureKind = errorName === "TimeoutError" || errorName === "AbortError" ? "timeout" : "http_error"; logEngineCallFailure({ path, kind, elapsedMs, errorName }); throw new RectificationEngineError(persistenceCodeForKind(kind), "Jyotish API request failed", { kind, elapsedMs, path, errorName, }); } } async function postEngine( path: string, body: unknown, timeoutMs = 60_000, options?: Readonly<{ busyRetry?: boolean }>, ): Promise> { const init: RequestInit = { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }; if (!options?.busyRetry) return readEngineJson(path, init, timeoutMs); let retriesUsed = 0; let backoffUsedMs = 0; while (true) { try { return await readEngineJson(path, init, timeoutMs); } catch (error) { if (!(error instanceof RectificationEngineError) || error.kind !== "busy") throw error; const waitMs = nextEngineBusyBackoffMs({ retryAfterSeconds: error.retryAfterSeconds, retriesUsed, backoffUsedMs, }); if (waitMs == null) throw error; if (waitMs > 0) await engineBusyWait(waitMs); retriesUsed += 1; backoffUsedMs += waitMs; } } } async function getEngine(path: string, timeoutMs = 5_000): Promise> { return readEngineJson(path, { method: "GET", headers: { accept: "application/json" }, }, timeoutMs); } /** Coarse product id is not a scoring identity; never treat it as live algorithm. */ const COARSE_ENGINE_PRODUCT_ID = "rectification-v5"; export type LiveEngineScoringIdentity = Readonly<{ algorithmVersion: string | null; policyVersion: string | null; }>; export function liveEngineScoringIdentityFromEnv( env: NodeJS.ProcessEnv | Record = process.env, ): LiveEngineScoringIdentity { const engine = env.RECTIFICATION_ENGINE_VERSION?.trim() || ""; const algorithm = env.RECTIFICATION_ALGORITHM_VERSION?.trim() || (engine && engine !== COARSE_ENGINE_PRODUCT_ID ? engine : ""); const policy = env.RECTIFICATION_DECISION_POLICY_VERSION?.trim() || ""; return { algorithmVersion: algorithm || null, policyVersion: policy || null, }; } export function scoringIdentityFromEnginePayload( payload: Readonly> | null | undefined, ): LiveEngineScoringIdentity { const algorithm = typeof payload?.algorithm_version === "string" ? payload.algorithm_version.trim() : ""; const policy = typeof payload?.decision_policy_version === "string" ? payload.decision_policy_version.trim() : ""; return { algorithmVersion: algorithm || null, policyVersion: policy || null, }; } export function scoringIdentityIsTrusted(identity: LiveEngineScoringIdentity): boolean { return Boolean(identity.algorithmVersion && identity.policyVersion); } export function scoringIdentityMatches( stored: { algorithmVersion?: string | null; policyVersion?: string | null } | null | undefined, live: LiveEngineScoringIdentity, ): boolean { return Boolean(stored && scoringIdentityIsTrusted(live) && stored.algorithmVersion === live.algorithmVersion && stored.policyVersion === live.policyVersion); } export function cachedEngineScoreIsReusable( stored: { evidenceLedgerFingerprint?: string | null; candidateRangeFingerprint?: string | null; algorithmVersion?: string | null; policyVersion?: string | null; } | null | undefined, fingerprints: { evidenceLedgerFingerprint: string; candidateRangeFingerprint: string }, live: LiveEngineScoringIdentity, ): boolean { if (!stored) return false; if (stored.evidenceLedgerFingerprint !== fingerprints.evidenceLedgerFingerprint) return false; if (stored.candidateRangeFingerprint !== fingerprints.candidateRangeFingerprint) return false; return scoringIdentityMatches(stored, live); } /** * In-process memo for `/v5/versions` when the deployment does not pin the * scoring identity in env (BUG-1047 D5). One turn asked the engine for its * versions several times. Only a complete identity pair is kept; failures and * partial payloads are always re-read. The memo is scoped to the transport * (`fetch` implementation) and engine base URL, never to a user or Case. */ export const RECTIFICATION_ENGINE_VERSIONS_CACHE_TTL_MS = 30_000; type VersionsMemo = { identity: LiveEngineScoringIdentity; expiresAt: number }; const versionsMemo = new WeakMap>(); async function readNativeScoringIdentity(): Promise { const transport = globalThis.fetch as unknown as object; const base = engineBase(); const now = Date.now(); const memo = versionsMemo.get(transport)?.get(base); if (memo && memo.expiresAt > now) return { ...memo.identity }; const native = scoringIdentityFromEnginePayload(await getEngine("/api/rectification/v5/versions")); if (scoringIdentityIsTrusted(native)) { let byBase = versionsMemo.get(transport); if (!byBase) { byBase = new Map(); versionsMemo.set(transport, byBase); } byBase.set(base, { identity: native, expiresAt: now + RECTIFICATION_ENGINE_VERSIONS_CACHE_TTL_MS }); } return native; } export async function readV9EngineScoringIdentity(): Promise { const fromEnv = liveEngineScoringIdentityFromEnv(); if (scoringIdentityIsTrusted(fromEnv)) return fromEnv; try { const native = await readNativeScoringIdentity(); // Partial overrides cannot invent the missing half of a computation identity. // A conflicting partial override is not a coherent identity pair. if ((fromEnv.algorithmVersion && fromEnv.algorithmVersion !== native.algorithmVersion) || (fromEnv.policyVersion && fromEnv.policyVersion !== native.policyVersion)) { return { algorithmVersion: null, policyVersion: null }; } return native; } catch { return { algorithmVersion: null, policyVersion: null }; } } function readClusterCoverage(row: Record | null): { clusterTimes?: readonly string[]; clusterStart?: string; clusterEnd?: string; } { if (!row) return {}; const times = Array.isArray(row.cluster_times) ? row.cluster_times.flatMap((item) => ( typeof item === "string" && timePattern.test(item) ? [item] : [] )) : []; const start = typeof row.cluster_start === "string" && timePattern.test(row.cluster_start) ? row.cluster_start : times[0]; const end = typeof row.cluster_end === "string" && timePattern.test(row.cluster_end) ? row.cluster_end : times[times.length - 1]; if (!start || !end) return {}; return { ...(times.length > 0 ? { clusterTimes: times } : {}), clusterStart: start, clusterEnd: end, }; } function readCandidates( value: unknown, range: DatedCandidateRange, dated = false, ): 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); const cluster = readClusterCoverage(row); const position = row ? readCandidatePosition(row) : null; if (dated && (!position || !range.candidate_intervals?.length || position.cluster_intervals!.some((part) => { const declared = range.candidate_intervals![part.segment_index]; return !declared || part.start_at < declared.start_at || part.end_at > declared.end_at || (Date.parse(`${part.start_at}:00Z`) - Date.parse(`${range.candidate_intervals![0]!.start_at}:00Z`)) / 60000 !== part.start_offset_minutes; }))) { throw new RectificationEngineError("engine_invalid_candidate_decisions", "dated candidate coverage is missing or invalid"); } candidates.push({ ...(position ?? {}), candidateId, time, rank, relativeSupport, tiedMinuteCount, ...cluster, }); } return candidates; } type ParsedReceipt = Readonly<{ raw: V9DecisionReceipt; eventContractVersion: string; policyVersion: string; acceptanceAllowed: boolean; selectionAllowed: boolean; proposeAllowed: 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.propose_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) || (row.propose_allowed === true && row.selection_allowed !== true) || !questionContractVersionIsCompatible(row.question_contract_version ?? row.question_contract) ) { return invalidReceipt(); } return { raw: row, eventContractVersion: EVENT_CONTRACT_VERSION, policyVersion: row.policy_version, acceptanceAllowed: row.acceptance_allowed, selectionAllowed: row.selection_allowed, proposeAllowed: row.propose_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]; } export function executedMethodsFromLedger( ledger: V9ExecutionLedger | null | undefined, ): PublicRectificationMethod[] { if (!ledger) return []; return executedMethods(ledger); } function engineDiagnostics(data: Record): Readonly> { return record(data.diagnostics) ?? {}; } export const ENGINE_ASKED_PROBE_KEY_MAX_LENGTH = 200; export function sanitizeAskedProbeKeysForEngine( keys: readonly string[] | null | undefined, ): string[] { const seen = new Set(); const next: string[] = []; for (const raw of keys ?? []) { const key = raw.trim(); if (!key) continue; if (key.length > ENGINE_ASKED_PROBE_KEY_MAX_LENGTH || key.includes(":varga.")) { console.warn( `[rectification-v9] dropping asked_probe_key length=${key.length} varga_hash=${key.includes(":varga.")}`, ); continue; } if (seen.has(key)) continue; seen.add(key); next.push(key); } return next; } /** * BUG-1178: the engine rejects the whole request when any event lies outside * [birth_date, today] (`events[i] dates must be between birth_date and today`). A typo'd future * year, or any 「今年 / 这个月」 event whose period ends after today, then failed every rescore * of the Case and the delivery card never came. Before sending: drop events that start after * the cap or end before birth; clip the rest into the window. The cap is yesterday (UTC) so the * engine's own clock can never be behind ours. */ export function boundEngineEventDates( events: readonly V9EngineEvent[], birthDate: string, now: Date = new Date(), ): V9EngineEvent[] { const cap = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10); const floor = /^\d{4}-\d{2}-\d{2}$/.test(birthDate) ? birthDate : null; return events.flatMap((event): V9EngineEvent[] => { if (event.date_start > cap) return []; if (floor && event.date_end < floor) return []; const start = floor && event.date_start < floor ? floor : event.date_start; const end = event.date_end > cap ? cap : event.date_end; return start === event.date_start && end === event.date_end ? [event] : [{ ...event, date_start: start, date_end: end }]; }); } export function engineRequestBody(input: { baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange; events: readonly V9EngineEvent[]; askedProbeKeys?: readonly string[]; declinedDomains?: readonly string[]; columnTimes?: readonly string[]; refreshProbes?: boolean; }): 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"); } const events = boundEngineEventDates(input.events, birthDate); if (events.length === 0) { throw new RectificationEngineError("no_scorable_evidence", "no scorable evidence for the engine"); } const askedProbeKeys = sanitizeAskedProbeKeysForEngine(input.askedProbeKeys); const columnTimes = [...new Set( (input.columnTimes ?? []) .map((value) => value.slice(0, 5)) .filter((value) => timePattern.test(value)), )].slice(0, 64); return { birth_date: birthDate, candidate_intervals: requestCandidateIntervals(snapshot, input.candidateRange), start_time: input.candidateRange.start_time, end_time: input.candidateRange.end_time, lat, lon, tz, ayanamsa: resolveAyanamsa(snapshot), node_mode: "mean", 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, ...(askedProbeKeys.length ? { asked_probe_keys: askedProbeKeys } : {}), ...(sanitizeDeclinedDomainsForEngine(input.declinedDomains).length ? { declined_domains: sanitizeDeclinedDomainsForEngine(input.declinedDomains) } : {}), ...(columnTimes.length ? { column_times: columnTimes } : {}), ...(input.refreshProbes === true ? { refresh_probes: true } : {}), }; } const ENGINE_COLLECT_DOMAINS = new Set([ "education", "career", "relocation", "relationship", "family", "finance", "health_pressure", ]); export function sanitizeDeclinedDomainsForEngine( domains: readonly string[] | null | undefined, ): string[] { const seen = new Set(); const next: string[] = []; for (const raw of domains ?? []) { const domain = raw.trim(); if (!ENGINE_COLLECT_DOMAINS.has(domain) || seen.has(domain)) continue; seen.add(domain); next.push(domain); } return next; } export type V9VedastroValidateResult = Readonly<{ status: "passed" | "failed" | "not_evaluated"; canConfirmExactMinute: boolean; minuteSensitiveStatus: "passed" | "failed" | "not_evaluated"; searchEventsSupportsLocalWinner: boolean; failure: Readonly<{ code: "timeout" | "engine_request_failed" | "engine_invalid_response" | "unknown"; }> | null; raw: Readonly>; }>; function vedastroValidateStatus(value: unknown): "passed" | "failed" | "not_evaluated" { if (value === "pass" || value === "passed") return "passed"; if (value === "fail" || value === "failed") return "failed"; return "not_evaluated"; } export function mergeVedastroValidateIntoReceipt( receipt: Readonly>, validation: V9VedastroValidateResult, ): Record { const next = { ...receipt }; const gates = record(next.gates) ? { ...record(next.gates)! } : {}; const exact = record(gates.exact_confirmation) ? { ...record(gates.exact_confirmation)! } : {}; exact.vedastro_event_validation = { status: validation.status, search_events_primary_supports_local_winner: validation.searchEventsSupportsLocalWinner, can_confirm_exact_minute: validation.canConfirmExactMinute, failure: validation.failure, }; const minuteSensitiveStatus = validation.status === "passed" ? validation.minuteSensitiveStatus : validation.status === "failed" ? "failed" : "not_evaluated"; if ( (minuteSensitiveStatus === "passed" || minuteSensitiveStatus === "failed") && (exact.external_validation_status === "not_evaluated" || exact.external_validation_status == null) ) { exact.external_validation_status = minuteSensitiveStatus; } gates.exact_confirmation = exact; next.gates = gates; next.unique_minute_claim = false; return next; } export async function runV9VedastroValidate(input: { baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange; events: readonly V9EngineEvent[]; candidateTimes: readonly [string, string]; }): Promise { const unevaluated = (): V9VedastroValidateResult => ({ status: "not_evaluated", canConfirmExactMinute: false, minuteSensitiveStatus: "not_evaluated", searchEventsSupportsLocalWinner: false, failure: null, raw: { status: "not_evaluated", can_confirm_exact_minute: false }, }); if (input.candidateTimes[0] === input.candidateTimes[1]) return unevaluated(); try { const data = await postEngine( "/api/rectification/v5/vedastro-validate", { ...engineRequestBody(input), candidate_times: [...input.candidateTimes] }, 12_000, ); const eventValidation = record(data.event_validation); const minuteValidation = record(data.minute_sensitive_validation); const status = vedastroValidateStatus(data.status); const minuteSensitiveStatus = status === "passed" ? vedastroValidateStatus(minuteValidation?.status) : status === "failed" ? "failed" : "not_evaluated"; return { status, canConfirmExactMinute: data.can_confirm_exact_minute === true && status === "passed" && minuteSensitiveStatus === "passed", minuteSensitiveStatus, searchEventsSupportsLocalWinner: eventValidation?.search_events_primary_supports_local_winner === true, failure: null, raw: data, }; } catch (error) { const kind = engineFailureKind(error); const code = kind === "timeout" || (error instanceof RectificationEngineError && error.code === "timeout") ? "timeout" : error instanceof RectificationEngineError && (error.kind === "bad_payload" || error.code === "engine_invalid_response") ? "engine_invalid_response" : error instanceof RectificationEngineError ? "engine_request_failed" : error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError") ? "timeout" : "unknown"; return { status: "failed", canConfirmExactMinute: false, minuteSensitiveStatus: "failed", searchEventsSupportsLocalWinner: false, failure: { code }, raw: { status: "failed", failure: { code }, can_confirm_exact_minute: false }, }; } } export async function runV9CandidateScore(input: { baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange; events: readonly V9EngineEvent[]; askedProbeKeys?: readonly string[]; declinedDomains?: readonly string[]; columnTimes?: readonly string[]; refreshProbes?: boolean; }): Promise { const data = await postEngine("/api/rectification/v5/score", engineRequestBody(input), 60_000, { busyRetry: true }); const datedRange = { ...input.candidateRange, candidate_intervals: requestCandidateIntervals(input.baselineBirthSnapshot, input.candidateRange) }; const dated = isDatedScoringAlgorithmVersion(data.algorithm_version); const metadata = candidateSegmentMetadata(data); const candidates = readCandidates(data.candidate_decisions, datedRange, dated).map((candidate) => ({ ...candidate, ...metadata.byTime.get(candidate.time) })); 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, acceptanceAllowed: receipt.acceptanceAllowed, selectionAllowed: receipt.selectionAllowed, proposeAllowed: receipt.proposeAllowed, confirmationAllowed: receipt.confirmationAllowed, representativeCandidateId: receipt.representativeCandidateId, representativeTime: receipt.representativeTime, decisionReceipt: dated ? { ...receipt.raw, candidate_window_contract: "dated-v1", declared_birth_date: String(input.baselineBirthSnapshot.birth_date), candidate_intervals: datedRange.candidate_intervals, candidate_timezone_offset: Number(input.baselineBirthSnapshot.timezone_offset), candidate_timezone_id: String(input.baselineBirthSnapshot.timezone_id ?? "") } : receipt.raw, executionLedger: ledger, executedMethods: executedMethods(ledger), windowScan: parseWindowScan(engineDiagnostics(data).window_scan), }; } export async function runV9Diagnostics(input: { baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange; events: readonly V9EngineEvent[]; askedProbeKeys?: readonly string[]; }): Promise { const data = await postEngine("/api/rectification/v5/diagnostics", engineRequestBody(input)); const datedRange = { ...input.candidateRange, candidate_intervals: requestCandidateIntervals(input.baselineBirthSnapshot, input.candidateRange) }; const dated = isDatedScoringAlgorithmVersion(data.algorithm_version); const metadata = candidateSegmentMetadata(data); const candidates = readCandidates(data.candidate_decisions, datedRange, dated).map((candidate) => ({ ...candidate, ...metadata.byTime.get(candidate.time) })); 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, window_scan: parseWindowScan(diagnostics.window_scan), }, missingLayers, canConfirmExactMinute: receipt.confirmationAllowed, decisionReceipt: dated ? { ...receipt.raw, candidate_window_contract: "dated-v1", declared_birth_date: String(input.baselineBirthSnapshot.birth_date), candidate_intervals: datedRange.candidate_intervals, candidate_timezone_offset: Number(input.baselineBirthSnapshot.timezone_offset), candidate_timezone_id: String(input.baselineBirthSnapshot.timezone_id ?? "") } : receipt.raw, executionLedger: ledger, executedMethods: executedMethods(ledger), }; } // Keep the receipt default aligned with Python's ALGORITHM_VERSION; persisted // receipts retain their original identity (this is not a Skill/open gate). export const v9EngineVersion = (): string => process.env.RECTIFICATION_ENGINE_VERSION?.trim() || "rectification-v5-matrix-scoring-12"; export type V9RangeReading = Readonly<{ stableThemes: readonly string[]; sensitiveThemes: readonly string[]; claimBoundary: string | null; themeSensitivity: Readonly>; window: Readonly> | null; }>; export async function runV9RangeReading(input: { baselineBirthSnapshot: Readonly>; candidateRange: { start_time: string; end_time: string; representative_time?: string }; representativeTime?: string | null; birthTimeAccuracy?: "provisional" | "approximate"; }): Promise { try { 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) return null; const representative = input.representativeTime ?? input.candidateRange.representative_time ?? input.candidateRange.start_time; const data = await postEngine("/api/rectification/v5/range_reading", { birth_date: birthDate, lat, lon, tz, ayanamsa: resolveAyanamsa(snapshot), node_mode: "mean", birth_time_accuracy: input.birthTimeAccuracy ?? "provisional", representative_time: representative, candidate_range: { start_time: input.candidateRange.start_time, end_time: input.candidateRange.end_time, representative_time: representative, }, }, 8_000); const stable = Array.isArray(data.stable_themes) ? data.stable_themes.filter((item): item is string => typeof item === "string") : []; const sensitive = Array.isArray(data.sensitive_themes) ? data.sensitive_themes.filter((item): item is string => typeof item === "string") : []; return { stableThemes: stable, sensitiveThemes: sensitive, claimBoundary: typeof data.claim_boundary === "string" ? data.claim_boundary : null, themeSensitivity: record(data.theme_sensitivity) ?? {}, window: record(data.window), }; } catch { return null; } } export type V9BlockScanResult = Readonly<{ resultId: string | null; algorithmVersion: string; policyVersion: string | null; minuteStep: number; blocks: readonly BlockScanBlock[]; }>; export async function runV9BlockScan(input: { baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange; events: readonly V9EngineEvent[]; blocks?: readonly { period: string; start_time: string; end_time: string }[]; minuteStep?: number; }): Promise { const data = await postEngine("/api/rectification/v5/block_scan", { ...engineRequestBody(input), minute_step: input.minuteStep ?? 10, ...(input.blocks?.length ? { blocks: input.blocks } : {}), }, 60_000, { busyRetry: true }); const parsed = parseBlockScanPayload({ blocks: data.blocks, algorithm_version: data.algorithm_version, minute_step: data.minute_step, evidence_ledger_fingerprint: data.evidence_ledger_fingerprint, }); if (!parsed) { throw new RectificationEngineError("engine_invalid_response", "block_scan payload is invalid"); } return { resultId: typeof data.result_id === "string" ? data.result_id : null, algorithmVersion: parsed.algorithmVersion ?? String(data.algorithm_version ?? ""), policyVersion: scoringIdentityFromEnginePayload(record(data.decision_receipt)).policyVersion, minuteStep: parsed.minuteStep ?? 10, blocks: parsed.blocks, }; }