fix(rectification): ask tie-break questions before the range card (BUG-685/686/687)
Close leads hold delivery until unused D9/D10 style questions are asked. Tie-break POST merges the new turn into the transcript. Range copy no longer lists declined lines.
This commit is contained in:
@@ -197,6 +197,8 @@ export type RectificationDecision = Readonly<{
|
||||
droppedProbes: readonly DroppedProbe[];
|
||||
stopReason?: EvidenceStopReason | null;
|
||||
terminationCopy?: string | null;
|
||||
/** Close lead with unused D9/D10 style questions: ask those before delivering. */
|
||||
heldForTieBreak?: boolean;
|
||||
}>;
|
||||
|
||||
export type DecideRectificationInput = Readonly<{
|
||||
@@ -228,6 +230,8 @@ export type DecideRectificationInput = Readonly<{
|
||||
targetedCollectExhausted?: boolean;
|
||||
/** Opening search window from `case.candidateRange`. Omit in helper/unit paths. */
|
||||
openingCandidateRange?: readonly [string, string] | null;
|
||||
/** Unasked D9/D10 style questions remain. */
|
||||
pendingTieBreak?: boolean;
|
||||
}>;
|
||||
|
||||
function classifyStop(
|
||||
@@ -329,7 +333,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
}
|
||||
|
||||
if (userStopped && separation.ranked.length > 0) {
|
||||
return completeWithRange(separation, holdout, range, "user_stopped", rangeDeliveryCapability);
|
||||
return deliverRange(input, separation, holdout, range, "user_stopped", rangeDeliveryCapability);
|
||||
}
|
||||
|
||||
if (input.snapshotCurrent === false) {
|
||||
@@ -346,7 +350,8 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
&& !probe
|
||||
&& input.targetedCollectExhausted !== false
|
||||
) {
|
||||
return completeWithRange(
|
||||
return deliverRange(
|
||||
input,
|
||||
separation,
|
||||
holdout,
|
||||
range,
|
||||
@@ -367,6 +372,9 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
&& engineOffers
|
||||
&& !narrowingOpen
|
||||
) {
|
||||
if (shouldHoldForTieBreak(input, separation)) {
|
||||
return holdForTieBreak(separation, holdout, range, rangeDeliveryCapability, stopReason);
|
||||
}
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, rangeDeliveryCapability);
|
||||
}
|
||||
return collect(
|
||||
@@ -382,7 +390,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return collect(separation, holdout, range, probe, capability, stopClass.reason);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted" && stopClass.reason === "user_uncertainty_too_high") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (!separation.sufficient) {
|
||||
if (probe) {
|
||||
@@ -400,7 +408,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return collect(separation, holdout, range, probe, waitToNarrowCapability(capability), stopReason);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (rangeDeliveryCapability.canAdopt && input.methodCoverageAll) {
|
||||
return finish("adopt_representative", {
|
||||
@@ -413,10 +421,10 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
stopReason: "probe_pool_exhausted",
|
||||
});
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, "offer", rangeDeliveryCapability);
|
||||
return deliverRange(input, separation, holdout, range, "offer", rangeDeliveryCapability);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (input.accepted) {
|
||||
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
|
||||
@@ -448,9 +456,12 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
if (probe) {
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability);
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability);
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability);
|
||||
}
|
||||
if (holdout === "unavailable") {
|
||||
if (shouldHoldForTieBreak(input, separation)) {
|
||||
return holdForTieBreak(separation, holdout, range, rangeDeliveryCapability, stopReason);
|
||||
}
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, rangeDeliveryCapability);
|
||||
}
|
||||
return finish("adopt_representative", {
|
||||
@@ -479,7 +490,7 @@ function discriminateOrExhaust(
|
||||
stopReason: EvidenceStopReason | null = null,
|
||||
): RectificationDecision {
|
||||
if (budgetExhausted(input)) {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopReason);
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", capability, stopReason);
|
||||
}
|
||||
return discriminate(separation, holdout, range, probe, capability, stopReason);
|
||||
}
|
||||
@@ -565,6 +576,61 @@ function stillNeedNarrowing(input: DecideRectificationInput): boolean {
|
||||
return input.refreshExhausted === false || input.targetedCollectExhausted === false;
|
||||
}
|
||||
|
||||
export function shouldHoldForTieBreak(
|
||||
input: Pick<DecideRectificationInput, "pendingTieBreak" | "userStopped">,
|
||||
separation: CandidateSeparation,
|
||||
kind?: "user_stopped" | "offer" | "exhausted",
|
||||
): boolean {
|
||||
if (kind === "user_stopped" || input.userStopped === true) return false;
|
||||
if (input.pendingTieBreak !== true) return false;
|
||||
if (separation.ranked.length < 2) return false;
|
||||
return separation.lead <= 1;
|
||||
}
|
||||
|
||||
function holdForTieBreak(
|
||||
separation: CandidateSeparation,
|
||||
holdout: HoldoutValidationStatus,
|
||||
range: readonly [string, string] | null,
|
||||
capability: DeliveryCapability,
|
||||
stopReason: EvidenceStopReason | null = null,
|
||||
): RectificationDecision {
|
||||
return {
|
||||
phase: "discrimination",
|
||||
nextAction: "ask_candidate_discriminator",
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
resultStatus: "discriminating",
|
||||
canOfferRange: false,
|
||||
...waitToNarrowCapability(capability),
|
||||
precisionStage: "theme_refine",
|
||||
activeFocusPolicy: "keep",
|
||||
completionStatus: null,
|
||||
validated: false,
|
||||
credibleRange: range,
|
||||
representativeTime: separation.representativeTime,
|
||||
separation,
|
||||
probe: null,
|
||||
holdoutValidation: holdout,
|
||||
droppedProbes: [],
|
||||
heldForTieBreak: true,
|
||||
...(stopReason ? { stopReason } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function deliverRange(
|
||||
input: DecideRectificationInput,
|
||||
separation: CandidateSeparation,
|
||||
holdout: HoldoutValidationStatus,
|
||||
range: readonly [string, string] | null,
|
||||
kind: "user_stopped" | "offer" | "exhausted",
|
||||
capability: DeliveryCapability,
|
||||
stopReason: EvidenceStopReason | null = null,
|
||||
): RectificationDecision {
|
||||
if (shouldHoldForTieBreak(input, separation, kind)) {
|
||||
return holdForTieBreak(separation, holdout, range, capability, stopReason);
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, kind, capability, stopReason);
|
||||
}
|
||||
|
||||
function clockMinute(value: string): string {
|
||||
const match = /^(\d{1,2}):(\d{2})/.exec(value.trim());
|
||||
if (!match) return value.trim().slice(0, 5);
|
||||
|
||||
Reference in New Issue
Block a user