68a78af9e1
Keep Vimshottari/Narayana start dates instead of truncating to year, so same-year month splits can appear on the choice card. Co-authored-by: Cursor <cursoragent@cursor.com>
731 lines
24 KiB
TypeScript
731 lines
24 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";
|
||
|
||
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 = 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;
|
||
}>;
|
||
|
||
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"
|
||
| "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[];
|
||
}>;
|
||
|
||
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 asRecord(value: unknown): Readonly<Record<string, unknown>> | null {
|
||
return value && typeof value === "object" && !Array.isArray(value)
|
||
? value as Readonly<Record<string, unknown>>
|
||
: 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<string>();
|
||
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} 发生变化`,
|
||
...(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 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<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 {
|
||
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);
|
||
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;
|
||
if (!options.allowQuality && source === "known_event_quality") 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 } : {}),
|
||
};
|
||
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 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;
|
||
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<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[];
|
||
event_clarification_probes: readonly DiscriminatingEventProbe[];
|
||
evidence_collection_probes: readonly DiscriminatingEventProbe[];
|
||
candidate_contrast_opportunities: readonly CandidateContrastOpportunity[];
|
||
} {
|
||
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),
|
||
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),
|
||
};
|
||
}
|