fix(rectification): do not hold delivery on exhausted or closed-ceiling paths (BUG-688)
Style questions still precede a converging range card when a renderable followup exists. Exhausted, closed-ceiling, and holdout-unavailable exits deliver immediately. Hold at most once per Case. Restore the single-gate exit assertion and stop treating the tie-break ack as an exit carrier.
This commit is contained in:
@@ -232,6 +232,10 @@ export type DecideRectificationInput = Readonly<{
|
||||
openingCandidateRange?: readonly [string, string] | null;
|
||||
/** Unasked D9/D10 style questions remain. */
|
||||
pendingTieBreak?: boolean;
|
||||
/** A renderable style followup exists this turn (choice_frame present). */
|
||||
tieBreakFollowupReady?: boolean;
|
||||
/** This Case already entered a style-question hold (at most one round). */
|
||||
tieBreakAlreadyHeld?: boolean;
|
||||
}>;
|
||||
|
||||
function classifyStop(
|
||||
@@ -372,9 +376,6 @@ 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(
|
||||
@@ -411,9 +412,6 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (rangeDeliveryCapability.canAdopt && input.methodCoverageAll) {
|
||||
if (shouldHoldForTieBreak(input, separation)) {
|
||||
return holdForTieBreak(separation, holdout, range, rangeDeliveryCapability, stopReason);
|
||||
}
|
||||
return finish("adopt_representative", {
|
||||
input,
|
||||
separation,
|
||||
@@ -468,9 +466,6 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
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);
|
||||
}
|
||||
if (shouldHoldForTieBreak(input, separation)) {
|
||||
@@ -589,12 +584,17 @@ function stillNeedNarrowing(input: DecideRectificationInput): boolean {
|
||||
}
|
||||
|
||||
export function shouldHoldForTieBreak(
|
||||
input: Pick<DecideRectificationInput, "pendingTieBreak" | "userStopped">,
|
||||
input: Pick<
|
||||
DecideRectificationInput,
|
||||
"pendingTieBreak" | "tieBreakFollowupReady" | "tieBreakAlreadyHeld" | "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 (kind === "user_stopped" || kind === "exhausted" || input.userStopped === true) return false;
|
||||
if (input.tieBreakAlreadyHeld === true) return false;
|
||||
const ready = input.tieBreakFollowupReady ?? input.pendingTieBreak;
|
||||
if (ready !== true) return false;
|
||||
return separation.ranked.length >= 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
isRemainingEvidenceCollect,
|
||||
tieBreakGateInput,
|
||||
tieBreakPersonalityAvailable,
|
||||
tieBreakPersonalityFollowup,
|
||||
} from "./method-followup";
|
||||
import { buildConfirmationGate } from "./confirmation-gate";
|
||||
import {
|
||||
@@ -763,24 +764,53 @@ function windowWidenSuggestedFromDossier(dossier: DecisionDossier, evidenceFinge
|
||||
});
|
||||
}
|
||||
|
||||
function isStyleProbeKey(value: unknown): boolean {
|
||||
const key = String(value ?? "");
|
||||
return key.startsWith("varga.d9") || key.startsWith("varga.d10");
|
||||
}
|
||||
|
||||
function tieBreakHoldFlags(
|
||||
dossier: DecisionDossier,
|
||||
catalog: ReturnType<typeof rectificationFollowupCatalog>,
|
||||
birthDate?: string | null,
|
||||
): {
|
||||
pendingTieBreak: boolean;
|
||||
tieBreakFollowupReady: boolean;
|
||||
tieBreakAlreadyHeld: boolean;
|
||||
} {
|
||||
const gate = tieBreakGateInput({
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog,
|
||||
birthDate,
|
||||
accepted: Boolean(dossier.case.acceptedTime),
|
||||
stage: dossier.case.stage,
|
||||
blockScan: dossier.case.blockScan,
|
||||
reportedBirthTime: dossier.case.reportedBirthTime,
|
||||
candidateRange: dossier.case.candidateRange,
|
||||
});
|
||||
const followup = tieBreakPersonalityFollowup(gate);
|
||||
const asked = catalog.askedProbeKeys ?? [];
|
||||
const answered = catalog.answeredProbes ?? [];
|
||||
const schema = dossier.conversationSummary.activeFocus?.expectedAnswerSchema;
|
||||
const alreadyHeld = asked.some(isStyleProbeKey)
|
||||
|| answered.some((item) => isStyleProbeKey(item.semantic_key))
|
||||
|| Boolean(schema && typeof schema === "object" && schema.tie_break_round === true);
|
||||
return {
|
||||
pendingTieBreak: tieBreakPersonalityAvailable(gate),
|
||||
tieBreakFollowupReady: Boolean(followup?.choice_frame),
|
||||
tieBreakAlreadyHeld: alreadyHeld,
|
||||
};
|
||||
}
|
||||
|
||||
export function decideFromDossier(
|
||||
dossier: DecisionDossier,
|
||||
options?: DecideFromDossierOptions,
|
||||
): RectificationDecision {
|
||||
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
|
||||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
const pendingTieBreak = tieBreakPersonalityAvailable(tieBreakGateInput({
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog,
|
||||
birthDate: options?.birthDate,
|
||||
accepted: Boolean(dossier.case.acceptedTime),
|
||||
stage: dossier.case.stage,
|
||||
blockScan: dossier.case.blockScan,
|
||||
reportedBirthTime: dossier.case.reportedBirthTime,
|
||||
candidateRange: dossier.case.candidateRange,
|
||||
}));
|
||||
const tieBreakHold = tieBreakHoldFlags(dossier, catalog, options?.birthDate);
|
||||
const oosBlindPrompts = refinementFromDecisionReceipt(
|
||||
dossier.latestResult?.decisionReceipt ?? null,
|
||||
).oos_blind_prompts;
|
||||
@@ -892,7 +922,7 @@ export function decideFromDossier(
|
||||
),
|
||||
windowWidenSuggested,
|
||||
openingCandidateRange: openingRangeFromCandidateRange(dossier.case.candidateRange),
|
||||
pendingTieBreak,
|
||||
...tieBreakHold,
|
||||
...narrowingExhaustion(dossier, inference, options, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
@@ -907,18 +937,7 @@ export function decideAfterInferenceChange(input: {
|
||||
snapshotCurrent?: boolean;
|
||||
}): RectificationDecision {
|
||||
const catalog = rectificationFollowupCatalog(input.dossier.latestResult, input.dossier.evidence);
|
||||
const pendingTieBreak = tieBreakPersonalityAvailable(tieBreakGateInput({
|
||||
evidence: input.dossier.evidence,
|
||||
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
closedCollectFocuses: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
catalog,
|
||||
birthDate: input.birthDate,
|
||||
accepted: Boolean(input.dossier.case.acceptedTime),
|
||||
stage: input.dossier.case.stage,
|
||||
blockScan: input.dossier.case.blockScan,
|
||||
reportedBirthTime: input.dossier.case.reportedBirthTime,
|
||||
candidateRange: input.dossier.case.candidateRange,
|
||||
}));
|
||||
const tieBreakHold = tieBreakHoldFlags(input.dossier, catalog, input.birthDate);
|
||||
const collecting = buildMethodFollowupPlan({
|
||||
evidence: input.dossier.evidence,
|
||||
declinedTopics: input.dossier.conversationSummary.declinedSkippedTopics,
|
||||
@@ -974,7 +993,7 @@ export function decideAfterInferenceChange(input: {
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
pendingTieBreak,
|
||||
...tieBreakHold,
|
||||
});
|
||||
}
|
||||
const training = input.state.events.filter((item) => item.usage === "training");
|
||||
@@ -1052,7 +1071,7 @@ export function decideAfterInferenceChange(input: {
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
pendingTieBreak,
|
||||
...tieBreakHold,
|
||||
...narrowingExhaustion(input.dossier, input.state, undefined, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
|
||||
Reference in New Issue
Block a user