Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/inference-adapter.ts
T
Jesse_Chen 7d6b88c886
Independent Staging Quality Gate / validate (push) Successful in 14m51s
Independent Staging Quality Gate / publish (push) Successful in 17m0s
fix(rectification): honor persisted probe focus
2026-08-26 22:17:15 +08:00

366 lines
13 KiB
TypeScript

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 { askedKeysFromLedgerEvidence } from "../core/candidate-contrast-packet.ts";
import type { AnswerClass, ConflictProbe, InferenceState } from "../core/types.ts";
import {
isHoldoutVerificationQuote,
parseChoiceKeyFromUserMessage,
type ChoiceKey,
} from "./choice-card.ts";
import type { DiscriminatingEventProbe } 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 === "day" || value === "month" || value === "year") return value;
return "unknown";
}
export function askedProbeKeysFromReceipt(
receipt: Readonly<Record<string, unknown>> | 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<string, unknown>;
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<Record<string, unknown>> | null | undefined,
evidence: readonly Readonly<{
domain?: string | null;
eventKind?: string | null;
summary?: string | null;
}>[] = [],
): string[] {
return [
...askedProbeKeysFromReceipt(receipt),
...askedKeysFromLedgerEvidence(evidence),
];
}
export { previousInferenceFromReceipt } from "../core/compose-receipt.ts";
export function compactInferenceProjection(state: InferenceState | null | undefined): Record<string, unknown> | 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<Record<string, Readonly<Record<string, number>>>>;
}): 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<Record<string, unknown>> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Readonly<Record<string, unknown>>
: 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;
status?: "resolved" | "declined" | "skipped";
userMessage?: string | null;
}): ChoiceKey | null {
const explicit = input.choiceKey?.trim().toUpperCase();
if (explicit === "A" || explicit === "B" || explicit === "C" || explicit === "D") return explicit;
const fromMessage = parseChoiceKeyFromUserMessage(input.userMessage);
if (fromMessage) return fromMessage;
if (input.status === "declined") return "C";
if (input.status === "skipped") return "D";
if (input.status === "resolved") return "B";
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<Record<string, unknown>>,
state: InferenceState | null,
questionId: string,
preferred?: {
semantic_key?: string | null;
candidate_split_hash?: string | null;
probe_id?: string | null;
},
): Record<string, unknown> {
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) {
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;
status?: "resolved" | "declined" | "skipped";
userMessage?: string | null;
schema?: unknown;
questionId?: string | null;
domain?: string | null;
},
): ChoiceWithoutEvidenceResult {
if (!hasChoiceSchema(input.schema) && !input.choiceKey && !parseChoiceKeyFromUserMessage(input.userMessage)) {
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);
if (!choiceKey) {
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 = classifyChoiceAnswer(choiceKey, input.schema);
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),
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),
answerClass,
probeId: probe.id,
};
}