fix(rectification): show question stems and unblock adopt after occupation
Independent Staging Quality Gate / validate (push) Successful in 13m40s
Independent Staging Quality Gate / publish (push) Successful in 9m59s

Choice legends and spoken collect prompts were hidden after BUG-461, and occupation still blocked canAdopt once dated coverage was done. Restore visible stems and stop polling extra collects when adopt is allowed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 11:09:56 +08:00
co-authored by Cursor
parent 058e5db9b5
commit 1a3e14e726
17 changed files with 272 additions and 164 deletions
+14 -9
View File
@@ -2956,6 +2956,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-block: var(--space-3);
margin-inline-start: var(--assistant-content-inset);
}
.rectification-question-slot__prompt {
margin: 0;
color: var(--color-ink);
font-size: var(--type-body-sm);
font-weight: 600;
line-height: 1.55;
}
.rectification-question-slot__status {
margin: 0;
color: var(--color-ink-secondary);
@@ -2988,16 +2995,14 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
border-radius: var(--radius-lg);
background: var(--color-canvas-soft);
}
.rectification-choice-card .birth-time-choice-question legend.sr-only {
position: absolute;
width: 1px;
height: 1px;
.rectification-choice-card .birth-time-choice-question legend {
display: block;
margin: 0 0 var(--space-3);
padding: 0;
margin: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
color: var(--color-ink);
font-size: var(--type-body-sm);
font-weight: 600;
line-height: 1.55;
}
.rectification-choice-card .birth-time-choice-question:disabled .birth-time-choice-option {
cursor: default;
@@ -325,6 +325,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
const boardId = useId();
const boardTitleId = useId();
const collectSpokenPromptId = useId();
useLayoutEffect(() => {
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
@@ -1229,6 +1230,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onStop={submitStop}
/>
)}
{collectSpokenPrompt && !showLiveChoiceCard && !readonly && (
<p id={collectSpokenPromptId} className="rectification-question-slot__prompt">
{collectSpokenPrompt}
</p>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
@@ -1278,6 +1284,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
<Textarea
ref={composer}
aria-label={readonly ? "该校正已结束,只能查看历史" : "继续描述你的经历或回答"}
aria-describedby={collectSpokenPrompt && !showLiveChoiceCard && !readonly
? collectSpokenPromptId
: undefined}
value={draft}
disabled={!canSend}
placeholder={readonly
@@ -48,7 +48,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
disabled={props.pending || props.disabled || answered}
data-answered={answered ? "true" : "false"}
>
<legend className="sr-only">{props.card.prompt}</legend>
<legend>{props.card.prompt}</legend>
{props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
<div className="birth-time-primary-choices">
{props.card.options.map((option, index) => (
@@ -78,6 +78,29 @@ function openingRangeFromDossier(dossier: {
return openingRangeFromCandidateRange(dossier.case?.candidateRange ?? null);
}
function shouldSkipFollowupPersist(input: {
canAdopt: boolean;
nextAction: string;
}): boolean {
return input.canAdopt
&& input.nextAction !== "ask_fact_collection"
&& input.nextAction !== "ask_candidate_discriminator"
&& input.nextAction !== "ask_holdout_validation";
}
function adoptHostNarration(input: {
credibleRange?: readonly [string, string] | null;
representativeTime?: string | null;
openingRange: readonly [string, string] | null;
receipt: Readonly<Record<string, unknown>> | null | undefined;
}): string {
return withProspectiveWindows(deliveryAdoptNarration({
credibleRange: input.credibleRange,
representativeTime: input.representativeTime,
openingRange: input.openingRange,
}), input.receipt);
}
export type ApplyChoiceCommand = Readonly<{
userId: string;
caseId: string;
@@ -316,6 +339,21 @@ export async function persistNextInterviewAfterChoice(input: {
nextAction: ReturnType<typeof publicNextAction>;
birthDate?: string | null;
}): Promise<{ hostNarration: string; choiceReady: boolean; persisted?: boolean }> {
if (shouldSkipFollowupPersist({
canAdopt: input.nextAction.can_adopt,
nextAction: input.nextAction.type,
})) {
return {
hostNarration: adoptHostNarration({
credibleRange: input.nextAction.credible_range,
representativeTime: input.nextAction.representative_time,
openingRange: openingRangeFromDossier(input.dossier),
receipt: input.dossier.latestResult?.decisionReceipt,
}),
choiceReady: false,
persisted: false,
};
}
const latest = input.dossier.latestResult
? {
...input.dossier.latestResult,
@@ -553,20 +591,16 @@ export async function persistNextInterviewIfIdle(input: {
birthDate = null;
}
const decision = decideFromDossier(dossier, { birthDate });
if (
decision.canAdopt
&& decision.nextAction !== "ask_fact_collection"
&& decision.nextAction !== "ask_candidate_discriminator"
&& decision.nextAction !== "ask_holdout_validation"
) {
if (shouldSkipFollowupPersist(decision)) {
return {
persisted: false,
choiceReady: false,
hostNarration: withProspectiveWindows(deliveryAdoptNarration({
hostNarration: adoptHostNarration({
credibleRange: decision.credibleRange,
representativeTime: decision.representativeTime,
openingRange: openingRangeFromDossier(dossier),
}), dossier.latestResult?.decisionReceipt),
receipt: dossier.latestResult?.decisionReceipt,
}),
};
}
if (isNonConvergingRangeOffer(decision)) {
@@ -22,10 +22,13 @@
* Finance/health score if volunteered; they are not method-layer rotation.
* Method coverage finishes before repeating a precision-stage ask.
* Appearance and marks are skipped_by_policy. Horary does not block offering
* time cards. Occupation still blocks adopt and confirmation until a note
* exists, but once the training gate is open it must not also block yearless
* varga cards. Career evidence plus a closed occupation collect focus covers
* occupation without depending on the model picking domain=occupation.
* time cards. Occupation is a method-layer note, not an adopt gate: dated
* dasha/D9/D10/relatives coverage plus the engine ceiling can offer
* representative cards while occupation remains uncovered. Yearless-to-collect
* conversion still waits until occupation is covered, so that note can be
* asked later without blocking adopt. Career evidence
* plus a closed occupation collect focus still covers the occupation method
* without depending on the model picking domain=occupation.
* Method coverage asks for dated events in natural language.
* Known-event quality probes (exam went badly for a year already
* in the ledger) are not reverse-inference cards. Dasha existence
@@ -205,7 +208,6 @@ const BLOCKING_COVERAGE_IDS = new Set<MethodFollowupId>([
"d9_relationship",
"d10_career",
"relatives",
"occupation",
]);
function isConfirmedDated(item: MethodFollowupEvidence): boolean {
@@ -1685,7 +1687,7 @@ export function buildMethodFollowupPlan(input: {
source: "method_coverage",
});
}
if (!next && dashaCovered && coverageComplete) {
if (!next && dashaCovered && coverageComplete && occupationCovered) {
const allowLowGainDiscriminator = !coverageComplete || !candidatesSeparated;
const yearless = yearlessDiscriminators[0];
if (yearless && (allowLowGainDiscriminator || yearless.score >= 0.08)) {
@@ -33,9 +33,10 @@ export function dossierHasNonemptyAssistantBody(
/**
* Whether to append the server-owned focus prompt as its own assistant turn.
*
* Structured only: action, focus kind/intent, and whether this turn already
* has a nonempty assistant body. Never inspect the body text to decide
* whether it already asked.
* Structured only: focus kind/intent, and whether this turn already has a
* nonempty assistant body. Never inspect the body text to decide
* whether it already asked. Collect prompts live in the question slot,
* so a second assistant turn is only written when this turn has no body.
*/
export function shouldPersistFocusPromptTurn(input: {
action: RectificationRouteAction;
@@ -45,7 +46,7 @@ export function shouldPersistFocusPromptTurn(input: {
}): boolean {
const collect = input.questionKind === "collect_spoken"
|| input.questionIntent === "collect_method_evidence";
return !(input.action === "opening" && collect && input.assistantBodyPresent);
return !(collect && input.assistantBodyPresent);
}
export async function finalizeSuccessfulTurnExit(input: {