/** * Public-safe P0/P1 refinement fields from the decision receipt. * * Scores, weights and event IDs stay out of public copy. D9/D10 sign names * may appear for the skill type-table verification report. D1 sign names may * appear on lagna contrast, matching the natal house table. */ const TIME = /^(?:[01]\d|2[0-3]):[0-5]\d$/; const UUID = /^[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 DENIED = /lon|latitude|longitude|degree|score|weight|fingerprint|\bpoints\b/i; const LAYER_LABEL: Record = { d1: "本命上升", d9: "D9", d10: "D10", d4: "D4", d5: "D5", d7: "D7", d12: "D12", d24: "D24", d2: "D2", d11: "D11", d30: "D30", pada: "Nakshatra pada", hora: "Hora Lagna", ghati: "Ghati Lagna", bhava: "Bhava Lagna", pranapada: "Pranapada Lagna", kp1: "KP 1宫子主", kp4: "KP 4宫子主", kp7: "KP 7宫子主", kp10: "KP 10宫子主", }; export const WINDOW_SCAN_SCORING_LAYER_ORDER = [ "d1", "d9", "d10", "d4", "d5", "d7", "d12", "d24", "d2", "d11", "d30", ] as const satisfies readonly WindowScanLayer[]; export const WINDOW_SCAN_DISPLAY_LAYER_ORDER = [ "pada", "hora", "ghati", "bhava", "pranapada", "kp1", "kp4", "kp7", "kp10", ] as const satisfies readonly WindowScanLayer[]; export function windowScanLayerLabel(layer: WindowScanLayer): string { return LAYER_LABEL[layer]; } export function isWindowScanDisplayLayer(layer: WindowScanLayer): boolean { return (WINDOW_SCAN_DISPLAY_LAYER_ORDER as readonly WindowScanLayer[]).includes(layer); } export type WindowScanLayer = | "d1" | "d9" | "d10" | "d4" | "d5" | "d7" | "d12" | "d24" | "d2" | "d11" | "d30" | "pada" | "hora" | "ghati" | "bhava" | "pranapada" | "kp1" | "kp4" | "kp7" | "kp10"; export type WindowScanTransition = Readonly<{ layer: WindowScanLayer; at: string; user_meaning: string; }>; export type EventDashaLedgerRow = Readonly<{ summary: string; match: "strong" | "medium" | "weak" | "none"; match_label: string; user_meaning: string; gochara: "activated" | "not_seen" | null; }>; export type DashaAgreement = Readonly<{ status: "agree" | "conflict" | "partial" | "unavailable"; vimshottari_top: string | null; narayana_top: string | null; user_meaning: string; }>; export type LagnaContrastInterval = Readonly<{ start: string; end: string; lagna: string; lords: Readonly<{ l1: string | null; l4: string | null; l7: string | null; l10: string | null }>; }>; export type LagnaContrast = Readonly<{ intervals: readonly LagnaContrastInterval[]; user_meaning: string; unique_minute_claim: false; }>; export type NakshatraOption = Readonly<{ key: "A" | "B"; time_bias: "earlier" | "later"; traits: readonly string[]; }>; export type NakshatraBoundary = Readonly<{ near_boundary: boolean; user_meaning: string | null; options: readonly NakshatraOption[]; }>; export type PrecisionStageId = | "collect_events" | "lagna_frame" | "d9_refine" | "d10_refine" | "d4_refine" | "d5_refine" | "theme_refine" | "ready_to_adopt"; export type PrecisionStage = Readonly<{ current: PrecisionStageId; can_stop: boolean; user_meaning: string; unique_minute_claim: false; }>; export type OosBlindPrompt = Readonly<{ domain: string; user_meaning: string; used_for_scoring: false; }>; export const EVENT_PROBE_DOMAINS = [ "education", "relocation", "relationship", "career", "family", "finance", "health_pressure", ] as const; export type EventProbeDomain = (typeof EVENT_PROBE_DOMAINS)[number]; export type EventProbeSource = | "dasha_activation" | "dasha_boundary" | "age_band" | "known_event_quality"; export type EventProbeRole = "distinguish" | "reverse_verify"; export type DiscriminatingEventProbe = Readonly<{ year: number; year_label: string; domain: EventProbeDomain; event_family: string; source: EventProbeSource; tracks: readonly ("vimshottari" | "narayana")[]; tracks_agree: boolean; unique_minute_claim: false; user_meaning: string; role: EventProbeRole; }>; function asRecord(value: unknown): Readonly> | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Readonly> : null; } function asTime(value: unknown): string | null { if (typeof value !== "string") return null; const normalized = value.slice(0, 5); return TIME.test(normalized) ? normalized : null; } function asText(value: unknown, maximum = 160): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed || trimmed.length > maximum || DENIED.test(trimmed) || UUID.test(trimmed)) return null; return trimmed; } export function parseWindowScanTransitions(value: unknown): readonly WindowScanTransition[] { if (!Array.isArray(value)) return []; const rows: WindowScanTransition[] = []; const seen = new Set(); for (const item of value) { const row = asRecord(item); const layer = row?.layer; const at = asTime(row?.at); if ( !row || ( layer !== "d1" && layer !== "d9" && layer !== "d10" && layer !== "d4" && layer !== "d5" && layer !== "d7" && layer !== "d12" && layer !== "d24" && layer !== "d2" && layer !== "d11" && layer !== "d30" && layer !== "pada" && layer !== "hora" && layer !== "ghati" && layer !== "bhava" && layer !== "pranapada" && layer !== "kp1" && layer !== "kp4" && layer !== "kp7" && layer !== "kp10" ) || !at ) continue; const key = `${layer}:${at}`; if (seen.has(key)) continue; seen.add(key); rows.push({ layer, at, user_meaning: `${LAYER_LABEL[layer]} 在 ${at} 发生变化`, }); } return rows; } export function parseEventDashaLedger(value: unknown): readonly EventDashaLedgerRow[] { if (!Array.isArray(value)) return []; const rows: EventDashaLedgerRow[] = []; for (const item of value) { const row = asRecord(item); const match = row?.match; if ( !row || (match !== "strong" && match !== "medium" && match !== "weak" && match !== "none") ) continue; const summary = asText(row.summary, 80); if (!summary) continue; const matchLabel = match === "strong" ? "强相关" : match === "medium" ? "有关联" : match === "weak" ? "弱关联" : "未见对应"; const gochara = row.gochara === "activated" || row.gochara === "not_seen" ? row.gochara : null; const gocharaNote = gochara === "activated" ? ";Gochara 激活相关宫" : gochara === "not_seen" ? ";Gochara 未见对应" : ""; rows.push({ summary, match, match_label: matchLabel, user_meaning: `${summary}:${matchLabel}${gocharaNote}`, gochara, }); } return rows; } export function parseDashaAgreement(value: unknown): DashaAgreement | null { const row = asRecord(value); const status = row?.status; if ( !row || (status !== "agree" && status !== "conflict" && status !== "partial" && status !== "unavailable") ) return null; const meaning = asText(row.user_meaning, 220) ?? (status === "conflict" ? "主限和分盘大运偏向不同时间。冲突时不能按更高把握收口。" : status === "agree" ? "主限和分盘大运都更支持同一段代表性时间。这仍不是唯一分钟确认。" : "还没有足够的大运对照。"); return { status, vimshottari_top: asTime(row.vimshottari_top), narayana_top: asTime(row.narayana_top), user_meaning: meaning, }; } function parseLords(value: unknown): LagnaContrastInterval["lords"] | null { const row = asRecord(value); if (!row) return null; const read = (key: "l1" | "l4" | "l7" | "l10"): string | null => { const label = asText(row[key], 12); return label; }; return { l1: read("l1"), l4: read("l4"), l7: read("l7"), l10: read("l10") }; } export function parseLagnaContrast(value: unknown): LagnaContrast | null { const row = asRecord(value); if (!row || !Array.isArray(row.intervals) || row.intervals.length < 2) return null; const intervals: LagnaContrastInterval[] = []; for (const item of row.intervals.slice(0, 3)) { const interval = asRecord(item); const start = asTime(interval?.start); const end = asTime(interval?.end); const lagna = asText(interval?.lagna, 12); const lords = parseLords(interval?.lords); if (!interval || !start || !end || !lagna || !lords) return null; intervals.push({ start, end, lagna, lords }); } if (intervals.length < 2) return null; const meaning = asText(row.user_meaning, 220) ?? `窗口里出现两段本命上升:${intervals[0].start}-${intervals[0].end} 为${intervals[0].lagna},${intervals[1].start}-${intervals[1].end} 为${intervals[1].lagna}。`; return { intervals, user_meaning: meaning, unique_minute_claim: false, }; } export function parseNakshatraBoundary(value: unknown): NakshatraBoundary | null { const row = asRecord(value); if (!row || typeof row.near_boundary !== "boolean") return null; if (!row.near_boundary) { return { near_boundary: false, user_meaning: null, options: [] }; } const options: NakshatraOption[] = []; if (Array.isArray(row.options)) { for (const item of row.options) { const option = asRecord(item); const key = option?.key; const bias = option?.time_bias; if ( !option || (key !== "A" && key !== "B") || (bias !== "earlier" && bias !== "later") || !Array.isArray(option.traits) ) continue; const traits = option.traits .map((trait) => asText(trait, 40)) .filter((trait): trait is string => Boolean(trait)) .slice(0, 2); if (traits.length === 0) continue; options.push({ key, time_bias: bias, traits }); } } if (options.length < 2) return { near_boundary: false, user_meaning: null, options: [] }; return { near_boundary: true, user_meaning: asText(row.user_meaning, 400) ?? "升点靠近两段日常节奏的交界。哪一组更像你近年的处事方式?这只用来偏置时间窗,不能确认唯一分钟。", options: options.slice(0, 2), }; } export function parsePrecisionStage(value: unknown): PrecisionStage | null { const row = asRecord(value); const current = row?.current; if ( !row || ( current !== "collect_events" && current !== "lagna_frame" && current !== "d9_refine" && current !== "d10_refine" && current !== "d4_refine" && current !== "d5_refine" && current !== "theme_refine" && current !== "ready_to_adopt" ) ) return null; const meaning = asText(row.user_meaning, 220) ?? "继续用带日期的经历缩小窗口。"; return { current, can_stop: row.can_stop === true || current === "ready_to_adopt", user_meaning: meaning, unique_minute_claim: false, }; } const EVENT_PROBE_DOMAIN_SET = new Set(EVENT_PROBE_DOMAINS); const EVENT_PROBE_SOURCES = new Set([ "dasha_activation", "dasha_boundary", "age_band", "known_event_quality", ]); const CLOCK_IN_COPY = /(?:[01]?\d|2[0-3]):[0-5]\d/; export function parseDiscriminatingEventProbes(value: unknown): readonly DiscriminatingEventProbe[] { if (!Array.isArray(value)) return []; const rows: DiscriminatingEventProbe[] = []; for (const item of value) { const row = asRecord(item); const year = typeof row?.year === "number" && Number.isInteger(row.year) ? row.year : null; const domain = typeof row?.domain === "string" ? row.domain : ""; const source = typeof row?.source === "string" ? row.source : ""; const family = asText(row?.event_family, 80); const meaning = asText(row?.user_meaning, 160); const label = asText(row?.year_label, 40); if ( !row || year === null || year < 1900 || year > 2100 || !EVENT_PROBE_DOMAIN_SET.has(domain) || !EVENT_PROBE_SOURCES.has(source) || !family || !meaning || !label || CLOCK_IN_COPY.test(meaning) || CLOCK_IN_COPY.test(family) || row.unique_minute_claim === true ) continue; const tracks = Array.isArray(row.tracks) ? row.tracks.filter((track): track is "vimshottari" | "narayana" => track === "vimshottari" || track === "narayana") : []; rows.push({ year, year_label: label, domain: domain as EventProbeDomain, event_family: family, source: source as EventProbeSource, tracks, tracks_agree: row.tracks_agree === true, unique_minute_claim: false, user_meaning: meaning, role: row.role === "distinguish" || source === "known_event_quality" ? "distinguish" : "reverse_verify", }); if (rows.length === 3) break; } return rows; } export function parseOosBlindPrompts(value: unknown): readonly OosBlindPrompt[] { if (!Array.isArray(value)) return []; const rows: OosBlindPrompt[] = []; const allowed = new Set(["relationship", "career", "family", "education", "finance", "health_pressure"]); for (const item of value) { const row = asRecord(item); const domain = typeof row?.domain === "string" ? row.domain : ""; const meaning = asText(row?.user_meaning, 180); if (!row || !allowed.has(domain) || !meaning) continue; rows.push({ domain, user_meaning: meaning, used_for_scoring: false }); if (rows.length === 3) break; } return rows; } export type EventFitRate = Readonly<{ matched: number; total: number; percent: number | null; label: string; user_meaning: string; unique_minute_claim: false; }>; export function parseEventFitRate(value: unknown): EventFitRate | null { const row = asRecord(value); if (!row) return null; const matched = typeof row.matched === "number" && Number.isInteger(row.matched) ? row.matched : null; const total = typeof row.total === "number" && Number.isInteger(row.total) ? row.total : null; if (matched === null || total === null) return null; const percent = typeof row.percent === "number" && Number.isFinite(row.percent) ? row.percent : null; return { matched, total, percent, label: asText(row.label, 80) ?? "事件吻合率", user_meaning: asText(row.user_meaning, 280) ?? "这是相对拟合,不是已确认唯一出生分钟。", unique_minute_claim: false, }; } export function refinementFromDecisionReceipt( receipt: Readonly> | null | undefined, ): { event_dasha_ledger: readonly EventDashaLedgerRow[]; event_fit_rate: EventFitRate | null; dasha_agreement: DashaAgreement | null; lagna_contrast: LagnaContrast | null; nakshatra_boundary: NakshatraBoundary | null; precision_stage: PrecisionStage | null; oos_blind_prompts: readonly OosBlindPrompt[]; discriminating_event_probes: readonly DiscriminatingEventProbe[]; } { return { event_dasha_ledger: parseEventDashaLedger(receipt?.event_dasha_ledger), event_fit_rate: parseEventFitRate(receipt?.event_fit_rate), dasha_agreement: parseDashaAgreement(receipt?.dasha_agreement), lagna_contrast: parseLagnaContrast(receipt?.lagna_contrast), nakshatra_boundary: parseNakshatraBoundary(receipt?.nakshatra_boundary), precision_stage: parsePrecisionStage(receipt?.precision_stage), oos_blind_prompts: parseOosBlindPrompts(receipt?.oos_blind_prompts), discriminating_event_probes: parseDiscriminatingEventProbes(receipt?.discriminating_event_probes), }; }