Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts
T

600 lines
23 KiB
TypeScript

/**
* Shared Case → decideRectification adapter.
*
* GET refresh, turn_decision, choice/stop, and interview cards must feed
* the same inputs. Snapshot selection_allowed is storage, not policy.
*/
import {
buildCandidateContrastPacket,
datedDomainsFromEvidence,
inspectDiscriminatorProbes,
mentionedVargaKeysFromLedgerEvidence,
volunteeredDomainsFromEvidence,
withCompletedContrastOptions,
type CandidateContrastPacket,
type CandidateDiscriminatorProbe,
type EngineContrastProbe,
} from "../core/candidate-contrast-packet.ts";
import {
decideRectification,
publicDecisionFields,
type HoldoutValidationStatus,
type RectificationDecision,
} from "../core/rectification-decision.ts";
import type { InferenceState } from "../core/types.ts";
import {
askedDiscriminatorKeys,
authoritativeCandidateProjection,
previousInferenceFromReceipt,
} from "./inference-adapter";
import {
blockingMethodsCovered,
buildMethodFollowupPlan,
datedMethodCollectOpen,
} from "./method-followup";
import { buildConfirmationGate } from "./confirmation-gate";
import {
MIN_ACCEPTANCE_DOMAINS,
MIN_ACCEPTANCE_EVENTS,
trainingScoreableGate,
} from "./evidence-model";
import { refinementFromDecisionReceipt, type DiscriminatingEventProbe } from "./refinement-packet";
import { windowScanFromDecisionReceipt } from "./varga-observations";
import type { DroppedProbe } from "./probe-question-contract.ts";
import { evidenceLedgerFingerprint } from "./tool-service";
import {
candidateSnapshotSource,
storedSnapshotIsCurrent,
} from "../core/snapshot-source.ts";
export type DecisionDossier = Readonly<{
evidence: readonly Readonly<{
id?: string;
status: string;
domain: string;
datePrecision: string;
occurredFrom: string | null;
occurredTo: string | null;
eventKind?: string | null;
summary?: string | null;
}>[];
conversationSummary: {
activeFocus: {
id?: string;
intent: string;
targetDomain: string | null;
targetKind: string | null;
expectedAnswerSchema?: Readonly<Record<string, unknown>> | null;
} | null;
declinedSkippedTopics: readonly Readonly<Record<string, unknown>>[];
};
latestResult: {
resultId?: string;
decisionReceipt?: Readonly<Record<string, unknown>> | null;
selectionAllowed?: boolean;
confirmationAllowed?: boolean;
candidates?: readonly Readonly<{
candidateId?: string;
time: string;
rank?: number;
relativeSupport?: number;
posterior_score?: number;
tiedMinuteCount?: number;
}>[];
representativeTime?: string | null;
evidenceLedgerFingerprint?: string | null;
candidateRangeFingerprint?: string | null;
policyVersion?: string | null;
algorithmVersion?: string | null;
} | null;
case: {
acceptedTime: string | null;
status?: string;
};
turns?: readonly Readonly<{ role: string; text: string | null }>[];
}>;
export function candidateScoresFromDossier(latest: DecisionDossier["latestResult"]) {
const projection = authoritativeCandidateProjection(latest);
if (projection.scores.length > 0) return projection.scores;
if (previousInferenceFromReceipt(latest?.decisionReceipt ?? null)) return [];
return (latest?.candidates ?? []).flatMap((item) => (
typeof item.time === "string" && item.time.trim()
? [{
time: item.time,
score: item.posterior_score ?? item.relativeSupport ?? 0,
}]
: []
));
}
const CLOCK_TIME = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
export function discriminatorCandidateTimes(
latest: DecisionDossier["latestResult"],
): string[] {
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
const fromInference = [...new Set(
(inference?.candidates ?? [])
.filter((item) => item.status !== "eliminated")
.map((item) => item.time)
.filter((time) => CLOCK_TIME.test(time)),
)];
if (fromInference.length >= 2) return fromInference;
return candidateScoresFromDossier(latest).map((item) => item.time);
}
function canAskHoldout(input: {
events?: readonly Readonly<{ usage: string; year: number | null }>[];
oosBlindPrompts?: readonly unknown[] | null;
}): boolean {
if ((input.oosBlindPrompts?.length ?? 0) > 0) return true;
return Boolean(input.events?.some((item) => item.usage === "holdout" && item.year !== null));
}
function holdoutValidationStatus(input: {
events?: readonly Readonly<{ usage: string; year: number | null }>[];
holdoutPassed?: boolean | null;
resultStatus?: string | null;
oosBlindPrompts?: readonly unknown[] | null;
}): HoldoutValidationStatus {
if (input.holdoutPassed === true) return "passed";
if (input.holdoutPassed === false || input.resultStatus === "validation_failed") {
return "failed";
}
if (canAskHoldout(input)) return "not_started";
return "unavailable";
}
function holdoutStatusFromInference(
inference: ReturnType<typeof previousInferenceFromReceipt>,
oosBlindPrompts?: readonly unknown[] | null,
) {
if (!inference) return "unavailable" as const;
return holdoutValidationStatus({
events: inference.events,
holdoutPassed: inference.holdout_passed,
resultStatus: inference.result_status,
oosBlindPrompts,
});
}
function holdoutStatusFromState(
state: InferenceState,
oosBlindPrompts?: readonly unknown[] | null,
) {
return holdoutValidationStatus({
events: state.events,
holdoutPassed: state.holdout_passed,
resultStatus: state.result_status,
oosBlindPrompts,
});
}
export function contrastPacketFromLatestResult(
latest: DecisionDossier["latestResult"] | undefined,
evidence: DecisionDossier["evidence"] = [],
): CandidateContrastPacket {
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
const refinement = refinementFromDecisionReceipt(latest?.decisionReceipt ?? null);
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
const answered = new Set((inference?.answered_probes ?? []).map((item) => item.probe_id));
const fromInference: EngineContrastProbe[] = (inference?.probes ?? []).flatMap((probe) => {
if (answered.has(probe.id) || probe.information_gain <= 0) return [];
if (probe.source === "known_event_quality") return [];
return [{
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
domain: probe.domain,
year: probe.year > 0 ? probe.year : undefined,
user_meaning: probe.question,
information_gain: probe.information_gain,
expected_outcomes: probe.expected_outcomes,
candidate_ids: probe.candidate_ids,
...(probe.choice_kind === "varga_style"
|| probe.choice_kind === "event_quality"
|| probe.choice_kind === "existence"
? { choice_kind: probe.choice_kind }
: {}),
...(probe.style_options?.length ? { style_options: probe.style_options } : {}),
}];
});
const merged = mergeEngineProbes(
fromInference,
refinement.discriminating_event_probes.map((probe) => ({
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
domain: probe.domain,
year: probe.year,
user_meaning: probe.user_meaning,
information_gain: probe.information_gain,
expected_outcomes: probe.expected_outcomes,
candidate_ids: probe.candidate_ids,
choice_kind: probe.choice_kind,
style_options: probe.style_options,
})),
);
return buildCandidateContrastPacket({
candidateSetVersion: inference?.candidate_set_id ?? latest?.resultId ?? "none",
calculationResultId: latest?.resultId ?? null,
engineProbes: merged,
vargaDifferences: [
...(windowScan?.d9_candidates_differ && windowScan.d9_sign_names.length >= 2
? [{ layer: "d9", signs: windowScan.d9_sign_names }]
: []),
...(windowScan?.d10_candidates_differ && windowScan.d10_sign_names.length >= 2
? [{ layer: "d10", signs: windowScan.d10_sign_names }]
: []),
],
candidateTimes: discriminatorCandidateTimes(latest ?? null),
transitions: windowScan?.transitions ?? [],
askedKeys: askedDiscriminatorKeys(latest?.decisionReceipt, evidence),
volunteeredDomains: volunteeredDomainsFromEvidence(evidence),
providedDomains: datedDomainsFromEvidence(evidence),
});
}
function mergeEngineProbes(
...groups: ReadonlyArray<readonly EngineContrastProbe[] | undefined>
): EngineContrastProbe[] {
const byKey = new Map<string, EngineContrastProbe>();
for (const group of groups) {
for (const probe of group ?? []) {
const key = probe.semantic_key?.trim() ?? "";
if (!key) continue;
const current = byKey.get(key);
if (!current || (probe.information_gain ?? 0) > (current.information_gain ?? 0)) {
byKey.set(key, probe);
}
}
}
return [...byKey.values()];
}
export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateContrastPacket {
return contrastPacketFromLatestResult(dossier.latestResult, dossier.evidence);
}
export function rectificationFollowupCatalog(
latest: DecisionDossier["latestResult"] | undefined,
evidence: DecisionDossier["evidence"] = [],
) {
const receipt = latest?.decisionReceipt ?? null;
const refinement = refinementFromDecisionReceipt(receipt);
const inference = previousInferenceFromReceipt(receipt);
return {
contrastPacket: contrastPacketFromLatestResult(latest ?? null, evidence),
topCandidateTimes: discriminatorCandidateTimes(latest ?? null),
askedProbeKeys: askedDiscriminatorKeys(receipt, evidence),
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
precisionStage: refinement.precision_stage?.current ?? null,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
holdoutEvents: (inference?.events ?? [])
.filter((item) => item.usage === "holdout")
.map((item) => ({ domain: item.domain, year: item.year })),
};
}
function contrastPacketFromState(state: InferenceState): CandidateContrastPacket {
const answered = new Set(state.answered_probes.map((item) => item.probe_id));
return buildCandidateContrastPacket({
candidateSetVersion: state.candidate_set_id,
calculationResultId: null,
engineProbes: state.probes
.filter((item) => !answered.has(item.id))
.map((item) => ({
semantic_key: item.semantic_key,
candidate_split_hash: item.candidate_split_hash,
domain: item.domain,
year: item.year,
user_meaning: item.question,
information_gain: item.information_gain,
expected_outcomes: item.expected_outcomes,
candidate_ids: item.candidate_ids,
...(item.choice_kind === "varga_style"
|| item.choice_kind === "event_quality"
|| item.choice_kind === "existence"
? { choice_kind: item.choice_kind }
: {}),
...(item.style_options?.length ? { style_options: item.style_options } : {}),
})),
candidateTimes: state.candidates
.filter((item) => item.status !== "eliminated")
.map((item) => item.time),
askedKeys: state.answered_probes.map((item) => item.semantic_key),
});
}
function scoreableSnapshotCurrentFromDossier(
dossier: DecisionDossier,
options: { currentEvidenceFingerprint?: string | null } | undefined,
inference: ReturnType<typeof previousInferenceFromReceipt>,
): boolean {
const latest = dossier.latestResult;
if (!latest) return true;
const stored = candidateSnapshotSource({
birthProfileFingerprint: latest.candidateRangeFingerprint ?? "",
scoreableEvidenceFingerprint: latest.evidenceLedgerFingerprint ?? "",
inferenceRevision: inference?.revision ?? 0,
candidateSetVersion: inference?.candidate_set_id ?? latest.resultId ?? "",
scoringPolicyVersion: String(latest.policyVersion ?? latest.algorithmVersion ?? ""),
});
const currentFingerprint = options?.currentEvidenceFingerprint
?? evidenceLedgerFingerprint(dossier.evidence as never);
const current = candidateSnapshotSource({
...stored,
scoreableEvidenceFingerprint: currentFingerprint ?? "",
});
return storedSnapshotIsCurrent(stored, current);
}
function mergeDroppedProbes(
...groups: readonly (readonly DroppedProbe[] | undefined)[]
): DroppedProbe[] {
const byKey = new Map<string, DroppedProbe>();
for (const group of groups) {
for (const item of group ?? []) {
if (!byKey.has(item.semantic_key)) byKey.set(item.semantic_key, item);
}
}
return [...byKey.values()];
}
export function followupAsksRenderableDiscriminator(
followup: { intent?: string; choice_frame?: unknown } | null | undefined,
): boolean {
return followup?.intent === "distinguish_candidates" && Boolean(followup.choice_frame);
}
export type DecideFromDossierOptions = Readonly<{
currentEvidenceFingerprint?: string | null;
birthDate?: string | null;
}>;
function decisionBudgetFromInference(inference: InferenceState | null | undefined) {
const rounds = inference?.rounds ?? [];
let plateauRounds = 0;
for (let index = rounds.length - 1; index >= 0; index -= 1) {
if (rounds[index]?.kind !== "low_information") break;
plateauRounds += 1;
}
return {
// The existing convergence evaluator budgets informative rounds. Keep the
// authoritative decision aligned with that persisted interpretation.
inferenceRounds: rounds.filter((item) => item.kind === "informative").length,
effectiveAnswerCount: inference?.answered_probes.length ?? 0,
plateauRounds,
};
}
function completedProbeForSemanticKey(
semanticKey: string,
packet: CandidateContrastPacket | null | undefined,
eventProbes: readonly DiscriminatingEventProbe[] | undefined,
): CandidateDiscriminatorProbe | null {
const fromPacket = (packet?.probes ?? []).find((item) => item.semanticKey === semanticKey);
if (fromPacket) {
const completed = withCompletedContrastOptions(fromPacket);
return completed.ok ? completed.probe : fromPacket;
}
const event = (eventProbes ?? []).find((item) => item.semantic_key === semanticKey);
if (!event) return null;
const built = buildCandidateContrastPacket({
candidateSetVersion: event.candidate_set_version ?? event.candidate_split_hash ?? "event",
calculationResultId: null,
engineProbes: [{
semantic_key: event.semantic_key,
candidate_split_hash: event.candidate_split_hash,
domain: event.domain,
year: event.year,
user_meaning: event.user_meaning,
question: event.user_meaning,
information_gain: event.information_gain,
expected_outcomes: event.expected_outcomes,
...(event.choice_kind ? { choice_kind: event.choice_kind } : {}),
...(event.style_options?.length ? { style_options: event.style_options } : {}),
}],
candidateTimes: [...(event.candidate_ids ?? [])],
});
const matched = built.probes.find((item) => item.semanticKey === semanticKey) ?? built.probes[0] ?? null;
if (!matched) return null;
const completed = withCompletedContrastOptions(matched);
return completed.ok ? completed.probe : matched;
}
function discriminatorProbeIfFollowupCanAsk(input: {
dossier: DecisionDossier;
inspected: ReturnType<typeof inspectDiscriminatorProbes>;
contrastPacket?: CandidateContrastPacket;
askedKeys?: readonly string[];
birthDate?: string | null;
}): {
probe: CandidateDiscriminatorProbe | null;
dropped: DroppedProbe[];
} {
const catalog = rectificationFollowupCatalog(input.dossier.latestResult, input.dossier.evidence);
const plan = buildMethodFollowupPlan({
evidence: input.dossier.evidence,
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: input.dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: "discriminate_candidates",
...catalog,
...(input.contrastPacket ? { contrastPacket: input.contrastPacket } : {}),
...(input.askedKeys ? { askedProbeKeys: input.askedKeys } : {}),
...(input.birthDate ? { birthDate: input.birthDate } : {}),
candidatesSeparated: false,
});
const dropped = mergeDroppedProbes(input.inspected.dropped, plan.dropped_probes);
if (!followupAsksRenderableDiscriminator(plan.next_followup)) {
return { probe: null, dropped };
}
const key = plan.next_followup?.semantic_key;
const packet = input.contrastPacket ?? catalog.contrastPacket;
const matched = key
? completedProbeForSemanticKey(key, packet, catalog.eventProbes)
: null;
return { probe: matched ?? input.inspected.selected, dropped };
}
export function decideFromDossier(
dossier: DecisionDossier,
options?: DecideFromDossierOptions,
): RectificationDecision {
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const oosBlindPrompts = refinementFromDecisionReceipt(
dossier.latestResult?.decisionReceipt ?? null,
).oos_blind_prompts;
const trainingGate = trainingScoreableGate(dossier.evidence);
const collecting = buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: "collect_evidence",
});
const latest = dossier.latestResult;
const decisionBudget = decisionBudgetFromInference(inference);
const snapshotCurrent = scoreableSnapshotCurrentFromDossier(dossier, options, inference);
const confirmationGate = buildConfirmationGate({
engineConfirmationAllowed: latest?.confirmationAllowed === true,
candidates: (latest?.candidates ?? []).map((candidate) => ({
time: candidate.time,
rank: candidate.rank ?? Number.MAX_SAFE_INTEGER,
tiedMinuteCount: candidate.tiedMinuteCount ?? Number.MAX_SAFE_INTEGER,
})),
decisionReceipt: latest?.decisionReceipt ?? null,
});
const askedKeys = askedDiscriminatorKeys(dossier.latestResult?.decisionReceipt, dossier.evidence);
const mentionedKeys = mentionedVargaKeysFromLedgerEvidence(dossier.evidence);
const topCandidateTimes = discriminatorCandidateTimes(dossier.latestResult);
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier), {
askedKeys,
mentionedKeys,
topCandidateTimes,
});
const gated = discriminatorProbeIfFollowupCanAsk({
dossier,
inspected,
askedKeys,
birthDate: options?.birthDate,
});
return {
...decideRectification({
methodCoverageAll: blockingMethodsCovered(collecting.methods),
trainingGateOpen: trainingGate.open,
confirmationAllowed: confirmationGate.confirmation_allowed,
userStopped: dossier.case.status === "paused",
snapshotCurrent,
candidateScores: candidateScoresFromDossier(dossier.latestResult),
discriminatorProbe: gated.probe,
holdoutValidation: holdoutStatusFromInference(inference, oosBlindPrompts),
accepted: Boolean(dossier.case.acceptedTime),
inferenceCredibleRange: inference?.credible_range ?? null,
engineAcceptAllowed: latest?.selectionAllowed === true
|| latest?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: latest?.decisionReceipt?.propose_allowed === true,
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudget,
}),
droppedProbes: gated.dropped,
};
}
export function decideAfterInferenceChange(input: {
dossier: DecisionDossier;
state: InferenceState | null;
userStopped: boolean;
birthDate?: string | null;
}): RectificationDecision {
const collecting = buildMethodFollowupPlan({
evidence: input.dossier.evidence,
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: input.dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: "collect_evidence",
});
if (!input.state) {
return decideRectification({
methodCoverageAll: blockingMethodsCovered(collecting.methods),
trainingGateOpen: trainingScoreableGate(input.dossier.evidence).open,
candidateScores: [],
userStopped: input.userStopped,
...decisionBudgetFromInference(null),
});
}
const training = input.state.events.filter((item) => item.usage === "training");
const trainingDomains = new Set(training.map((item) => item.domain));
const contrastPacket = contrastPacketFromState(input.state);
const topCandidateTimes = input.state.candidates
.filter((item) => item.status !== "eliminated")
.map((item) => item.time);
const inspected = inspectDiscriminatorProbes(contrastPacket, {
mentionedKeys: mentionedVargaKeysFromLedgerEvidence(input.dossier.evidence),
askedKeys: input.state.answered_probes.map((item) => item.semantic_key),
topCandidateTimes,
});
const gated = discriminatorProbeIfFollowupCanAsk({
dossier: input.dossier,
inspected,
contrastPacket,
askedKeys: input.state.answered_probes.map((item) => item.semantic_key),
birthDate: input.birthDate,
});
return {
...decideRectification({
methodCoverageAll: blockingMethodsCovered(collecting.methods),
trainingGateOpen: training.length >= MIN_ACCEPTANCE_EVENTS
&& trainingDomains.size >= MIN_ACCEPTANCE_DOMAINS,
candidateScores: input.state.candidates
.filter((item) => item.status !== "eliminated")
.map((item) => ({ time: item.time, score: item.posterior_score })),
discriminatorProbe: gated.probe,
holdoutValidation: holdoutStatusFromState(
input.state,
refinementFromDecisionReceipt(input.dossier.latestResult?.decisionReceipt ?? null).oos_blind_prompts,
),
inferenceCredibleRange: input.state.credible_range,
userStopped: input.userStopped,
accepted: Boolean(input.dossier.case.acceptedTime),
engineAcceptAllowed: input.dossier.latestResult?.selectionAllowed === true
|| input.dossier.latestResult?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: input.dossier.latestResult?.decisionReceipt?.propose_allowed === true,
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudgetFromInference(input.state),
}),
droppedProbes: gated.dropped,
};
}
export function overlayPublicDecision<T extends object>(
snapshot: T,
decision: RectificationDecision,
): T & ReturnType<typeof publicDecisionFields> & {
selectionAllowed: boolean;
validated: boolean;
completionStatus: ReturnType<typeof publicDecisionFields>["completion_status"];
} {
const fields = publicDecisionFields(decision);
const projection = authoritativeCandidateProjection(snapshot as DecisionDossier["latestResult"]);
const inconsistent = !projection.consistent;
return {
...snapshot,
...fields,
...(projection.fromInference
? {
candidates: projection.candidates,
representativeTime: projection.representativeTime,
representative_time: projection.representativeTime,
credibleRange: projection.credibleRange,
credible_range: projection.credibleRange,
}
: {}),
...(inconsistent ? { can_adopt: false, selection_allowed: false } : {}),
selectionAllowed: inconsistent ? false : fields.selection_allowed,
validated: fields.validated,
completionStatus: fields.completion_status,
};
}