报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。 Co-Authored-By: Grok (agent loop)
1061 lines
37 KiB
TypeScript
1061 lines
37 KiB
TypeScript
/**
|
||
* 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.
|
||
*/
|
||
|
||
import { distinguishContractErrors } from "../core/distinguish-contract.ts";
|
||
import { parseCandidateIntervals } from "../core/candidate-window.ts";
|
||
import { hasDatedTransition, parseDatedTransition, validDatedTransitions, type DatedTransitionPosition } from "../core/sign-from-transitions.ts";
|
||
import { engineMeaningToDisplayCopy } from "../user-copy.ts";
|
||
import { questionContractVersionIsCompatible } from "./probe-question-contract.ts";
|
||
import { asRecord } from "@/lib/guards";
|
||
|
||
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<WindowScanLayer, string> = {
|
||
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 = Partial<DatedTransitionPosition> & Readonly<{
|
||
layer: WindowScanLayer;
|
||
at: string;
|
||
user_meaning: string;
|
||
from_sign?: string;
|
||
to_sign?: string;
|
||
}>;
|
||
|
||
export type EventDashaLedgerRow = Readonly<{
|
||
summary: string;
|
||
match: "strong" | "medium" | "weak" | "none";
|
||
match_label: string;
|
||
user_meaning: string;
|
||
gochara: "activated" | "not_seen" | null;
|
||
domain?: string;
|
||
year_month?: string;
|
||
}>;
|
||
|
||
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;
|
||
start_at?: string;
|
||
end_at?: string;
|
||
segment_index?: number;
|
||
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"
|
||
| "oos_blind";
|
||
|
||
export type EventProbeRole = "distinguish" | "reverse_verify" | "clarify" | "collect" | "holdout";
|
||
|
||
export type ProbePhase =
|
||
| "evidence_collection"
|
||
| "event_clarification"
|
||
| "candidate_discriminator"
|
||
| "holdout_validation";
|
||
|
||
export type EventProbeChoiceKind = "existence" | "varga_style" | "event_quality";
|
||
|
||
export type EventProbeStyleOption = Readonly<{
|
||
label: string;
|
||
answer_class: string;
|
||
sign?: string;
|
||
}>;
|
||
|
||
export type ProbeExpectedOutcome = Readonly<{
|
||
answer_class: string;
|
||
supports: readonly string[];
|
||
conflicts: readonly string[];
|
||
}>;
|
||
|
||
export type DiscriminatingEventProbe = Readonly<{
|
||
year: number;
|
||
year_label: string;
|
||
month?: number;
|
||
domain: EventProbeDomain;
|
||
event_family: string;
|
||
source: EventProbeSource;
|
||
tracks: readonly ("vimshottari" | "narayana")[];
|
||
tracks_agree: boolean;
|
||
unique_minute_claim: false;
|
||
user_meaning: string;
|
||
role: EventProbeRole;
|
||
phase?: ProbePhase;
|
||
information_gain?: number;
|
||
semantic_key?: string;
|
||
candidate_split_hash?: string;
|
||
candidate_set_version?: string;
|
||
candidate_ids?: readonly string[];
|
||
expected_outcomes?: readonly ProbeExpectedOutcome[];
|
||
left_time?: string;
|
||
right_time?: string;
|
||
choice_kind?: EventProbeChoiceKind;
|
||
style_options?: readonly EventProbeStyleOption[];
|
||
target_evidence_id?: string;
|
||
display_date_label?: string;
|
||
}>;
|
||
|
||
export type GuidedCollectWindowRow = Readonly<{
|
||
year: number;
|
||
month_lo: number;
|
||
month_hi: number;
|
||
domain: string;
|
||
split: Readonly<{ left: number; right: number }>;
|
||
}>;
|
||
|
||
export function parseGuidedCollectWindows(value: unknown): readonly GuidedCollectWindowRow[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: GuidedCollectWindowRow[] = [];
|
||
for (const item of value) {
|
||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||
const row = item as Record<string, unknown>;
|
||
const year = typeof row.year === "number" ? row.year : Number.NaN;
|
||
const monthLo = typeof row.month_lo === "number" ? row.month_lo : Number.NaN;
|
||
const monthHi = typeof row.month_hi === "number" ? row.month_hi : Number.NaN;
|
||
const domain = typeof row.domain === "string" ? row.domain.trim() : "";
|
||
const split = row.split && typeof row.split === "object" && !Array.isArray(row.split)
|
||
? row.split as Record<string, unknown>
|
||
: null;
|
||
const left = typeof split?.left === "number" ? split.left : Number.NaN;
|
||
const right = typeof split?.right === "number" ? split.right : Number.NaN;
|
||
if (!Number.isInteger(year) || year < 1900 || year > 2100) continue;
|
||
if (!Number.isInteger(monthLo) || monthLo < 1 || monthLo > 12) continue;
|
||
if (!Number.isInteger(monthHi) || monthHi < 1 || monthHi > 12) continue;
|
||
if (!domain || !EVENT_PROBE_DOMAINS.includes(domain as EventProbeDomain)) continue;
|
||
if (!Number.isInteger(left) || !Number.isInteger(right)) continue;
|
||
if (left < 0 || right < 0) continue;
|
||
rows.push({
|
||
year,
|
||
month_lo: monthLo,
|
||
month_hi: monthHi,
|
||
domain,
|
||
split: { left, right },
|
||
});
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
export type ProspectiveWindow = Readonly<{
|
||
domain: string;
|
||
from: string;
|
||
to: string;
|
||
}>;
|
||
|
||
export function parseProspectiveWindows(value: unknown): readonly ProspectiveWindow[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: ProspectiveWindow[] = [];
|
||
for (const item of value) {
|
||
const row = asRecord(item);
|
||
const domain = typeof row?.domain === "string" ? row.domain.trim() : "";
|
||
const from = typeof row?.from === "string" ? row.from.trim() : "";
|
||
const to = typeof row?.to === "string" ? row.to.trim() : from;
|
||
if (!row || !domain || !/^\d{4}-\d{2}$/.test(from) || !/^\d{4}-\d{2}$/.test(to)) continue;
|
||
rows.push({ domain, from, to });
|
||
if (rows.length >= 2) break;
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
function parseClockKeyedMap<T>(
|
||
value: unknown,
|
||
parse: (item: unknown) => T,
|
||
): Readonly<Record<string, T>> {
|
||
const row = asRecord(value);
|
||
if (!row) return {};
|
||
const out: Record<string, T> = {};
|
||
for (const [key, item] of Object.entries(row)) {
|
||
const clock = asTime(key);
|
||
if (!clock) continue;
|
||
out[clock] = parse(item);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function parseEventDashaLedgerByTime(
|
||
value: unknown,
|
||
): Readonly<Record<string, readonly EventDashaLedgerRow[]>> {
|
||
return parseClockKeyedMap(value, (item) => parseEventDashaLedger(item));
|
||
}
|
||
|
||
export function parseProspectiveWindowsByTime(
|
||
value: unknown,
|
||
): Readonly<Record<string, readonly ProspectiveWindow[]>> {
|
||
return parseClockKeyedMap(value, (item) => parseProspectiveWindows(item));
|
||
}
|
||
|
||
export type ProspectiveProbe = Readonly<{
|
||
candidate_label: string;
|
||
domain: string;
|
||
window_label: string;
|
||
user_meaning: string;
|
||
used_for_scoring: false;
|
||
}>;
|
||
|
||
export type DroppedPacketProbe = Readonly<{
|
||
semantic_key?: string;
|
||
reason: string;
|
||
domain?: string;
|
||
year?: number;
|
||
source?: string;
|
||
}>;
|
||
|
||
export type CandidateContrastOpportunity = Readonly<{
|
||
domain: string;
|
||
time_window: Readonly<{ year: number | null; year_label: string | null }>;
|
||
candidate_groups: readonly (readonly string[])[];
|
||
expected_outcomes: readonly ProbeExpectedOutcome[];
|
||
information_gain: number;
|
||
source_features: readonly Readonly<{ technique: string; layers?: readonly string[] }>[];
|
||
semantic_key?: string;
|
||
candidate_split_hash?: string;
|
||
candidate_set_version?: string;
|
||
event_family?: string;
|
||
phase: "candidate_discriminator";
|
||
}>;
|
||
|
||
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<string>();
|
||
const dated = value.some((item) => { const row = asRecord(item); return row && hasDatedTransition(row); });
|
||
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
|
||
) {
|
||
if (dated) return [];
|
||
continue;
|
||
}
|
||
const position = dated ? parseDatedTransition(row) : null;
|
||
if (dated && !position) return [];
|
||
const key = `${layer}:${at}`;
|
||
if (seen.has(key)) { if (dated) return []; else continue; }
|
||
seen.add(key);
|
||
rows.push({
|
||
...(position ?? {}),
|
||
layer,
|
||
at,
|
||
user_meaning: position?.segment_start ? `${LAYER_LABEL[layer]} 在这一段起点的状态` : `${LAYER_LABEL[layer]} 在 ${at} 发生变化`,
|
||
...(asText(row.from_sign, 12) ? { from_sign: asText(row.from_sign, 12)! } : {}),
|
||
...(asText(row.to_sign, 12) ? { to_sign: asText(row.to_sign, 12)! } : {}),
|
||
});
|
||
}
|
||
return dated && !validDatedTransitions(rows) ? [] : 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,
|
||
...(typeof row.domain === "string" && row.domain.trim() ? { domain: row.domain.trim() } : {}),
|
||
...(typeof row.year_month === "string" && /^\d{4}(?:-\d{2})?$/.test(row.year_month)
|
||
? { year_month: row.year_month }
|
||
: {}),
|
||
});
|
||
}
|
||
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 dashaAgreementCopy(
|
||
status: DashaAgreement["status"],
|
||
vim: string | null,
|
||
nar: string | null,
|
||
): DashaAgreement {
|
||
if (status === "conflict" && vim && nar) {
|
||
return {
|
||
status,
|
||
vimshottari_top: vim,
|
||
narayana_top: nar,
|
||
user_meaning: `主限更偏向 ${vim},分盘大运更偏向 ${nar}。冲突时不能按更高把握收口。`,
|
||
};
|
||
}
|
||
if (status === "agree") {
|
||
return {
|
||
status,
|
||
vimshottari_top: vim,
|
||
narayana_top: nar,
|
||
user_meaning: "主限和分盘大运都更支持同一段代表性时间。这仍不是唯一分钟确认。",
|
||
};
|
||
}
|
||
if (status === "partial") {
|
||
return {
|
||
status,
|
||
vimshottari_top: vim,
|
||
narayana_top: nar,
|
||
user_meaning: "主限和分盘大运还不能做成完整对照,只作观察。",
|
||
};
|
||
}
|
||
return {
|
||
status: "unavailable",
|
||
vimshottari_top: null,
|
||
narayana_top: null,
|
||
user_meaning: "还没有足够的大运对照。",
|
||
};
|
||
}
|
||
|
||
export type DashaClusterHint = Readonly<{
|
||
time?: string;
|
||
cluster_range?: readonly [string, string];
|
||
cluster_start?: string;
|
||
cluster_end?: string;
|
||
clusterTimes?: readonly string[];
|
||
cluster_times?: readonly string[];
|
||
}>;
|
||
|
||
function clockMinutes(value: string): number | null {
|
||
if (!TIME.test(value)) return null;
|
||
return Number(value.slice(0, 2)) * 60 + Number(value.slice(3));
|
||
}
|
||
|
||
function clusterKeyForTime(time: string, clusters: readonly DashaClusterHint[]): string {
|
||
const point = clockMinutes(time);
|
||
for (const row of clusters) {
|
||
const start = row.cluster_start ?? row.cluster_range?.[0];
|
||
const end = row.cluster_end ?? row.cluster_range?.[1];
|
||
const times = row.cluster_times ?? row.clusterTimes ?? [];
|
||
const inTimes = times.includes(time) || row.time === time;
|
||
const startMinutes = start ? clockMinutes(start) : null;
|
||
const endMinutes = end ? clockMinutes(end) : null;
|
||
const inSpan = point !== null && startMinutes !== null && endMinutes !== null
|
||
&& point >= startMinutes && point <= endMinutes;
|
||
if (inTimes || inSpan) {
|
||
return start && end ? `${start}–${end}` : time;
|
||
}
|
||
}
|
||
return time;
|
||
}
|
||
|
||
function activeClusterKey(
|
||
time: string | null,
|
||
activeTimes: readonly string[],
|
||
clusters: readonly DashaClusterHint[],
|
||
): string | null {
|
||
if (!time) return null;
|
||
const key = clusterKeyForTime(time, clusters);
|
||
if (activeTimes.some((item) => item === time || clusterKeyForTime(item, clusters) === key)) {
|
||
return key;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Recompute Vimshottari / Narayana tops inside the still-active inference minutes. */
|
||
export function dashaAgreementAmongActive(
|
||
engine: DashaAgreement | null,
|
||
activeTimes: readonly string[],
|
||
clusters: readonly DashaClusterHint[] = [],
|
||
): DashaAgreement | null {
|
||
if (!engine) return null;
|
||
const active = new Set(activeTimes);
|
||
if (active.size === 0) return dashaAgreementCopy("unavailable", null, null);
|
||
const vimCluster = activeClusterKey(engine.vimshottari_top, activeTimes, clusters);
|
||
const narCluster = activeClusterKey(engine.narayana_top, activeTimes, clusters);
|
||
if (vimCluster && narCluster && vimCluster === narCluster) {
|
||
return dashaAgreementCopy("agree", engine.vimshottari_top, engine.narayana_top);
|
||
}
|
||
const vim = engine.vimshottari_top && active.has(engine.vimshottari_top)
|
||
? engine.vimshottari_top
|
||
: null;
|
||
const nar = engine.narayana_top && active.has(engine.narayana_top)
|
||
? engine.narayana_top
|
||
: null;
|
||
if (vim && nar && vim === nar) return dashaAgreementCopy("agree", vim, nar);
|
||
if (vim && nar) return dashaAgreementCopy("conflict", vim, nar);
|
||
if (!vim && !nar) return dashaAgreementCopy("agree", null, null);
|
||
return dashaAgreementCopy("partial", vim, nar);
|
||
}
|
||
|
||
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 lagnaContrastIntervalLabel(interval: LagnaContrastInterval): string {
|
||
return interval.start_at && interval.end_at
|
||
? `${interval.start_at.replace("T", " ")}–${interval.end_at.replace("T", " ")}`
|
||
: `${interval.start}-${interval.end}`;
|
||
}
|
||
|
||
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[] = [];
|
||
const dated = row.intervals.some((item) => {
|
||
const part = asRecord(item);
|
||
return part && ["start_at", "end_at", "segment_index"].some((key) => key in part);
|
||
});
|
||
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;
|
||
const absolute = dated ? parseCandidateIntervals([interval])?.[0] : null;
|
||
if (dated && (!absolute || !Number.isSafeInteger(interval.segment_index) || Number(interval.segment_index) < 0
|
||
|| absolute.start_at.slice(11) !== start || absolute.end_at.slice(11) !== end
|
||
|| (intervals.length > 0 && (intervals.at(-1)!.end_at! >= absolute.start_at
|
||
|| intervals.at(-1)!.segment_index! > Number(interval.segment_index))))) return null;
|
||
intervals.push({ start, end, lagna, lords, ...(absolute ? { ...absolute, segment_index: Number(interval.segment_index) } : {}) });
|
||
}
|
||
if (intervals.length < 2) return null;
|
||
const datedMeaning = `窗口里出现两段本命上升:${lagnaContrastIntervalLabel(intervals[0])} 为${intervals[0].lagna},${lagnaContrastIntervalLabel(intervals[1])} 为${intervals[1].lagna}。`;
|
||
const meaning = dated ? `${datedMeaning}不是命运承诺,也不能确认唯一分钟。`
|
||
: asText(row.user_meaning, 220) ?? datedMeaning;
|
||
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<string>(EVENT_PROBE_DOMAINS);
|
||
const EVENT_PROBE_SOURCES = new Set<string>([
|
||
"dasha_activation",
|
||
"dasha_boundary",
|
||
"age_band",
|
||
"known_event_quality",
|
||
"oos_blind",
|
||
]);
|
||
const PROBE_PHASES = new Set<string>([
|
||
"evidence_collection",
|
||
"event_clarification",
|
||
"candidate_discriminator",
|
||
"holdout_validation",
|
||
]);
|
||
const CLOCK_IN_COPY = /(?:[01]?\d|2[0-3]):[0-5]\d/;
|
||
|
||
function parseExpectedOutcomes(value: unknown): ProbeExpectedOutcome[] {
|
||
if (!Array.isArray(value)) return [];
|
||
return value.flatMap((item) => {
|
||
const outcome = asRecord(item);
|
||
const answer = typeof outcome?.answer_class === "string" ? outcome.answer_class : "";
|
||
if (!outcome || !answer) return [];
|
||
return [{
|
||
answer_class: answer,
|
||
supports: Array.isArray(outcome.supports)
|
||
? outcome.supports.filter((entry): entry is string => typeof entry === "string")
|
||
: [],
|
||
conflicts: Array.isArray(outcome.conflicts)
|
||
? outcome.conflicts.filter((entry): entry is string => typeof entry === "string")
|
||
: [],
|
||
}];
|
||
});
|
||
}
|
||
|
||
function parseProbeRole(row: Readonly<Record<string, unknown>>, source: string): EventProbeRole {
|
||
const anchored = typeof row.target_evidence_id === "string" && row.target_evidence_id.length > 0;
|
||
if (source === "known_event_quality" && row.role === "distinguish" && anchored) return "distinguish";
|
||
if (row.role === "clarify" || source === "known_event_quality") return "clarify";
|
||
if (row.role === "collect" || source === "age_band") return "collect";
|
||
if (row.role === "holdout" || source === "oos_blind") return "holdout";
|
||
if (row.role === "distinguish" || row.phase === "candidate_discriminator") return "distinguish";
|
||
return "reverse_verify";
|
||
}
|
||
|
||
export function parseEventProbes(
|
||
value: unknown,
|
||
options: { allowQuality?: boolean; requireDistinguishContract?: boolean } = {},
|
||
): readonly DiscriminatingEventProbe[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: DiscriminatingEventProbe[] = [];
|
||
for (const item of value) {
|
||
const row = asRecord(item);
|
||
if (!questionContractVersionIsCompatible(row?.question_contract_version ?? row?.question_contract)) continue;
|
||
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);
|
||
const month = typeof row?.month === "number" && Number.isInteger(row.month) && row.month >= 1 && row.month <= 12
|
||
? row.month
|
||
: null;
|
||
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 evidenceId = typeof row.target_evidence_id === "string" && UUID.test(row.target_evidence_id)
|
||
? row.target_evidence_id
|
||
: null;
|
||
const anchoredQuality = source === "known_event_quality"
|
||
&& Boolean(evidenceId)
|
||
&& (row.role === "distinguish" || row.phase === "candidate_discriminator");
|
||
if (!options.allowQuality && source === "known_event_quality" && !anchoredQuality) continue;
|
||
const tracks = Array.isArray(row.tracks)
|
||
? row.tracks.filter((track): track is "vimshottari" | "narayana" => track === "vimshottari" || track === "narayana")
|
||
: [];
|
||
const expectedOutcomes = parseExpectedOutcomes(row.expected_outcomes);
|
||
const candidateIds = Array.isArray(row.candidate_ids)
|
||
? row.candidate_ids.filter((entry): entry is string => typeof entry === "string" && TIME.test(entry.slice(0, 5)))
|
||
: [];
|
||
const probe: DiscriminatingEventProbe = {
|
||
year,
|
||
year_label: label,
|
||
...(month ? { month } : {}),
|
||
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: parseProbeRole(row, source),
|
||
...(typeof row.phase === "string" && PROBE_PHASES.has(row.phase) ? { phase: row.phase as ProbePhase } : {}),
|
||
...(typeof row.information_gain === "number" && Number.isFinite(row.information_gain)
|
||
? { information_gain: row.information_gain }
|
||
: {}),
|
||
...(asText(row.semantic_key, 80) ? { semantic_key: asText(row.semantic_key, 80)! } : {}),
|
||
...(asText(row.candidate_split_hash, 120) ? { candidate_split_hash: asText(row.candidate_split_hash, 120)! } : {}),
|
||
...(asText(row.candidate_set_version, 40) ? { candidate_set_version: asText(row.candidate_set_version, 40)! } : {}),
|
||
...(candidateIds.length > 0 ? { candidate_ids: candidateIds } : {}),
|
||
...(asTime(row.left_time) ? { left_time: asTime(row.left_time)! } : {}),
|
||
...(asTime(row.right_time) ? { right_time: asTime(row.right_time)! } : {}),
|
||
...(row.choice_kind === "existence" || row.choice_kind === "varga_style" || row.choice_kind === "event_quality"
|
||
? { choice_kind: row.choice_kind }
|
||
: {}),
|
||
...(Array.isArray(row.style_options) ? {
|
||
style_options: row.style_options.flatMap((optionItem) => {
|
||
const option = asRecord(optionItem);
|
||
const optionLabel = asText(option?.label, 80);
|
||
const answer = typeof option?.answer_class === "string" ? option.answer_class : "";
|
||
if (!option || !optionLabel || !answer) return [];
|
||
return [{
|
||
label: optionLabel,
|
||
answer_class: answer,
|
||
...(asText(option.sign, 12) ? { sign: asText(option.sign, 12)! } : {}),
|
||
}];
|
||
}),
|
||
} : {}),
|
||
...(expectedOutcomes.length > 0 ? { expected_outcomes: expectedOutcomes } : {}),
|
||
...(evidenceId ? { target_evidence_id: evidenceId } : {}),
|
||
...(asText(row.display_date_label, 40) ? { display_date_label: asText(row.display_date_label, 40)! } : {}),
|
||
};
|
||
if (options.requireDistinguishContract && distinguishContractErrors({ ...probe, role: "distinguish" }).length > 0) continue;
|
||
rows.push(probe);
|
||
if (rows.length === 8) break;
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
export function parseDiscriminatingEventProbes(value: unknown): readonly DiscriminatingEventProbe[] {
|
||
return parseEventProbes(value, { requireDistinguishContract: true });
|
||
}
|
||
|
||
export function parseClarificationProbes(value: unknown): readonly DiscriminatingEventProbe[] {
|
||
return parseEventProbes(value, { allowQuality: true }).filter((item) => (
|
||
item.source === "known_event_quality" || item.phase === "event_clarification" || item.role === "clarify"
|
||
));
|
||
}
|
||
|
||
export function parseCollectionProbes(value: unknown): readonly DiscriminatingEventProbe[] {
|
||
return parseEventProbes(value).filter((item) => (
|
||
item.phase === "evidence_collection" || item.role === "collect" || item.source === "age_band"
|
||
));
|
||
}
|
||
|
||
export function parseDroppedPacketProbes(value: unknown): readonly DroppedPacketProbe[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: DroppedPacketProbe[] = [];
|
||
for (const item of value) {
|
||
const row = asRecord(item);
|
||
const reason = asText(row?.reason, 80);
|
||
if (!row || !reason) continue;
|
||
rows.push({
|
||
reason,
|
||
...(asText(row.semantic_key, 80) ? { semantic_key: asText(row.semantic_key, 80)! } : {}),
|
||
...(typeof row.domain === "string" ? { domain: row.domain } : {}),
|
||
...(typeof row.year === "number" && Number.isInteger(row.year) ? { year: row.year } : {}),
|
||
...(typeof row.source === "string" ? { source: row.source } : {}),
|
||
});
|
||
if (rows.length === 16) break;
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
export function parseProspectiveProbes(value: unknown): readonly ProspectiveProbe[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: ProspectiveProbe[] = [];
|
||
for (const item of value) {
|
||
const row = asRecord(item);
|
||
const label = asText(row?.candidate_label, 8);
|
||
const domain = typeof row?.domain === "string" ? row.domain : "";
|
||
const window = asText(row?.window_label, 40);
|
||
const meaning = asText(row?.user_meaning, 200);
|
||
if (
|
||
!row
|
||
|| !label
|
||
|| !EVENT_PROBE_DOMAIN_SET.has(domain)
|
||
|| !window
|
||
|| !meaning
|
||
|| row.used_for_scoring === true
|
||
|| CLOCK_IN_COPY.test(meaning)
|
||
|| CLOCK_IN_COPY.test(window)
|
||
) continue;
|
||
rows.push({
|
||
candidate_label: label,
|
||
domain,
|
||
window_label: window,
|
||
user_meaning: meaning,
|
||
used_for_scoring: false,
|
||
});
|
||
if (rows.length === 3) break;
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
export function prospectiveWindowsNarration(probes: readonly ProspectiveProbe[]): string | null {
|
||
const meanings = probes
|
||
.filter((item) => item.used_for_scoring === false)
|
||
.map((item) => engineMeaningToDisplayCopy(item.user_meaning).trim())
|
||
.filter((item) => item.length > 0 && !CLOCK_IN_COPY.test(item));
|
||
if (meanings.length === 0) return null;
|
||
return meanings.join(" ");
|
||
}
|
||
|
||
export function parseCandidateContrastOpportunities(value: unknown): readonly CandidateContrastOpportunity[] {
|
||
if (!Array.isArray(value)) return [];
|
||
const rows: CandidateContrastOpportunity[] = [];
|
||
for (const item of value) {
|
||
const row = asRecord(item);
|
||
if (!row) continue;
|
||
const domain = typeof row.domain === "string" ? row.domain : "";
|
||
const gain = typeof row.information_gain === "number" && Number.isFinite(row.information_gain)
|
||
? row.information_gain
|
||
: 0;
|
||
const window = asRecord(row.time_window);
|
||
const groups = Array.isArray(row.candidate_groups)
|
||
? row.candidate_groups.flatMap((group) => (
|
||
Array.isArray(group)
|
||
? [group.filter((entry): entry is string => typeof entry === "string")]
|
||
: []
|
||
))
|
||
: [];
|
||
const outcomes = parseExpectedOutcomes(row.expected_outcomes);
|
||
if (!domain || gain <= 0 || groups.length < 2 || outcomes.length < 2) continue;
|
||
rows.push({
|
||
domain,
|
||
time_window: {
|
||
year: typeof window?.year === "number" && Number.isInteger(window.year) ? window.year : null,
|
||
year_label: asText(window?.year_label, 40),
|
||
},
|
||
candidate_groups: groups,
|
||
expected_outcomes: outcomes,
|
||
information_gain: gain,
|
||
source_features: Array.isArray(row.source_features)
|
||
? row.source_features.flatMap((feature) => {
|
||
const parsed = asRecord(feature);
|
||
const technique = asText(parsed?.technique, 80);
|
||
if (!parsed || !technique) return [];
|
||
return [{
|
||
technique,
|
||
...(Array.isArray(parsed.layers)
|
||
? { layers: parsed.layers.filter((layer): layer is string => typeof layer === "string") }
|
||
: {}),
|
||
}];
|
||
})
|
||
: [],
|
||
phase: "candidate_discriminator",
|
||
...(asText(row.semantic_key, 80) ? { semantic_key: asText(row.semantic_key, 80)! } : {}),
|
||
...(asText(row.candidate_split_hash, 120) ? { candidate_split_hash: asText(row.candidate_split_hash, 120)! } : {}),
|
||
...(asText(row.candidate_set_version, 40) ? { candidate_set_version: asText(row.candidate_set_version, 40)! } : {}),
|
||
...(asText(row.event_family, 80) ? { event_family: asText(row.event_family, 80)! } : {}),
|
||
});
|
||
if (rows.length === 8) 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;
|
||
band: "high" | "medium" | "low" | "insufficient" | 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;
|
||
const band = row.band === "high" || row.band === "medium" || row.band === "low" || row.band === "insufficient"
|
||
? row.band
|
||
: null;
|
||
return {
|
||
matched,
|
||
total,
|
||
percent,
|
||
band,
|
||
label: asText(row.label, 80) ?? "事件吻合率",
|
||
user_meaning: asText(row.user_meaning, 280)
|
||
?? "这是相对拟合,不是已确认唯一出生分钟。",
|
||
unique_minute_claim: false,
|
||
};
|
||
}
|
||
|
||
export function refinementFromDecisionReceipt(
|
||
receipt: Readonly<Record<string, unknown>> | 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[];
|
||
guided_collect_windows: readonly GuidedCollectWindowRow[];
|
||
event_clarification_probes: readonly DiscriminatingEventProbe[];
|
||
evidence_collection_probes: readonly DiscriminatingEventProbe[];
|
||
candidate_contrast_opportunities: readonly CandidateContrastOpportunity[];
|
||
dropped_probes: readonly DroppedPacketProbe[];
|
||
prospective_probes: readonly ProspectiveProbe[];
|
||
event_dasha_ledger_by_time: Readonly<Record<string, readonly EventDashaLedgerRow[]>>;
|
||
prospective_windows_by_time: Readonly<Record<string, readonly ProspectiveWindow[]>>;
|
||
} {
|
||
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),
|
||
guided_collect_windows: parseGuidedCollectWindows(receipt?.guided_collect_windows),
|
||
event_clarification_probes: parseClarificationProbes(
|
||
receipt?.event_clarification_probes ?? receipt?.discriminating_event_probes,
|
||
),
|
||
evidence_collection_probes: parseCollectionProbes(
|
||
receipt?.evidence_collection_probes ?? receipt?.discriminating_event_probes,
|
||
),
|
||
candidate_contrast_opportunities: parseCandidateContrastOpportunities(receipt?.candidate_contrast_opportunities),
|
||
dropped_probes: parseDroppedPacketProbes(receipt?.dropped_probes),
|
||
prospective_probes: parseProspectiveProbes(receipt?.prospective_probes),
|
||
event_dasha_ledger_by_time: parseEventDashaLedgerByTime(receipt?.event_dasha_ledger_by_time),
|
||
prospective_windows_by_time: parseProspectiveWindowsByTime(receipt?.prospective_windows_by_time),
|
||
};
|
||
}
|