fix(chat): keep rectification sessions findable and keep the delivery card
Independent Staging Quality Gate / validate (push) Failing after 6m45s
Independent Staging Quality Gate / publish (push) Skipped

Rectification answers now advance chat_sessions.updated_at (BUG-704).
A ?c= id missing from the loaded page is fetched before anyone may call
it deleted (BUG-705). The range card no longer has a post-card tie-break
button; live questions leave the card visible with adopt locked
(BUG-706/708). Spoken copy bans 相对支持度 (BUG-709).

Task docs assigned 700-704; qizheng already took 700-703.
This commit is contained in:
jesse-ux
2026-09-15 17:10:53 +08:00
parent d3a2c48ba5
commit f51e494c5a
37 changed files with 799 additions and 177 deletions
@@ -201,6 +201,7 @@ export const RECTIFICATION_USER_COPY = {
rangeDeliveryTieBreakEntry: "再答两道参考题微调排序",
rangeDeliveryTieBreakAck: "只微调排序,不改目前范围。",
rangeDeliveryTieBreakUsed: "这两分钟按现有信息分不开,参考题已经用过。",
rangeDeliveryAdoptLocked: "先答完上面这道,再选时间。",
verificationReportSummary: "查看验证报告",
} as const;
@@ -586,6 +587,7 @@ export function listUserVisibleCopy(): string[] {
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry,
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakAck,
RECTIFICATION_USER_COPY.rangeDeliveryTieBreakUsed,
RECTIFICATION_USER_COPY.rangeDeliveryAdoptLocked,
rangeDeliveryCollectClosed(2),
rangeDeliveryCollectClosed(4),
rangeDeliveryCollectClosed(),
@@ -22,7 +22,9 @@ export const ADOPT_NARRATION_TIMEOUT_MS = 8_000;
export const ADOPT_NARRATION_INSTRUCTIONS = `你只写生时校正采用卡出现时的旁白,不做决定,不改状态,不提问。
用 2 到 4 句中文对用户说清三件事:为什么这一轮不再往下问、现在给的范围和代表分钟是什么、采用之后会用哪些前事核对。
若 post_adopt_verification 为空,必须写「采用后没有还能核对的前事,之后新建对话即按此时间排盘,对不上可改选。」,不得写「会拿……核对」。
只能使用输入事实里出现的时间、年份和相对支持度数字;输入里没有的数字一律不要写。
只能使用输入事实里出现的时间和年份;输入里没有的数字一律不要写。
不要写「相对支持度」「概率」「置信度」。两个时间分不开时说「这两个时间按现有信息分不开」,不要念分数。
只有 stop_facts 里真有「不再问」时才可以那么说。
不要出现「确认」「精确」这两个词,不得再提问,不要写「我按你说的经历认真分析过了,下面是这次的结果」,不要说「当前候选」,不要说「先用着」。`;
export type AdoptNarrationOutcome =
@@ -126,7 +126,7 @@ function stopFactsFromDropped(
if (yearless > 0) {
facts.push({
kind: "yearless_varga",
label: "没有年份的分盘题不再问",
label: "没有年份的分盘题按现有信息分不开",
count: yearless,
});
}
@@ -277,6 +277,7 @@ export function validateAdoptNarration(
return { ok: false, reason: "question" };
}
if (/确认|精确/.test(trimmed)) return { ok: false, reason: "promise" };
if (/相对支持度|概率|置信度/.test(trimmed)) return { ok: false, reason: "machine_voice" };
if (
facts.post_adopt_verification.length === 0
&& /会拿[\s\S]{0,40}核对/.test(trimmed)
@@ -26,4 +26,5 @@ export const MACHINE_VOICE_LEXICON = [
"有关联",
"弱关联",
"以你说的为准",
"相对支持度",
] as const;
@@ -73,6 +73,7 @@ import {
type V9CaseDossier,
} from "./tool-service";
import { CHOICE_SKIP_QUESTION_LABEL, isPersistedFocusId, isTieBreakRoundSchema, type ChoiceKey } from "./choice-card";
import { vargaStyleFollowupRenderable } from "./probe-question-contract";
import { clusterScoreDeltas } from "./probe-explain.ts";
import {
adoptDeliveryFacts,
@@ -1326,6 +1327,21 @@ async function persistFocusAfterChoice(input: {
followup: ReturnType<typeof buildMethodFollowupPlan>["next_followup"];
askedTurnId?: string | null;
}) {
const followupKind = input.followup?.choice_kind
?? input.followup?.choice_frame?.choice_kind
?? null;
const followupStyles = input.followup?.style_options ?? [];
if (followupKind === "varga_style" && followupStyles.length > 0 && !vargaStyleFollowupRenderable({
choiceKind: followupKind,
styleOptions: followupStyles,
})) {
return {
status: "skipped" as const,
focus: null,
questionId: null,
prompt: null,
};
}
let persisted;
try {
persisted = await persistServerOwnedFocus({
@@ -143,6 +143,7 @@ import {
isEventQualityProbe,
sameYearProbeAsked,
SEMANTIC_YEAR_KEY,
vargaStyleFollowupRenderable,
type DroppedProbe,
type ProbeStyleOption,
} from "./probe-question-contract.ts";
@@ -3070,6 +3071,14 @@ export function tieBreakPersonalityFollowup(
if (kind !== "varga_style") return null;
const key = followup.semantic_key ?? "";
if (!key.startsWith("varga.d9") && !key.startsWith("varga.d10")) return null;
const styleOptions = followup.style_options ?? [];
if (styleOptions.length > 0 && !vargaStyleFollowupRenderable({
choiceKind: kind,
styleOptions,
})) {
return null;
}
if (styleOptions.length === 0 && !followup.choice_frame) return null;
return { ...followup, tie_break_round: true };
}
@@ -266,6 +266,22 @@ export function canRenderYearlessChoice(input: {
return !optionCopyPresupposesPeriod(input.styleOptions);
}
/** Same gate as buildChoiceCard: followup exists is not enough. */
export function vargaStyleFollowupRenderable(input: {
choiceKind?: string | null;
styleOptions?: readonly unknown[] | null;
}): boolean {
const styles = completeStyleOptions({
choiceKind: input.choiceKind,
styleOptions: input.styleOptions,
});
if (!styles.ok) return false;
return canRenderYearlessChoice({
choiceKind: input.choiceKind,
styleOptions: styles.options,
});
}
export function isRenderableProbe(input: {
informationGain?: number | null;
candidateIds?: readonly string[] | null;
@@ -167,19 +167,12 @@ export function applyLiveCandidateOffer<T extends CandidateOfferAnchor>(
));
}
if (!input.canOffer) return [...messages];
const liveInterview = messages.some((message) => (
interviewQuestionBlocksAdoptOffer(message.question, false)
));
if (liveInterview) {
return messages.map((message) => (
message.candidateOffer ? { ...message, candidateOffer: undefined } : message
));
}
const owner = [...messages].reverse().find((message) => (
isSettledAssistant(message)
&& !interviewQuestionBlocksAdoptOffer(message.question, false)
));
if (!owner) {
if (messages.some((message) => message.candidateOffer)) return [...messages];
return messages.map((message) => (
message.candidateOffer ? { ...message, candidateOffer: undefined } : message
));