Merge remote-tracking branch 'origin/staging' into codex/consult-no-presupposition-backtest-20261001
# Conflicts: # CHANGELOG.md # docs/BUG_HISTORY.md
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { postAdoptCheckVerdict } from "./post-adopt-verdict.ts";
|
||||
import { posteriorMap, scoreDeltas } from "../core/decision-fingerprint";
|
||||
import { productDeliveryForDomain } from "./product-delivery.ts";
|
||||
import { nextProbe } from "../core/build-state";
|
||||
@@ -2415,6 +2416,26 @@ ${nextInterview.hostNarration}`;
|
||||
hostNarration = adoptionNarration ?? completedRangeNarration ?? hostNarration;
|
||||
}
|
||||
|
||||
// BUG-1170: an answered post-adopt check says what it meant for the adopted time.
|
||||
const answeredIntent = input.dossier.conversationSummary.activeFocus?.intent;
|
||||
if (accepted && !skipThisProbe && input.userStopped !== true
|
||||
&& (answeredIntent === "reverse_verify" || answeredIntent === "out_of_sample_check")) {
|
||||
const before = inferenceForPersistedAnswer(input.dossier.latestResult?.decisionReceipt ?? null);
|
||||
const after = input.decisionState ?? before;
|
||||
const segmentProduct = input.dossier.case.rectificationDomain !== null && input.dossier.case.rectificationDomain !== undefined;
|
||||
const verdict = postAdoptCheckVerdict({
|
||||
acceptedTime: input.dossier.case.acceptedTime,
|
||||
probe: input.probeId
|
||||
? (after?.probes.find((probe) => probe.id === input.probeId) ?? before?.probes.find((probe) => probe.id === input.probeId) ?? null)
|
||||
: null,
|
||||
answerClass: input.answerClass,
|
||||
candidates: after?.candidates ?? [],
|
||||
segmentBefore: segmentProduct ? before?.segment_summary ?? null : null,
|
||||
segmentAfter: segmentProduct ? after?.segment_summary ?? null : null,
|
||||
});
|
||||
if (verdict) hostNarration = hostNarration ? `${verdict}\n\n${hostNarration}` : verdict;
|
||||
}
|
||||
|
||||
const choiceStem = nextChoiceReady ? renderableChoiceStem(nextFocus) : null;
|
||||
if (choiceStem) {
|
||||
const body = skipThisProbe
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* What a post-adopt check said about the adopted time (BUG-1170, product 10-01 option A).
|
||||
*
|
||||
* The closing line used to be only 「前事核对到这里」: the reader answered and learned nothing.
|
||||
* The verdict reads the answered probe's own expected outcomes for the candidate that holds the
|
||||
* adopted minute, and — in the segment product — whether any target chart's ascendant or tier
|
||||
* moved with this answer. Server facts only; no model text.
|
||||
*/
|
||||
import type { AnswerClass, ConflictProbe, InferenceCandidate } from "../core/types.ts";
|
||||
import { chartSummaryCopy, type SegmentSummary } from "../core/segment-summary.ts";
|
||||
|
||||
export type PostAdoptVerdictInput = Readonly<{
|
||||
acceptedTime: string | null | undefined;
|
||||
probe: ConflictProbe | null | undefined;
|
||||
answerClass: AnswerClass | null | undefined;
|
||||
candidates: readonly InferenceCandidate[];
|
||||
segmentBefore?: SegmentSummary | null;
|
||||
segmentAfter?: SegmentSummary | null;
|
||||
}>;
|
||||
|
||||
function minutes(clock: string): number | null {
|
||||
const match = /^(\d{2}):(\d{2})/.exec(clock);
|
||||
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
|
||||
}
|
||||
|
||||
function inRange(clock: number, range: readonly [string, string]): boolean {
|
||||
const start = minutes(range[0]);
|
||||
const end = minutes(range[1]);
|
||||
if (start === null || end === null) return false;
|
||||
return start <= end ? clock >= start && clock <= end : clock >= start || clock <= end;
|
||||
}
|
||||
|
||||
/** The candidate that stands for the adopted minute: its cluster holds it, else the nearest. */
|
||||
export function candidateForAdoptedMinute(
|
||||
acceptedTime: string,
|
||||
candidates: readonly InferenceCandidate[],
|
||||
): InferenceCandidate | null {
|
||||
const clock = acceptedTime.slice(0, 5);
|
||||
const at = minutes(clock);
|
||||
if (at === null || candidates.length === 0) return null;
|
||||
const holding = candidates.find((candidate) => candidate.time === clock || candidate.cluster_times?.includes(clock))
|
||||
?? candidates.find((candidate) => inRange(at, candidate.cluster_range));
|
||||
if (holding) return holding;
|
||||
const distance = (candidate: InferenceCandidate) => {
|
||||
const value = minutes(candidate.time);
|
||||
if (value === null) return Number.POSITIVE_INFINITY;
|
||||
const raw = Math.abs(value - at);
|
||||
return Math.min(raw, 1440 - raw);
|
||||
};
|
||||
return [...candidates].sort((left, right) => distance(left) - distance(right))[0] ?? null;
|
||||
}
|
||||
|
||||
function chartsMoved(before: SegmentSummary, after: SegmentSummary): boolean {
|
||||
return after.charts.some((chart) => {
|
||||
const prior = before.charts.find((row) => row.chart === chart.chart);
|
||||
return !prior || prior.sign !== chart.sign || prior.tier !== chart.tier;
|
||||
});
|
||||
}
|
||||
|
||||
export function postAdoptCheckVerdict(input: PostAdoptVerdictInput): string | null {
|
||||
const clock = input.acceptedTime?.trim().slice(0, 5) ?? "";
|
||||
if (!clock || !input.answerClass) return null;
|
||||
if (input.answerClass === "unsure") return "这件事记不清,不算。";
|
||||
if (!input.probe) return null;
|
||||
const candidate = candidateForAdoptedMinute(clock, input.candidates);
|
||||
const outcome = input.probe.expected_outcomes.find((row) => row.answer_class === input.answerClass) ?? null;
|
||||
const ids = candidate ? new Set([candidate.id, candidate.time]) : new Set<string>();
|
||||
const supports = Boolean(outcome?.supports.some((id) => ids.has(id)));
|
||||
const conflicts = Boolean(outcome?.conflicts.some((id) => ids.has(id)));
|
||||
const fit = supports && !conflicts
|
||||
? `这件事和 ${clock} 对得上。`
|
||||
: conflicts && !supports
|
||||
? `这件事和 ${clock} 对不上;如果别的经历也对不上,可以回到上面的卡片改选。`
|
||||
: `这件事分不出 ${clock} 和别的时间。`;
|
||||
const before = input.segmentBefore ?? null;
|
||||
const after = input.segmentAfter ?? null;
|
||||
if (!before || !after) return fit;
|
||||
return chartsMoved(before, after)
|
||||
? `${fit}盘型结论有变化:${chartSummaryCopy(after)}`
|
||||
: `${fit}盘型结论不变。`;
|
||||
}
|
||||
Reference in New Issue
Block a user