fix(rectification): centralize delivery authority
Independent Staging Quality Gate / validate (push) Successful in 13m0s
Independent Staging Quality Gate / publish (push) Successful in 10m58s

This commit is contained in:
Jesse_Chen
2026-09-01 01:13:04 +08:00
parent 9aebd0474a
commit 9f011194f2
23 changed files with 764 additions and 170 deletions
@@ -56,8 +56,12 @@ export function decideNextAction(input: DecideNextActionInput): RectificationNex
holdoutValidation: input.holdoutValidation,
accepted: input.accepted,
inferenceCredibleRange: input.inferenceCredibleRange,
engineAcceptAllowed: input.selectionAllowed,
engineProposeAllowed: input.proposeAllowed,
engineCeiling: {
acceptanceAllowed: input.selectionAllowed === true,
selectionAllowed: input.selectionAllowed === true,
proposeAllowed: input.proposeAllowed === true,
confirmationAllowed: input.confirmationAllowed === true,
},
inferenceRounds: input.inferenceRounds,
effectiveAnswerCount: input.effectiveAnswerCount,
plateauRounds: input.plateauRounds,
@@ -38,6 +38,61 @@ export type EvidenceStopReason =
| "tied_first"
| "user_uncertainty_too_high";
export type StopClass =
| Readonly<{ kind: "keep_collecting"; reason: "insufficient_dated_events" | "insufficient_domains" }>
| Readonly<{ kind: "exhausted"; reason: "tied_first" | "user_uncertainty_too_high" }>
| Readonly<{ kind: "user_stopped" }>;
export type EngineCapabilityCeiling = Readonly<{
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
}>;
type DeliveryCapability = Readonly<{
canAdopt: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
canConfirmExactMinute: boolean;
}>;
const CLOSED_ENGINE_CAPABILITY_CEILING: EngineCapabilityCeiling = {
acceptanceAllowed: false,
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: false,
};
export function engineCapabilityCeilingFromReceipt(value: unknown): EngineCapabilityCeiling {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return CLOSED_ENGINE_CAPABILITY_CEILING;
}
const row = value as Readonly<Record<string, unknown>>;
const acceptanceAllowed = row.acceptance_allowed;
const selectionAllowed = row.selection_allowed;
const proposeAllowed = row.propose_allowed;
const confirmationAllowed = row.confirmation_allowed;
if (
typeof acceptanceAllowed !== "boolean"
|| typeof selectionAllowed !== "boolean"
|| typeof proposeAllowed !== "boolean"
|| typeof confirmationAllowed !== "boolean"
|| acceptanceAllowed !== selectionAllowed
|| (row.accept_allowed !== undefined
&& (typeof row.accept_allowed !== "boolean" || row.accept_allowed !== selectionAllowed))
|| (row.confirm_allowed !== undefined
&& (typeof row.confirm_allowed !== "boolean" || row.confirm_allowed !== confirmationAllowed))
|| (row.display_allowed !== undefined
&& (typeof row.display_allowed !== "boolean" || (selectionAllowed && !row.display_allowed)))
|| (proposeAllowed && !selectionAllowed)
|| (confirmationAllowed && !selectionAllowed)
) {
return CLOSED_ENGINE_CAPABILITY_CEILING;
}
return { acceptanceAllowed, selectionAllowed, proposeAllowed, confirmationAllowed };
}
export const RECTIFICATION_TERMINATION_COPY = "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。";
/** Standalone range-delivery floor; the exact-minute confirmation gate remains 4/3. */
@@ -98,8 +153,7 @@ export type DecideRectificationInput = Readonly<{
holdoutValidation?: HoldoutValidationStatus;
accepted?: boolean;
inferenceCredibleRange?: readonly [string, string] | null;
engineAcceptAllowed?: boolean;
engineProposeAllowed?: boolean;
engineCeiling: EngineCapabilityCeiling;
datedMethodCollectOpen?: boolean;
/** Server-persisted inference counters; never infer these from chat text. */
inferenceRounds?: number;
@@ -111,28 +165,57 @@ export type DecideRectificationInput = Readonly<{
userUncertaintyHigh?: boolean;
}>;
function evidenceStopReason(
function classifyStop(
input: DecideRectificationInput,
separation: CandidateSeparation,
): EvidenceStopReason | null {
if (separation.ranked.length === 0) return null;
): StopClass | null {
if (
input.datedEventCount !== undefined
&& input.datedEventCount < MIN_STANDALONE_DATED_EVENTS
) {
return "insufficient_dated_events";
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
}
if (
input.datedDomainCount !== undefined
&& input.datedDomainCount < MIN_STANDALONE_DATED_DOMAINS
) {
return "insufficient_domains";
return { kind: "keep_collecting", reason: "insufficient_domains" };
}
if (separation.ranked.length === 0 && !input.discriminatorProbe) {
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
}
if (input.userStopped === true) return { kind: "user_stopped" };
if (separation.tiedForFirst) return { kind: "exhausted", reason: "tied_first" };
if (input.userUncertaintyHigh === true) {
return { kind: "exhausted", reason: "user_uncertainty_too_high" };
}
if (separation.tiedForFirst) return "tied_first";
if (input.userUncertaintyHigh === true) return "user_uncertainty_too_high";
return null;
}
function deliveryCapability(input: {
stopClass: StopClass | null;
separation: CandidateSeparation;
holdout: HoldoutValidationStatus;
engineCeiling: EngineCapabilityCeiling;
confirmationAllowed: boolean;
coverageBlocks: boolean;
}): DeliveryCapability {
const locallySelectable = input.separation.ranked.length > 0
&& !input.coverageBlocks
&& input.stopClass?.kind !== "keep_collecting"
&& input.stopClass?.kind !== "exhausted"
&& input.separation.sufficient
&& input.holdout === "passed";
return {
canAdopt: locallySelectable && input.engineCeiling.acceptanceAllowed,
selectionAllowed: locallySelectable && input.engineCeiling.selectionAllowed,
proposeAllowed: locallySelectable && input.engineCeiling.proposeAllowed,
canConfirmExactMinute: locallySelectable
&& input.confirmationAllowed
&& input.engineCeiling.confirmationAllowed,
};
}
export function decideRectification(input: DecideRectificationInput): RectificationDecision {
const separation = evaluateCandidateSeparation(input.candidateScores);
const probe = input.discriminatorProbe ?? null;
@@ -144,9 +227,63 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
?? (separation.representativeTime
? [separation.representativeTime, separation.representativeTime] as const
: null);
const stopClass = classifyStop(input, separation);
const stopReason = stopClass && "reason" in stopClass ? stopClass.reason : null;
const canDiscriminateDespiteCoverage = Boolean(probe)
&& !separation.sufficient
&& input.trainingGateOpen !== false;
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|| input.trainingGateOpen === false;
const capability = deliveryCapability({
stopClass,
separation,
holdout,
engineCeiling: input.engineCeiling,
confirmationAllowed,
coverageBlocks,
});
const stopReason = evidenceStopReason(input, separation);
if (input.snapshotCurrent === false) {
if (probe && !userStopped && input.trainingGateOpen !== false) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
}
return collect(separation, holdout, range, probe, capability, stopReason);
}
if (userStopped && separation.ranked.length > 0) {
return completeWithRange(separation, holdout, range, "user_stopped", capability);
}
if (coverageBlocks) {
const engineOffers = input.engineCeiling.acceptanceAllowed
|| input.engineCeiling.proposeAllowed;
if (
stopClass?.kind !== "keep_collecting"
&& input.trainingGateOpen !== false
&& separation.ranked.length > 0
&& !probe
&& engineOffers
&& input.datedMethodCollectOpen !== true
) {
return offerRangeWithoutAdopt(separation, holdout, range, capability);
}
return collect(separation, holdout, range, probe, capability, stopReason);
}
if (stopClass?.kind === "keep_collecting") {
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", capability, stopClass.reason);
}
if (!separation.sufficient) {
if (probe) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
}
return stopClass?.kind === "exhausted"
? completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason)
: completeWithRange(separation, holdout, range, "offer", capability);
}
if (stopClass?.kind === "exhausted") {
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
}
if (input.accepted) {
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
input,
@@ -154,7 +291,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: confirmationAllowed,
capability,
});
}
if (confirmationAllowed) {
@@ -164,78 +301,20 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: true,
capability,
});
}
if (userStopped && input.candidateScores.length > 0) {
return completeWithRange(separation, holdout, range, "user_stopped");
}
if (input.snapshotCurrent === false) {
if (probe && !userStopped && input.trainingGateOpen !== false) {
return discriminateOrExhaust(
input,
separation,
holdout,
range,
probe,
);
}
if (!(userStopped && input.candidateScores.length > 0)) {
return collect(separation, holdout, range, probe);
}
}
if (stopReason) {
return completeWithRange(separation, holdout, range, "exhausted", stopReason);
}
const canDiscriminateDespiteCoverage = Boolean(probe)
&& !separation.sufficient
&& input.trainingGateOpen !== false;
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|| input.trainingGateOpen === false;
if (coverageBlocks) {
const trainingOpen = input.trainingGateOpen !== false;
const engineOffers = input.engineAcceptAllowed === true || input.engineProposeAllowed === true;
if (
trainingOpen
&& input.candidateScores.length > 0
&& !probe
&& engineOffers
&& input.datedMethodCollectOpen !== true
) {
return offerRangeWithoutAdopt(separation, holdout, range);
}
return collect(separation, holdout, range, probe);
}
if (!separation.sufficient) {
if (probe && !userStopped) {
return discriminateOrExhaust(input, separation, holdout, range, probe);
}
return completeWithRange(separation, holdout, range, userStopped ? "user_stopped" : "offer");
}
if (holdout === "not_started" && !userStopped) {
return holdoutValidation(separation, range);
if (holdout === "not_started") {
return holdoutValidation(separation, range, capability);
}
if (holdout === "failed") {
if (probe && !userStopped) {
return discriminateOrExhaust(input, separation, holdout, range, probe);
if (probe) {
return discriminateOrExhaust(input, separation, holdout, range, probe, capability);
}
return completeWithRange(separation, holdout, range, "exhausted");
return completeWithRange(separation, holdout, range, "exhausted", capability);
}
if (holdout === "unavailable") {
if (userStopped) {
return completeWithRange(separation, holdout, range, "user_stopped");
}
return finish("adopt_representative", {
input,
separation,
holdout,
range,
probe: null,
canConfirmExactMinute: false,
});
}
if (userStopped && holdout !== "passed") {
return completeWithRange(separation, holdout, range, "user_stopped");
return offerRangeWithoutAdopt(separation, holdout, range, capability);
}
return finish("adopt_representative", {
input,
@@ -243,7 +322,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
range,
probe: null,
canConfirmExactMinute: false,
capability,
});
}
@@ -259,11 +338,13 @@ function discriminateOrExhaust(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
if (budgetExhausted(input)) {
return completeWithRange(separation, holdout, range, "exhausted");
return completeWithRange(separation, holdout, range, "exhausted", capability, stopReason);
}
return discriminate(separation, holdout, range, probe);
return discriminate(separation, holdout, range, probe, capability, stopReason);
}
function collect(
@@ -271,6 +352,8 @@ function collect(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe | null,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
return {
phase: "event_collection",
@@ -278,10 +361,7 @@ function collect(
sessionOutcome: "collect_evidence",
resultStatus: "insufficient_evidence",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "collect_events",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -292,6 +372,7 @@ function collect(
probe,
holdoutValidation: holdout,
droppedProbes: [],
stopReason,
};
}
@@ -299,6 +380,7 @@ function offerRangeWithoutAdopt(
separation: CandidateSeparation,
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
capability: DeliveryCapability,
): RectificationDecision {
return {
phase: "discrimination",
@@ -306,10 +388,7 @@ function offerRangeWithoutAdopt(
sessionOutcome: "provisional_range",
resultStatus: "insufficient_evidence",
canOfferRange: true,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "close",
completionStatus: null,
@@ -361,6 +440,8 @@ function discriminate(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
probe: CandidateDiscriminatorProbe,
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
return {
phase: "discrimination",
@@ -368,10 +449,7 @@ function discriminate(
sessionOutcome: "discriminate_candidates",
resultStatus: "discriminating",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -382,12 +460,14 @@ function discriminate(
probe,
holdoutValidation: holdout,
droppedProbes: [],
...(stopReason ? { stopReason } : {}),
};
}
function holdoutValidation(
separation: CandidateSeparation,
range: readonly [string, string] | null,
capability: DeliveryCapability,
): RectificationDecision {
return {
phase: "holdout_validation",
@@ -395,10 +475,7 @@ function holdoutValidation(
sessionOutcome: "validate_holdout",
resultStatus: "discriminating",
canOfferRange: false,
canAdopt: false,
canConfirmExactMinute: false,
selectionAllowed: false,
proposeAllowed: false,
...capability,
precisionStage: "theme_refine",
activeFocusPolicy: "keep",
completionStatus: null,
@@ -417,6 +494,7 @@ function completeWithRange(
holdout: HoldoutValidationStatus,
range: readonly [string, string] | null,
kind: "user_stopped" | "offer" | "exhausted",
capability: DeliveryCapability,
stopReason: EvidenceStopReason | null = null,
): RectificationDecision {
const userStopped = kind === "user_stopped";
@@ -433,10 +511,7 @@ function completeWithRange(
sessionOutcome,
resultStatus: "completed_with_range",
canOfferRange: true,
canAdopt: true,
canConfirmExactMinute: false,
selectionAllowed: true,
proposeAllowed: true,
...capability,
precisionStage: "ready_to_adopt",
activeFocusPolicy: "close",
completionStatus: userStopped ? "provisional_range_user_stopped" : null,
@@ -460,17 +535,17 @@ function finish(
holdout: HoldoutValidationStatus;
range: readonly [string, string] | null;
probe: CandidateDiscriminatorProbe | null;
canConfirmExactMinute: boolean;
capability: DeliveryCapability;
},
): RectificationDecision {
const completionStatus: CompletionStatus | null = input.canConfirmExactMinute && input.input.accepted
const completionStatus: CompletionStatus | null = input.capability.canConfirmExactMinute && input.input.accepted
? "exact_minute_confirmed"
: input.holdout === "passed"
? "validated_range"
: input.input.userStopped === true
? "provisional_range_user_stopped"
: null;
const kind = input.canConfirmExactMinute
const kind = input.capability.canConfirmExactMinute
? (input.input.accepted ? "exact_minute_confirmed" : "awaiting_confirmation")
: input.holdout === "passed"
? "validated_range"
@@ -483,10 +558,7 @@ function finish(
? "converged"
: "completed_with_range",
canOfferRange: true,
canAdopt: true,
canConfirmExactMinute: input.canConfirmExactMinute,
selectionAllowed: true,
proposeAllowed: true,
...input.capability,
precisionStage: "ready_to_adopt",
activeFocusPolicy: "close",
completionStatus,
@@ -679,7 +679,7 @@ ${nextInterview.hostNarration}`
nextInterviewPersisted = true;
}
}
const adoptionNarration = nextAction.type === "offer_provisional_range" && nextAction.can_adopt
const adoptionNarration = nextAction.can_offer_range && nextAction.can_adopt
? `${nonConvergingRangeNarration({
credibleRange: nextAction.credible_range,
representativeTime: nextAction.representative_time,
@@ -18,6 +18,7 @@ import {
} from "../core/candidate-contrast-packet.ts";
import {
decideRectification,
engineCapabilityCeilingFromReceipt,
publicDecisionFields,
type HoldoutValidationStatus,
type RectificationDecision,
@@ -522,9 +523,7 @@ export function decideFromDossier(
holdoutValidation: holdoutStatusFromInference(inference, oosBlindPrompts),
accepted: Boolean(dossier.case.acceptedTime),
inferenceCredibleRange: inference?.credible_range ?? null,
engineAcceptAllowed: latest?.selectionAllowed === true
|| latest?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: latest?.decisionReceipt?.propose_allowed === true,
engineCeiling: engineCapabilityCeilingFromReceipt(latest?.decisionReceipt ?? null),
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudget,
...evidenceStops,
@@ -553,6 +552,7 @@ export function decideAfterInferenceChange(input: {
trainingGateOpen: trainingScoreableGate(input.dossier.evidence).open,
candidateScores: [],
userStopped: input.userStopped,
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
...decisionBudgetFromInference(null),
...evidenceStops,
});
@@ -593,9 +593,7 @@ export function decideAfterInferenceChange(input: {
inferenceCredibleRange: input.state.credible_range,
userStopped: input.userStopped,
accepted: Boolean(input.dossier.case.acceptedTime),
engineAcceptAllowed: input.dossier.latestResult?.selectionAllowed === true
|| input.dossier.latestResult?.decisionReceipt?.accept_allowed === true,
engineProposeAllowed: input.dossier.latestResult?.decisionReceipt?.propose_allowed === true,
engineCeiling: engineCapabilityCeilingFromReceipt(input.dossier.latestResult?.decisionReceipt ?? null),
datedMethodCollectOpen: datedMethodCollectOpen(collecting.methods),
...decisionBudgetFromInference(input.state),
...evidenceStops,
@@ -616,9 +614,22 @@ export function overlayPublicDecision<T extends object>(
const fields = publicDecisionFields(decision);
const projection = authoritativeCandidateProjection(snapshot as DecisionDossier["latestResult"]);
const inconsistent = !projection.consistent;
const ceiling = engineCapabilityCeilingFromReceipt(
(snapshot as DecisionDossier["latestResult"])?.decisionReceipt ?? null,
);
const canAdopt = !inconsistent && fields.can_adopt && ceiling.acceptanceAllowed;
const selectionAllowed = !inconsistent && fields.selection_allowed && ceiling.selectionAllowed;
const proposeAllowed = !inconsistent && fields.propose_allowed && ceiling.proposeAllowed;
const canConfirmExactMinute = !inconsistent
&& fields.can_confirm_exact_minute
&& ceiling.confirmationAllowed;
return {
...snapshot,
...fields,
can_adopt: canAdopt,
selection_allowed: selectionAllowed,
propose_allowed: proposeAllowed,
can_confirm_exact_minute: canConfirmExactMinute,
...(projection.fromInference
? {
candidates: projection.candidates,
@@ -628,8 +639,7 @@ export function overlayPublicDecision<T extends object>(
credible_range: projection.credibleRange,
}
: {}),
...(inconsistent ? { can_adopt: false, selection_allowed: false } : {}),
selectionAllowed: inconsistent ? false : fields.selection_allowed,
selectionAllowed,
validated: fields.validated,
completionStatus: fields.completion_status,
};
@@ -70,7 +70,9 @@ export type V9EngineScoreResult = Readonly<{
candidates: readonly V9EngineCandidate[];
overallConfidence: "low" | "medium" | "high";
marginPercent: number | null;
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
@@ -237,7 +239,9 @@ type ParsedReceipt = Readonly<{
raw: V9DecisionReceipt;
eventContractVersion: string;
policyVersion: string;
acceptanceAllowed: boolean;
selectionAllowed: boolean;
proposeAllowed: boolean;
confirmationAllowed: boolean;
representativeCandidateId: string | null;
representativeTime: string | null;
@@ -267,6 +271,7 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
|| typeof row.display_allowed !== "boolean"
|| typeof row.selection_allowed !== "boolean"
|| typeof row.acceptance_allowed !== "boolean"
|| typeof row.propose_allowed !== "boolean"
|| typeof row.confirmation_allowed !== "boolean"
|| typeof row.accept_allowed !== "boolean"
|| typeof row.confirm_allowed !== "boolean"
@@ -288,7 +293,6 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
|| (representativeCandidateId !== null && (!representative || representative.time !== representativeTime))
|| (row.confirmation_allowed === true && row.selection_allowed !== true)
|| (row.selection_allowed === true && row.display_allowed !== true)
|| (row.propose_allowed !== undefined && typeof row.propose_allowed !== "boolean")
|| (row.propose_allowed === true && row.selection_allowed !== true)
|| !questionContractVersionIsCompatible(row.question_contract_version ?? row.question_contract)
) {
@@ -298,7 +302,9 @@ function readDecisionReceipt(value: unknown, candidates: readonly V9EngineCandid
raw: row,
eventContractVersion: EVENT_CONTRACT_VERSION,
policyVersion: row.policy_version,
acceptanceAllowed: row.acceptance_allowed,
selectionAllowed: row.selection_allowed,
proposeAllowed: row.propose_allowed,
confirmationAllowed: row.confirmation_allowed,
representativeCandidateId,
representativeTime,
@@ -527,7 +533,9 @@ export async function runV9CandidateScore(input: {
candidates,
overallConfidence: receipt.overallConfidence,
marginPercent: receipt.marginPercent,
acceptanceAllowed: receipt.acceptanceAllowed,
selectionAllowed: receipt.selectionAllowed,
proposeAllowed: receipt.proposeAllowed,
confirmationAllowed: receipt.confirmationAllowed,
representativeCandidateId: receipt.representativeCandidateId,
representativeTime: receipt.representativeTime,
@@ -1139,8 +1139,14 @@ export function decideConversationalSession(input: {
: discriminatorFromFollowup(input.nextFollowup),
holdoutValidation: input.holdoutValidation,
accepted: input.accepted,
engineAcceptAllowed: input.selectionAllowed,
engineProposeAllowed: input.proposeAllowed,
// This helper is not used by the production V9 delivery path; keep its
// delivery ceiling explicit without coupling it to question selection.
engineCeiling: {
acceptanceAllowed: input.selectionAllowed === true,
selectionAllowed: input.selectionAllowed === true,
proposeAllowed: input.proposeAllowed === true,
confirmationAllowed: input.confirmationAllowed === true,
},
datedMethodCollectOpen: input.methods ? datedMethodCollectOpen(input.methods) : undefined,
inferenceRounds: input.inferenceRounds,
effectiveAnswerCount: input.effectiveAnswerCount,