fix(rectification): count timeline width inclusively and keep eliminated minutes hollow (BUG-602, BUG-603)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1421,6 +1421,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
searchWindow,
|
||||
credibleRange: candidateResult?.credibleRange ?? null,
|
||||
candidateTimes: candidateResult?.candidates.map((candidate) => candidate.time) ?? [],
|
||||
inferenceMarks: candidateResult?.inferenceMarks ?? [],
|
||||
stage: caseStage,
|
||||
});
|
||||
const persistedOfferKey = [...messages].reverse().find((message) => message.candidateOffer)?.renderKey;
|
||||
|
||||
@@ -66,6 +66,11 @@ export type RectificationNatalRecast = Readonly<{
|
||||
confirmation_allowed: false;
|
||||
}>;
|
||||
|
||||
export type RectificationInferenceMark = Readonly<{
|
||||
time: string;
|
||||
eliminated: boolean;
|
||||
}>;
|
||||
|
||||
export type RectificationCandidateResult = Readonly<{
|
||||
resultId: string;
|
||||
candidates: readonly RectificationCandidate[];
|
||||
@@ -95,6 +100,8 @@ export type RectificationCandidateResult = Readonly<{
|
||||
credibleRange: readonly [string, string] | null;
|
||||
rangeDelivery: RangeDeliveryProjection | null;
|
||||
verificationReportMarkdown: string | null;
|
||||
/** Inference-layer minutes including eliminated ones. Empty when no inference_state. */
|
||||
inferenceMarks?: readonly RectificationInferenceMark[];
|
||||
}>;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
@@ -284,6 +291,24 @@ function parseCredibleRange(value: unknown): readonly [string, string] | null {
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
function parseInferenceMarks(receipt: Record<string, unknown> | null): readonly RectificationInferenceMark[] {
|
||||
const state = record(receipt?.inference_state);
|
||||
if (!state || !Array.isArray(state.candidates)) return [];
|
||||
const marks: RectificationInferenceMark[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of state.candidates) {
|
||||
const row = record(value);
|
||||
const clock = time(row?.time);
|
||||
if (!clock || seen.has(clock)) continue;
|
||||
seen.add(clock);
|
||||
marks.push({
|
||||
time: clock,
|
||||
eliminated: row?.status === "eliminated",
|
||||
});
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
|
||||
export function parseRectificationCandidateResult(value: unknown): RectificationCandidateResult | null {
|
||||
const snapshot = record(value);
|
||||
if (!snapshot || typeof snapshot.resultId !== "string") return null;
|
||||
@@ -372,6 +397,7 @@ export function parseRectificationCandidateResult(value: unknown): Rectification
|
||||
?? verificationMarkdownFromUnknown(
|
||||
(snapshot.rangeDelivery ?? snapshot.range_delivery) as { verification_markdown?: unknown } | undefined,
|
||||
),
|
||||
inferenceMarks: parseInferenceMarks(receipt),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export type RectificationTimelineView = Readonly<{
|
||||
ticks: readonly TimelineTick[];
|
||||
/** `05:07–05:09` */
|
||||
rangeLabel: string;
|
||||
/** `2 分钟` */
|
||||
/** `3 分钟` */
|
||||
widthLabel: string;
|
||||
}>;
|
||||
|
||||
@@ -131,12 +131,23 @@ function buildTicks(axisStart: number, axisEnd: number): TimelineTick[] {
|
||||
return ticks;
|
||||
}
|
||||
|
||||
export type TimelineInferenceMark = Readonly<{
|
||||
time: string;
|
||||
eliminated: boolean;
|
||||
}>;
|
||||
|
||||
export type RectificationTimelineInput = Readonly<{
|
||||
/** `candidate_range`: the case's current search window, and the axis. */
|
||||
searchWindow: readonly [string, string] | null;
|
||||
/** `credible_range`; absent during block scan, where the band is the window. */
|
||||
credibleRange: readonly [string, string] | null;
|
||||
candidateTimes: readonly string[];
|
||||
/**
|
||||
* Inference-layer minutes with elimination status. When present and non-empty,
|
||||
* these are the marks; `eliminated` is the only in/out judge. When absent,
|
||||
* engine `candidateTimes` are drawn solid.
|
||||
*/
|
||||
inferenceMarks?: readonly TimelineInferenceMark[];
|
||||
stage: RectificationTimelineStage | null;
|
||||
}>;
|
||||
|
||||
@@ -177,9 +188,12 @@ export function buildRectificationTimeline(
|
||||
// boundaries do not reach the client as structured data (see PROGRESS).
|
||||
const marks: TimelineMark[] = [];
|
||||
if (input.stage === "minute") {
|
||||
const sourced = input.inferenceMarks && input.inferenceMarks.length > 0
|
||||
? input.inferenceMarks.map((mark) => ({ time: mark.time, eliminated: mark.eliminated }))
|
||||
: input.candidateTimes.map((time) => ({ time, eliminated: false }));
|
||||
const seen = new Set<number>();
|
||||
for (const raw of input.candidateTimes) {
|
||||
const parsed = parseClockMinutes(raw);
|
||||
for (const item of sourced) {
|
||||
const parsed = parseClockMinutes(item.time);
|
||||
if (parsed === null) continue;
|
||||
const at = alignToAxis(parsed, axisStart, axisEnd);
|
||||
if (at < axisStart || at > axisEnd) continue;
|
||||
@@ -188,8 +202,8 @@ export function buildRectificationTimeline(
|
||||
marks.push({
|
||||
key: `m${at}`,
|
||||
percent: timelinePercent(at, axisStart, axisEnd),
|
||||
// Closed interval: a candidate sitting on a boundary is still in range.
|
||||
state: at >= bandStart && at <= bandEnd ? "in" : "out",
|
||||
// Status from the inference layer, never band position (BUG-603).
|
||||
state: item.eliminated ? "out" : "in",
|
||||
});
|
||||
}
|
||||
marks.sort((left, right) => left.percent - right.percent);
|
||||
@@ -203,6 +217,6 @@ export function buildRectificationTimeline(
|
||||
marks,
|
||||
ticks: buildTicks(axisStart, axisEnd),
|
||||
rangeLabel: `${formatClockMinutes(bandStart)}–${formatClockMinutes(bandEnd)}`,
|
||||
widthLabel: timelineDurationLabel(bandEnd - bandStart),
|
||||
widthLabel: timelineDurationLabel(bandEnd - bandStart + 1),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user