Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/refinement-packet.ts
T
Jesse_Chen 68a78af9e1
Independent Staging Quality Gate / validate (push) Failing after 8m39s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(rectification): ask reverse-inference probes at engine dasha months
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>
2026-08-28 10:20:05 +08:00

731 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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),
};
}