import type { DashaAgreement, EventDashaLedgerRow, LagnaContrast, NakshatraBoundary, OosBlindPrompt, PrecisionStage, WindowScanTransition, } from "./rectification-agentic/v9/refinement-packet"; import { parseWindowScanTransitions, refinementFromDecisionReceipt, } from "./rectification-agentic/v9/refinement-packet"; import { parseWindowScan } from "./rectification-agentic/v9/varga-observations"; import type { TechniqueAuditRow } from "./consultation-agent-events"; import { normalizeTechniqueAuditRows } from "./consultation-technique-audit"; import { buildConfirmationGate, type ConfirmationGate, } from "./rectification-agentic/v9/confirmation-gate"; import { MIN_SEPARATION_LEAD } from "./rectification-agentic/core/candidate-separation"; export type RectificationCandidate = Readonly<{ candidateId: string; rank: number; time: string; relativeSupport: number; tiedMinuteCount: number; }>; export type RectificationHouseRow = Readonly<{ house: number; sign: string; occupants: readonly string[]; }>; export type RectificationHouseTable = Readonly<{ time: string; lagna: string; houses: readonly RectificationHouseRow[]; }>; export type RectificationNatalRecast = Readonly<{ time: string; lagna: string; user_meaning: string; unique_minute_claim: false; confirmation_allowed: false; }>; export type RectificationCandidateResult = Readonly<{ resultId: string; candidates: readonly RectificationCandidate[]; overallConfidence: "low" | "medium" | "high"; selectionAllowed: boolean; canAdopt: boolean; confirmationAllowed: boolean; representativeTime: string | null; selectedTime: string | null; selectionKind: string | null; houseTable: RectificationHouseTable | null; houseTablesByTime: Readonly>; natalRecast: RectificationNatalRecast | null; techniqueAudit: readonly TechniqueAuditRow[]; windowTransitions: readonly WindowScanTransition[]; eventDashaLedger: readonly EventDashaLedgerRow[]; dashaAgreement: DashaAgreement | null; lagnaContrast: LagnaContrast | null; nakshatraBoundary: NakshatraBoundary | null; precisionStage: PrecisionStage | null; oosBlindPrompts: readonly OosBlindPrompt[]; confirmationGate: ConfirmationGate; validated: boolean; completionStatus: "provisional_range_user_stopped" | "validated_range" | "exact_minute_confirmed" | null; }>; function record(value: unknown): Record | null { return value && typeof value === "object" ? value as Record : null; } function time(value: unknown): string | null { if (typeof value !== "string") return null; const normalized = value.slice(0, 5); return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(normalized) ? normalized : null; } function text(value: unknown): string | null { return typeof value === "string" ? value : null; } function finiteNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } 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; const deniedHouseTable = /lon|latitude|longitude|degree|score|fingerprint/i; export function parseRectificationHouseTable(value: unknown): RectificationHouseTable | null { const row = record(value); const tableTime = time(row?.time); const lagna = text(row?.lagna)?.trim() ?? ""; if (!row || !tableTime || !lagna || !Array.isArray(row.houses) || row.houses.length !== 12) return null; const houses: RectificationHouseRow[] = []; for (let index = 0; index < 12; index += 1) { const house = record(row.houses[index]); const houseNumber = finiteNumber(house?.house); const sign = text(house?.sign)?.trim() ?? ""; if ( !house || houseNumber !== index + 1 || !sign || deniedHouseTable.test(sign) || !Array.isArray(house.occupants) ) return null; const occupants: string[] = []; for (const occupant of house.occupants) { const label = text(occupant)?.trim() ?? ""; if (!label) continue; if (deniedHouseTable.test(label)) return null; occupants.push(label); } houses.push({ house: houseNumber, sign, occupants }); } return { time: tableTime, lagna, houses }; } function houseTablesByTimeFromSnapshot( snapshot: Record, receipt: Record | null, ): Record { const raw = record(snapshot.houseTablesByTime) ?? record(snapshot.house_tables_by_time) ?? record(receipt?.house_tables_by_time); const tables: Record = {}; if (!raw) return tables; for (const [key, value] of Object.entries(raw)) { const clock = time(key); const table = parseRectificationHouseTable(value); if (!clock || !table) continue; tables[clock] = table; } return tables; } function natalRecastFromSnapshot( snapshot: Record, receipt: Record | null, table: RectificationHouseTable | null, wantedMinute: string | null, ): RectificationNatalRecast | null { const raw = record(snapshot.natalRecast) ?? record(snapshot.natal_recast) ?? record(receipt?.natal_recast); const recastTime = table?.time ?? time(raw?.time); const lagna = table?.lagna || (typeof raw?.lagna === "string" ? raw.lagna.trim() : "") || ""; if (!recastTime || !lagna || deniedHouseTable.test(lagna)) return null; if (wantedMinute && recastTime !== wantedMinute) return null; const meaning = natalRecastMeaning({ time: recastTime, lagna }) ?? (typeof raw?.user_meaning === "string" && raw.user_meaning.trim() && !deniedHouseTable.test(raw.user_meaning) ? raw.user_meaning.trim().slice(0, 220) : null); if (!meaning) return null; return { time: recastTime, lagna, user_meaning: meaning, unique_minute_claim: false, confirmation_allowed: false, }; } export function natalRecastMeaning(table: Pick | null): string | null { if (!table) return null; return `本命宫位已按 ${table.time} 重算(上升 ${table.lagna})。下面是本轮实际执行的技法,不能当作唯一分钟确认。`; } export function houseTableAlignedToMinute( wanted: string | null, tablesByTime: Readonly>, fallback: RectificationHouseTable | null, ): RectificationHouseTable | null { if (wanted && tablesByTime[wanted]?.time === wanted) return tablesByTime[wanted]; if (fallback && wanted && fallback.time === wanted) return fallback; if (!wanted) return fallback; return null; } export function publicChartForMinute( wanted: string | null, snapshot: Record, receipt: Record | null, ): { houseTable: RectificationHouseTable | null; natalRecast: RectificationNatalRecast | null; } { const tablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt); const fallback = parseRectificationHouseTable(snapshot.houseTable) ?? parseRectificationHouseTable(snapshot.house_table) ?? parseRectificationHouseTable(receipt?.house_table); const houseTable = houseTableAlignedToMinute(wanted, tablesByTime, fallback); return { houseTable, natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, wanted), }; } export function workingRectificationHouseTable( result: Pick, ): RectificationHouseTable | null { const wanted = result.selectedTime ?? result.representativeTime; return houseTableAlignedToMinute(wanted, result.houseTablesByTime, result.houseTable); } function techniqueAuditFromSnapshot( snapshot: Record, receipt: Record | null, ): readonly TechniqueAuditRow[] { return normalizeTechniqueAuditRows(snapshot.techniqueAudit) ?? normalizeTechniqueAuditRows(snapshot.technique_audit_table) ?? normalizeTechniqueAuditRows(receipt?.technique_audit_table) ?? []; } function receiptFromSnapshot(snapshot: Record): Record | null { return record(snapshot.decisionReceipt) ?? record(snapshot.decision_receipt); } export function parseRectificationCandidateResult(value: unknown): RectificationCandidateResult | null { const snapshot = record(value); if (!snapshot || typeof snapshot.resultId !== "string") return null; if (!Array.isArray(snapshot.candidates) || snapshot.candidates.length === 0) return null; const candidates: RectificationCandidate[] = []; for (const value of snapshot.candidates) { const candidate = record(value); const candidateId = candidate?.candidateId; const candidateTime = time(candidate?.time); const rank = finiteNumber(candidate?.rank); const relativeSupport = finiteNumber(candidate?.relativeSupport); const tiedMinuteCount = finiteNumber(candidate?.tiedMinuteCount); if ( !candidate || typeof candidateId !== "string" || !uuidPattern.test(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; candidates.push({ candidateId, rank, time: candidateTime, relativeSupport, tiedMinuteCount }); } const receipt = receiptFromSnapshot(snapshot); const refinement = refinementFromDecisionReceipt(receipt); const windowScan = parseWindowScan(receipt?.window_scan) ?? parseWindowScan(snapshot.window_scan); const selectedTime = time(snapshot.selectedTime); const representativeTime = time(snapshot.representativeTime); const houseTablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt); const fallbackHouseTable = parseRectificationHouseTable(snapshot.houseTable) ?? parseRectificationHouseTable(snapshot.house_table) ?? parseRectificationHouseTable(receipt?.house_table); const houseTable = houseTableAlignedToMinute( selectedTime ?? representativeTime, houseTablesByTime, fallbackHouseTable, ); return { resultId: snapshot.resultId, candidates, overallConfidence: snapshot.overallConfidence === "high" || snapshot.overallConfidence === "medium" ? snapshot.overallConfidence : "low", selectionAllowed: snapshot.selectionAllowed === true || snapshot.selection_allowed === true, canAdopt: snapshot.canAdopt === true || snapshot.can_adopt === true, confirmationAllowed: snapshot.confirmationAllowed === true, representativeTime, selectedTime, selectionKind: text(snapshot.selectionKind), houseTable, houseTablesByTime, natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, selectedTime ?? representativeTime), techniqueAudit: techniqueAuditFromSnapshot(snapshot, receipt), windowTransitions: windowScan?.transitions ?? parseWindowScanTransitions(snapshot.window_scan), eventDashaLedger: refinement.event_dasha_ledger, dashaAgreement: refinement.dasha_agreement, lagnaContrast: refinement.lagna_contrast, nakshatraBoundary: refinement.nakshatra_boundary, precisionStage: refinement.precision_stage, oosBlindPrompts: refinement.oos_blind_prompts, confirmationGate: buildConfirmationGate({ engineConfirmationAllowed: snapshot.confirmationAllowed === true, candidates, decisionReceipt: receipt, }), validated: snapshot.validated === true, completionStatus: snapshot.completionStatus === "provisional_range_user_stopped" || snapshot.completionStatus === "validated_range" || snapshot.completionStatus === "exact_minute_confirmed" || snapshot.completion_status === "provisional_range_user_stopped" || snapshot.completion_status === "validated_range" || snapshot.completion_status === "exact_minute_confirmed" ? (snapshot.completionStatus ?? snapshot.completion_status) as RectificationCandidateResult["completionStatus"] : null, }; } export function isRecommendedRectificationCandidate( result: RectificationCandidateResult, candidate: RectificationCandidate, ): boolean { if (result.selectedTime || !result.selectionAllowed || result.representativeTime !== candidate.time) { return false; } const ranked = [...result.candidates].sort((left, right) => right.relativeSupport - left.relativeSupport); const lead = (ranked[0]?.relativeSupport ?? 0) - (ranked[1]?.relativeSupport ?? 0); return lead >= MIN_SEPARATION_LEAD; }