import { answersFromEvidence, applyAnswerToState, applyHoldoutAnswer, applySupersedeAnswer, buildInferenceState, classifyChoiceAnswer, type EngineEventInput, } from "../core/build-state.ts"; import { isDuplicateProbe } from "../core/duplicate-probes.ts"; import { probeFromEngine } from "../core/probes-from-engine.ts"; import { selectHighestGainProbe } from "../core/select-probe.ts"; import { askedEventProbeKeysFromLedgerEvidence } from "../core/candidate-contrast-packet.ts"; import { rankActive } from "../core/convergence-evaluator.ts"; import { rangeFromTimes, unionStillValidRange } from "../core/credible-range.ts"; import { previousInferenceFromReceipt as parsePreviousInferenceFromReceipt, } from "../core/compose-receipt.ts"; import type { AnswerClass, ConflictProbe, InferenceState, ProbeAnswer } from "../core/types.ts"; import { datedPrecision } from "./evidence-model.ts"; import { isHoldoutVerificationQuote, type ChoiceKey, } from "./choice-card.ts"; import { parseNakshatraBoundary, type DiscriminatingEventProbe, type NakshatraBoundary, } from "./refinement-packet.ts"; function yearFrom(value: string | null | undefined): number | null { if (!value || value.length < 4 || !/^\d{4}/.test(value)) return null; const year = Number(value.slice(0, 4)); return year >= 1900 && year <= 2100 ? year : null; } function asPrecision(value: string | null | undefined): EngineEventInput["precision"] { if (!value) return "unknown"; return datedPrecision(value); } export function askedProbeKeysFromReceipt( receipt: Readonly> | null | undefined, ): string[] { const inference = receipt?.inference_state; if (!inference || typeof inference !== "object" || Array.isArray(inference)) return []; const answers = (inference as { answered_probes?: unknown }).answered_probes; if (!Array.isArray(answers)) return []; const keys: string[] = []; for (const item of answers) { if (!item || typeof item !== "object") continue; const row = item as Record; if (typeof row.probe_id === "string") keys.push(row.probe_id); if (typeof row.semantic_key === "string") keys.push(row.semantic_key); if (typeof row.candidate_split_hash === "string") keys.push(row.candidate_split_hash); } return keys; } export function askedDiscriminatorKeys( receipt: Readonly> | null | undefined, evidence: readonly Readonly<{ status?: string | null; domain?: string | null; eventKind?: string | null; summary?: string | null; occurredFrom?: string | null; occurredTo?: string | null; }>[] = [], ): string[] { return [ ...askedProbeKeysFromReceipt(receipt), ...askedEventProbeKeysFromLedgerEvidence(evidence), ]; } const NAKSHATRA_BOUNDARY_SOURCE = "nakshatra_boundary"; export function nakshatraBoundaryProbe( state: InferenceState | null | undefined, boundary: NakshatraBoundary | null | undefined, ): ConflictProbe | null { if (!state || !boundary?.near_boundary) return null; const optionA = boundary.options.find((item) => item.key === "A") ?? null; const optionB = boundary.options.find((item) => item.key === "B") ?? null; if ( !optionA?.traits.length || !optionB?.traits.length || optionA.time_bias === optionB.time_bias ) return null; const active = rankActive(state.candidates) .slice() .sort((left, right) => left.time.localeCompare(right.time)); if (active.length < 2) return null; const pivot = Math.ceil(active.length / 2); const earlier = active.slice(0, pivot).map((item) => item.id); const later = active.slice(pivot).map((item) => item.id); if (earlier.length === 0 || later.length === 0) return null; const semanticKey = `nakshatra-boundary:${state.candidate_set_id}`; const candidateSplitHash = `${semanticKey}:${earlier.join(",")}|${later.join(",")}`; const candidatesFor = (bias: "earlier" | "later") => bias === "earlier" ? earlier : later; const conflictsFor = (bias: "earlier" | "later") => bias === "earlier" ? later : earlier; const optionLabel = (key: "A" | "B", traits: readonly string[]) => `${key} 组:${traits.join("、")}`; return { id: `probe:${semanticKey}`, semantic_key: semanticKey, candidate_split_hash: candidateSplitHash, domain: "appearance", year: 0, question: boundary.user_meaning ?? "升点靠近两段日常节奏的交界。平时做事时,哪一组更像你?这只用来偏置时间窗,不能确认唯一分钟。", candidate_ids: active.map((item) => item.id), expected_outcomes: [ { answer_class: "yes", supports: candidatesFor(optionA.time_bias), conflicts: conflictsFor(optionA.time_bias), }, { answer_class: "weak_yes", supports: candidatesFor(optionB.time_bias), conflicts: conflictsFor(optionB.time_bias), }, { answer_class: "no", supports: [], conflicts: [] }, { answer_class: "unsure", supports: [], conflicts: [] }, ], information_gain: 0.01, source: NAKSHATRA_BOUNDARY_SOURCE, choice_kind: "varga_style", style_options: [ { label: optionLabel("A", optionA.traits), answer_class: "yes", sign: optionA.time_bias === "earlier" ? "较早时间窗" : "较晚时间窗", }, { label: optionLabel("B", optionB.traits), answer_class: "weak_yes", sign: optionB.time_bias === "earlier" ? "较早时间窗" : "较晚时间窗", }, { label: "两组都不太像我", answer_class: "no" }, { label: "一时说不好", answer_class: "unsure" }, ], }; } export function withNakshatraBoundaryProbe( state: InferenceState | null, boundary: NakshatraBoundary | null | undefined, ): InferenceState | null { if (!state) return null; const probe = nakshatraBoundaryProbe(state, boundary); const existingIndexes = state.probes.flatMap((item, index) => ( item.source === NAKSHATRA_BOUNDARY_SOURCE ? [index] : [] )); if (!probe || isDuplicateProbe(probe, state.answered_probes)) { if (existingIndexes.length === 0) return state; return { ...state, probes: state.probes.filter((item) => item.source !== NAKSHATRA_BOUNDARY_SOURCE), }; } const existing = existingIndexes.length > 0 ? state.probes[existingIndexes[0]!] : null; if ( existingIndexes.length === 1 && existing?.id === probe.id && existing.semantic_key === probe.semantic_key && existing.candidate_split_hash === probe.candidate_split_hash ) return state; const probes = state.probes.filter((item) => item.source !== NAKSHATRA_BOUNDARY_SOURCE); const insertAt = existingIndexes[0] ?? probes.length; return { ...state, probes: [ ...probes.slice(0, insertAt), probe, ...probes.slice(insertAt), ], }; } export function previousInferenceFromReceipt( receipt: Readonly> | null | undefined, ): InferenceState | null { return withNakshatraBoundaryProbe( parsePreviousInferenceFromReceipt(receipt), parseNakshatraBoundary(receipt?.nakshatra_boundary), ); } type CandidateSnapshotRow = Readonly<{ candidateId?: string; time: string; rank?: number; relativeSupport?: number; posterior_score?: number; tiedMinuteCount?: number; }>; type CandidateSnapshotSource = Readonly<{ decisionReceipt?: Readonly> | null; candidates?: readonly T[]; }> | null | undefined; type ProjectedCandidate = T & Readonly<{ rank?: number; relativeSupport?: number; posterior_score?: number; }>; type CandidateProjection = Readonly<{ fromInference: boolean; consistent: boolean; candidates: readonly ProjectedCandidate[]; scores: readonly Readonly<{ id?: string; time: string; score: number }>[]; representativeTime: string | null; credibleRange: readonly [string, string] | null; }>; export function authoritativeCandidateProjection( latest: CandidateSnapshotSource, ): CandidateProjection { const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null); if (!inference || inference.candidates.length === 0) { return { fromInference: true, consistent: false, candidates: [], scores: [], representativeTime: null, credibleRange: null, }; } const active = rankActive(inference.candidates); const persisted = latest?.candidates ?? []; const matchedPersistedIndexes = inference.candidates.map((item) => persisted.findIndex((row) => ( row.candidateId === item.id || row.time === item.time ))); const completeCandidateSet = inference.candidates.length === persisted.length && matchedPersistedIndexes.every((index) => index >= 0) && new Set(matchedPersistedIndexes).size === persisted.length && new Set(inference.candidates.map((item) => item.id)).size === inference.candidates.length && new Set(inference.candidates.map((item) => item.time)).size === inference.candidates.length; const candidates = active.flatMap((item, index) => { const source = persisted.find((row) => row.candidateId === item.id) ?? persisted.find((row) => row.time === item.time); if (!source) return []; return [{ ...source, rank: index + 1, relativeSupport: Math.max(0, Math.min(100, Math.round(item.posterior_score))), posterior_score: item.posterior_score, }]; }); const representativeTime = active[0]?.time ?? null; const activePoints = active.flatMap((item) => [item.cluster_range[0], item.time, item.cluster_range[1]]); const activeSpan = rangeFromTimes(activePoints); const stillValidRange = unionStillValidRange(inference.candidates); const receiptRange = inference.credible_range; const activeRangesValid = Boolean(activeSpan) && active.every((item) => { const clusterRange = rangeFromTimes(item.cluster_range); return clusterRange?.[0] === item.cluster_range[0] && clusterRange[1] === item.cluster_range[1] && rangeFromTimes([item.time])?.[0] === item.time && item.time >= clusterRange[0] && item.time <= clusterRange[1] && item.cluster_range[0] >= activeSpan![0] && item.cluster_range[1] <= activeSpan![1]; }); const receiptRangeMatches = Boolean(stillValidRange && receiptRange) && receiptRange![0] <= receiptRange![1] && receiptRange![0] === stillValidRange![0] && receiptRange![1] === stillValidRange![1]; const consistent = active.length > 0 && completeCandidateSet && candidates.length === active.length && inference.representative_time === representativeTime && activeRangesValid && receiptRangeMatches; return { fromInference: true, consistent, candidates: consistent ? candidates : [], scores: consistent ? active.map((item) => ({ id: item.id, time: item.time, score: item.posterior_score })) : [], representativeTime: consistent ? representativeTime : null, credibleRange: consistent ? stillValidRange : null, }; } export function compactInferenceProjection(state: InferenceState | null | undefined): Record | null { if (!state) return null; const next = selectHighestGainProbe(state.probes, state.answered_probes); return { algorithm_version: state.algorithm_version, candidate_set_id: state.candidate_set_id, revision: state.revision, phase: state.result_status === "discriminating" && state.phase === "event_collection" ? "discrimination" : state.phase, result_status: state.result_status, entropy: state.entropy, representative_time: state.representative_time, credible_range: state.credible_range, candidates: state.candidates.map((item) => ({ id: item.id, time: item.time, probability: item.probability, posterior_score: item.posterior_score, status: item.status, rank: item.rank, cluster_range: item.cluster_range, })), next_probe: next ? { semantic_key: next.semantic_key, information_gain: next.information_gain, question: next.question, domain: next.domain, year: next.year, } : null, answered_probe_count: state.answered_probes.length, last_inference_round: state.last_inference_round ? { kind: state.last_inference_round.kind, entropy_before: state.last_inference_round.entropy_before, entropy_after: state.last_inference_round.entropy_after, eliminated_ids: state.last_inference_round.eliminated_ids, score_deltas: state.last_inference_round.score_deltas ?? {}, } : null, informative_round_count: state.rounds.filter((item) => item.kind === "informative").length, }; } export function buildCaseInferenceState(input: { range: { start_time: string; end_time: string }; candidates: readonly Readonly<{ candidateId: string; time: string; relativeSupport: number }>[]; evidence: readonly Readonly<{ id: string; domain: string; occurredFrom: string | null; datePrecision: string; }>[]; probes: readonly DiscriminatingEventProbe[]; extraProbes?: readonly ConflictProbe[]; previous?: InferenceState | null; transitionTimes?: readonly string[]; eventLedger?: Readonly>>>; }): InferenceState { const events = input.evidence.map((item) => ({ id: item.id, domain: item.domain, year: yearFrom(item.occurredFrom), precision: asPrecision(item.datePrecision), })); const probes = [ ...input.probes.flatMap((probe) => { const mapped = probeFromEngine(probe); return mapped ? [mapped] : []; }), ...(input.extraProbes ?? []), ]; return buildInferenceState({ range_start: input.range.start_time, range_end: input.range.end_time, candidates: input.candidates.map((item) => ({ id: item.time, time: item.time, relative_support: item.relativeSupport, })), events, probes, previous: input.previous, answered_probes: answersFromEvidence(probes, events), transition_times: input.transitionTimes, event_ledger: input.eventLedger, }); } function asRecord(value: unknown): Readonly> | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Readonly> : null; } function asText(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } export function hasChoiceSchema(schema: unknown): boolean { const row = asRecord(schema); if (!row) return false; if (asRecord(row.choice)) return true; return Boolean(asText(row.semantic_key) || asText(row.probe_id) || asText(row.candidate_split_hash)); } export function isHoldoutChoiceSchema( schema: unknown, questionId?: string | null, userMessage?: string | null, ): boolean { if (userMessage && isHoldoutVerificationQuote(userMessage)) return true; if (questionId?.endsWith(":holdout")) return true; const row = asRecord(schema); return row?.scoring === false; } export function resolveChoiceKey(input: { choiceKey?: string | null; }): ChoiceKey | null { const explicit = input.choiceKey?.trim().toUpperCase(); if (explicit === "A" || explicit === "B" || explicit === "C" || explicit === "D") return explicit; return null; } export function matchProbeForChoice( state: InferenceState, schema: unknown, domain?: string | null, ): ConflictProbe | null { const row = asRecord(schema); const probeId = asText(row?.probe_id); const semanticKey = asText(row?.semantic_key); const splitHash = asText(row?.candidate_split_hash); const probes = state.probes; if (probeId || semanticKey || splitHash) { return probes.find((item) => ( (!probeId || item.id === probeId || item.semantic_key === probeId || probeId === `probe:${item.semantic_key}`) && (!semanticKey || item.semantic_key === semanticKey || item.id === semanticKey) && (!splitHash || item.candidate_split_hash === splitHash) )) ?? null; } const unanswered = domain ? probes.filter((item) => item.domain === domain) : probes; return selectHighestGainProbe(unanswered.length > 0 ? unanswered : probes, state.answered_probes); } function probeMatchesPreferred( probe: ConflictProbe, preferred: { semantic_key?: string | null; candidate_split_hash?: string | null; probe_id?: string | null }, ): boolean { const semanticKey = preferred.semantic_key?.trim() || null; const splitHash = preferred.candidate_split_hash?.trim() || null; const probeId = preferred.probe_id?.trim() || null; if (probeId && (probe.id === probeId || probe.semantic_key === probeId)) return true; if (semanticKey && (probe.semantic_key === semanticKey || probe.id === semanticKey)) return true; if (splitHash && probe.candidate_split_hash === splitHash) return true; return false; } export function stampChoiceSchemaWithProbe( schema: Readonly>, state: InferenceState | null, questionId: string, preferred?: { semantic_key?: string | null; candidate_split_hash?: string | null; probe_id?: string | null; }, ): Record { if (!hasChoiceSchema(schema)) return { ...schema }; const scoring = schema.scoring === false || questionId.endsWith(":holdout") ? false : true; const preferredKey = preferred?.semantic_key?.trim() || asText(schema.semantic_key); const preferredSplit = preferred?.candidate_split_hash?.trim() || asText(schema.candidate_split_hash); const preferredId = preferred?.probe_id?.trim() || asText(schema.probe_id); const matched = state?.probes.find((probe) => probeMatchesPreferred(probe, { semantic_key: preferredKey, candidate_split_hash: preferredSplit, probe_id: preferredId, })) ?? null; if (preferredKey && !matched) { if (state) { const unstamped = { ...schema }; delete unstamped.probe_id; delete unstamped.semantic_key; delete unstamped.candidate_split_hash; return { ...unstamped, scoring }; } return { ...schema, probe_id: preferredId ?? `probe:${preferredKey}`, semantic_key: preferredKey, candidate_split_hash: preferredSplit ?? preferredKey, scoring, }; } const next = matched ?? (state ? selectHighestGainProbe(state.probes, state.answered_probes) : null); if (!next) { if (!preferredKey) return { ...schema }; return { ...schema, probe_id: preferredId ?? `probe:${preferredKey}`, semantic_key: preferredKey, candidate_split_hash: preferredSplit ?? preferredKey, scoring, }; } return { ...schema, probe_id: next.id, semantic_key: next.semantic_key, candidate_split_hash: next.candidate_split_hash, scoring, }; } export type ChoiceWithoutEvidenceResult = Readonly<{ applied: boolean; reason: "applied" | "no_choice" | "holdout" | "no_probe" | "already_answered" | "stale_probe" | "superseded"; state: InferenceState; answerClass: AnswerClass | null; probeId: string | null; }>; export function applyChoiceWithoutEvidence( state: InferenceState, input: { choiceKey?: string | null; userMessage?: string | null; schema?: unknown; questionId?: string | null; domain?: string | null; classifiedFrom?: Extract; }, ): ChoiceWithoutEvidenceResult { if (!hasChoiceSchema(input.schema) && !input.choiceKey) { return { applied: false, reason: "no_choice", state, answerClass: null, probeId: null }; } if (isHoldoutChoiceSchema(input.schema, input.questionId, input.userMessage)) { const choiceKey = resolveChoiceKey(input); const answerClass = choiceKey ? classifyChoiceAnswer(choiceKey, input.schema) : null; if (!answerClass) { return { applied: false, reason: "holdout", state, answerClass: null, probeId: null }; } return { applied: true, reason: "holdout", state: applyHoldoutAnswer(state, answerClass), answerClass, probeId: asText(asRecord(input.schema)?.probe_id), }; } const choiceKey = resolveChoiceKey(input); const declined = input.classifiedFrom === "declined"; if (!choiceKey && !declined) { return { applied: false, reason: "no_choice", state, answerClass: null, probeId: null }; } const schema = asRecord(input.schema); const submittedProbeId = asText(schema?.probe_id); const hasSubmittedProbeIdentity = Boolean( submittedProbeId || asText(schema?.semantic_key) || asText(schema?.candidate_split_hash), ); const probe = matchProbeForChoice(state, input.schema, input.domain); if (!probe) { return { applied: false, reason: hasSubmittedProbeIdentity ? "stale_probe" : "no_probe", state, answerClass: null, probeId: submittedProbeId }; } const lastAnsweredId = state.answered_probes.at(-1)?.probe_id ?? null; const answerClass = declined ? "unsure" : classifyChoiceAnswer(choiceKey!, input.schema); if (!answerClass) { return { applied: false, reason: "no_choice", state, answerClass: null, probeId: probe.id }; } const existing = state.answered_probes.find((item) => ( item.probe_id === probe.id || item.semantic_key === probe.semantic_key )); if (existing) { if (existing.answer_class === answerClass) { return { applied: false, reason: "already_answered", state, answerClass, probeId: probe.id }; } if (probe.id === lastAnsweredId) { return { applied: true, reason: "superseded", state: applySupersedeAnswer(state, probe.id, answerClass, input.classifiedFrom), answerClass, probeId: probe.id, }; } return { applied: false, reason: "stale_probe", state, answerClass, probeId: probe.id }; } if (isDuplicateProbe(probe, state.answered_probes)) { return { applied: false, reason: "already_answered", state, answerClass, probeId: probe.id }; } return { applied: true, reason: "applied", state: applyAnswerToState(state, probe.id, answerClass, input.classifiedFrom), answerClass, probeId: probe.id, }; }