fix(rectification): persist stop scores and deliver a range card (BUG-581–582)
Independent Staging Quality Gate / validate (push) Successful in 14m1s
Independent Staging Quality Gate / publish (push) Failing after 19m46s

Stop/idle reuse the scored evidence path so existing answers stay on the range. Delivery uses one range card (Skill 10.0.15) instead of four minute cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-07 19:22:01 +08:00
co-authored by Cursor
parent 058ad693cd
commit ab57d03f06
55 changed files with 2853 additions and 670 deletions
@@ -34,12 +34,12 @@ import {
import {
canShowRectificationReadonlyRange,
canShowRectificationSelectionCards,
isRecommendedRectificationCandidate,
natalRecastMeaning,
parseRectificationCandidateResult,
workingRectificationHouseTable,
type RectificationCandidateResult,
} from "@/lib/rectification-candidate-result";
import { RectificationRangeDelivery } from "@/components/rectification-range-delivery";
import {
diffRectificationBoard,
RECTIFICATION_BOARD_SPLIT_MIN_PX,
@@ -177,51 +177,6 @@ function RectificationReadonlyRange({
);
}
function RectificationCandidateCards({
result,
acceptingCandidateId,
readonly,
onAccept,
}: Readonly<{
result: RectificationCandidateResult;
acceptingCandidateId: string | null;
readonly: boolean;
onAccept: (candidateId: string) => void;
}>) {
return (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong></strong>
<span></span>
</div>
<div className="rectification-candidate-list">
{result.candidates.map((candidate) => {
const selected = result.selectedTime === candidate.time;
const recommended = isRecommendedRectificationCandidate(result, candidate);
return (
<button
type="button"
className={`rectification-candidate${selected ? " is-selected" : ""}`}
key={candidate.candidateId}
disabled={selected || Boolean(acceptingCandidateId) || readonly}
onClick={() => onAccept(candidate.candidateId)}
>
<span className="rectification-candidate-time">
<strong>{candidate.time}</strong>
{selected && <span className="rectification-candidate-badge"></span>}
{recommended && <span className="rectification-candidate-badge"></span>}
</span>
<span className="rectification-candidate-support"> {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : result.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);
})}
</div>
</section>
);
}
type RectificationAgenticChatProps = Readonly<{
caseId: string;
@@ -503,6 +458,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0);
const [openingRequested, setOpeningRequested] = useState(false);
const [acceptingCandidateId, setAcceptingCandidateId] = useState<string | null>(null);
const [dismissedRangeResultId, setDismissedRangeResultId] = useState<string | null>(null);
const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
const [copiedMessageKey, setCopiedMessageKey] = useState<string | null>(null);
const [regeneratingMessageKey, setRegeneratingMessageKey] = useState<string | null>(null);
@@ -1285,6 +1241,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
resultId: candidateResult.resultId,
candidateId,
requestId: globalThis.crypto.randomUUID(),
mode: "range",
}),
},
);
@@ -1695,12 +1652,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onRegenerate={() => void regenerateMessage(message)}
/>
)}
{showSelectionCards && message.renderKey === selectionCardMessageKey && candidateResult && (
<RectificationCandidateCards
{showSelectionCards && dismissedRangeResultId !== candidateResult.resultId && message.renderKey === selectionCardMessageKey && candidateResult && (
<RectificationRangeDelivery
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly || busy || regeneratingMessageKey !== null}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
onContinue={() => setDismissedRangeResultId(candidateResult.resultId)}
/>
)}
{showReadonlyRange && message.renderKey === latestSettledAssistant?.renderKey && candidateResult?.credibleRange && (
@@ -0,0 +1,159 @@
"use client";
import { useState } from "react";
import type { RectificationCandidateResult } from "@/lib/rectification-candidate-result";
import {
RECTIFICATION_USER_COPY,
REPRESENTATIVE_MINUTE_DISCLAIMER,
rangeDeliveryEventCopy,
rangeDeliverySensitiveCopy,
rangeDeliveryStableCopy,
} from "@/lib/rectification-agentic/user-copy";
import type { RangeDeliveryProjection } from "@/lib/rectification-agentic/v9/divergence-panel";
function adoptCandidateId(
result: RectificationCandidateResult,
selectedColumnId: string | null,
): string | null {
const delivery = result.rangeDelivery;
if (selectedColumnId) return selectedColumnId;
if (delivery?.representative_candidate_id) return delivery.representative_candidate_id;
const wanted = delivery?.representative_time ?? result.representativeTime;
if (!wanted) return result.candidates[0]?.candidateId ?? null;
return result.candidates.find((item) => item.time === wanted)?.candidateId
?? result.candidates[0]?.candidateId
?? null;
}
export function RectificationRangeDelivery({
result,
acceptingCandidateId,
readonly,
onAccept,
onContinue,
}: Readonly<{
result: RectificationCandidateResult;
acceptingCandidateId: string | null;
readonly: boolean;
onAccept: (candidateId: string) => void;
onContinue: () => void;
}>) {
const delivery: RangeDeliveryProjection | null = result.rangeDelivery;
const [selectedColumnId, setSelectedColumnId] = useState<string | null>(null);
const range = delivery?.range ?? result.credibleRange;
const representative = delivery?.representative_time ?? result.representativeTime;
const selected = Boolean(result.selectedTime);
const busy = Boolean(acceptingCandidateId) || readonly;
const panel = delivery?.divergence;
const showPanel = panel?.eligible === true && panel.columns.length >= 2 && !selected;
const candidateId = adoptCandidateId(result, selectedColumnId);
const adopting = Boolean(acceptingCandidateId);
return (
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正区间">
<div className="rectification-candidates-heading">
<strong>{RECTIFICATION_USER_COPY.rangeDeliveryTitle}</strong>
{range ? (
<span className="rectification-range-delivery__range">{range[0]}{range[1]}</span>
) : null}
</div>
<dl className="rectification-range-delivery__facts">
{representative ? (
<>
<dt>{RECTIFICATION_USER_COPY.rangeDeliveryRepresentativeLabel}</dt>
<dd>{representative}</dd>
</>
) : null}
{(delivery?.event_count ?? 0) > 0 ? (
<>
<dt>{RECTIFICATION_USER_COPY.rangeDeliveryEventsLabel}</dt>
<dd>{rangeDeliveryEventCopy(delivery!.event_count)}</dd>
</>
) : null}
{(delivery?.stable_themes.length ?? 0) > 0 ? (
<>
<dt>{RECTIFICATION_USER_COPY.rangeDeliveryStableLabel}</dt>
<dd>{rangeDeliveryStableCopy(delivery!.stable_themes)}</dd>
</>
) : null}
{(delivery?.sensitive_themes.length ?? 0) > 0 ? (
<>
<dt>{RECTIFICATION_USER_COPY.rangeDeliverySensitiveLabel}</dt>
<dd>{rangeDeliverySensitiveCopy(delivery!.sensitive_themes)}</dd>
</>
) : null}
</dl>
<p className="rectification-range-delivery__boundary">
{delivery?.boundary ?? REPRESENTATIVE_MINUTE_DISCLAIMER}
</p>
{showPanel && (
<div className="rectification-divergence">
<p className="rectification-divergence__title">{RECTIFICATION_USER_COPY.divergenceTitle}</p>
<div className="rectification-divergence-list">
{panel.columns.map((column) => {
const id = column.representative_candidate_id;
const active = selectedColumnId === id;
return (
<button
type="button"
key={`${column.cluster_range[0]}-${column.cluster_range[1]}`}
className={`rectification-divergence-column${active ? " is-selected" : ""}`}
aria-pressed={active}
disabled={busy}
onClick={() => setSelectedColumnId(active ? null : id)}
>
<strong>{column.cluster_range[0]}{column.cluster_range[1]} </strong>
{column.rows.map((row) => (
<span key={`${row.layer}-${row.sign}`}>{row.label}</span>
))}
</button>
);
})}
</div>
<div className="rectification-divergence-clear">
<button
type="button"
className="rectification-range-delivery__quiet"
disabled={busy || selectedColumnId === null}
onClick={() => setSelectedColumnId(null)}
>
{RECTIFICATION_USER_COPY.divergenceUnlike}
</button>
<button
type="button"
className="rectification-range-delivery__quiet"
disabled={busy || selectedColumnId === null}
onClick={() => setSelectedColumnId(null)}
>
{RECTIFICATION_USER_COPY.divergenceUnsure}
</button>
</div>
</div>
)}
<div className="rectification-range-delivery__actions">
{selected ? (
<p className="rectification-range-delivery__adopted" role="status"></p>
) : (
<>
<button
type="button"
className="rectification-range-delivery__adopt"
disabled={busy || !candidateId}
onClick={() => candidateId && onAccept(candidateId)}
>
{adopting ? "正在采用…" : RECTIFICATION_USER_COPY.rangeDeliveryAdopt}
</button>
<button
type="button"
className="rectification-range-delivery__continue"
disabled={busy}
onClick={onContinue}
>
{RECTIFICATION_USER_COPY.rangeDeliveryContinue}
</button>
</>
)}
</div>
</section>
);
}