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:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-14 — 风格参考题问不出时仍会给出范围卡
|
||||
|
||||
生时校正里,带年月的题问完后仍会先问还没用过的风格参考题。如果这道题这一轮问不出来(比如候选在该分盘上只有一种上升星座),或已经问过一轮,会直接给出范围卡,不会停在没有题目的等待里。Skill 版本不变。
|
||||
|
||||
## 2026-09-14 — 给范围卡之前先问两道风格参考题
|
||||
|
||||
生时校正里,带年月的题问完后、给出范围卡之前,会先连着问还没用过的风格参考题(相处方式、做事风格)。某条线答过「没有」不会把对应的风格题一起关掉。点「再答两道参考题」后,题会出现在对话里。范围卡不再列出你已经说过没有或跳过的线。Skill 版本不变。
|
||||
|
||||
@@ -10709,3 +10709,19 @@
|
||||
- 相关记录:BUG-661、BUG-662
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-688 | 风格题前置把耗尽与收口路径的交付也压住了
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-14
|
||||
- 最近更新:2026-09-14
|
||||
- 影响面:`decideRectification` `shouldHoldForTieBreak`、耗尽 / closed-ceiling 出口
|
||||
- 用户现象:带年月题和定向补事都问完、本应出范围卡时,若风格参考题拿不出来,会话停在「还在区分」且没有题。门禁上 6 条「该交付却不交付」变红。
|
||||
- 触发条件:`pendingTieBreak` 为真,但风格题因分盘只有一种上升星座、焦点落库失败或已 hold 过一次而无法渲染;路径是 exhausted / closed-ceiling / 无载体收口。
|
||||
- 根因:BUG-686 把「还有没问的风格题」做成无条件延迟交付,插在所有交付分支之前,没有出口、没有次数上限。`6fd7925a` 只把断言改绿,行为未修。
|
||||
- 修复:只有真正拿得到可渲染风格题时才 hold;exhausted、closed-ceiling、holdout 不可用的收口不 hold;同一 Case 最多 hold 一轮。恢复「恰好一条交付闸」和「参考题确认语不算出口」两条断言。
|
||||
- 验证:`frontend/tests/rectification-tiebreak-before-card-20260914.test.ts`、`rectification-exhaustion-exit-20260906.test.ts`、`rectification-probe-pool-exhausted-20260911.test.ts`。
|
||||
- 防复发:任何「延迟交付」的守卫都必须带出口,且不得作用于耗尽与收口路径。验收必须比对 `npm test` 失败数与基线。不得把参考题确认语算作出载体。
|
||||
- 相关记录:BUG-686、BUG-656、BUG-674、BUG-680、BUG-597
|
||||
- 复发自:BUG-686(风格题前置扩大到所有交付分支)
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# 进度 · 参考题前置不得压住出口(2026-09-14)
|
||||
|
||||
## 范围
|
||||
|
||||
- 分支:`codex/rectification-tiebreak-hold-exit-20260914`(基于 `origin/staging` @ `ada2cfb1`)
|
||||
- 任务单:`docs/tasks/TASK-rectification-tiebreak-hold-exit-fix-20260914.md`
|
||||
- BUG-688 resolved(复发自 BUG-686)
|
||||
- 未改 `active_rectification_event_engine.py`、确认门、并列判据、Skill 版本
|
||||
|
||||
## 完成
|
||||
|
||||
- T1:`shouldHoldForTieBreak` 要求 `tieBreakFollowupReady`(可渲染风格题);`exhausted` 与 `user_stopped` 不 hold;closed-ceiling(`coverageBlocks && engineOffers && !narrowingOpen`)和 holdout 不可用的收口不 hold。同一 Case 已 hold 过一轮(已问过 `varga.d9` / `varga.d10` 或焦点带 `tie_break_round`)不再 hold。
|
||||
- T2:恢复 `last closed-ceiling card…` 的 `assert.equal(gates.length, 1)`;`exitAppendCalls()` 去掉 `只微调排序`。文案放宽(「范围已经收到 / 能问的都问完了」)保留。
|
||||
- D3 已做,未走让步。
|
||||
|
||||
## 验收数字
|
||||
|
||||
- 定向:`rectification-tiebreak-before-card-20260914.test.ts` 12 条 + `rectification-probe-pool-exhausted-20260911.test.ts` 10 条全绿。`rectification-exhaustion-exit-20260906.test.ts` 本机因 Skill SHA 软链无法导入(Windows 基线缺口),Linux 门禁会跑。
|
||||
- `tsc --noEmit`:0 错
|
||||
- `npx eslint` 改动文件:0 error(2 条既有 unused-vars warning,未顺手改)
|
||||
- Skill 版本不变
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
| `TASK-rectification-targeted-card-dead-20260913.md` | `PROGRESS-rectification-targeted-card-dead-20260913.md` | **P0**:定向补事卡在快照投影里拿不到 `choice_card`(承接焦点分支不重建 `choice_frame`),卡片看得见点不动、流程停在采集等待态;模型还会把定向题改写成口述题(BUG-669~671)。先于 tie-break 修复单执行 | 待验收 | `codex/rectification-targeted-card-dead-20260913` |
|
||||
|
||||
| `TASK-rectification-tiebreak-hold-exit-fix-20260914.md` | `PROGRESS-rectification-tiebreak-hold-exit-20260914.md` | **P0 回归修复单**:BUG-686 的风格题前置把耗尽与收口路径的交付也压住了,`npm test` 由 27 红涨到 33(6 条「该交付却不交付」)。后续 `6fd7925a` 只改断言把它们改绿:4 条文案放宽可接受,2 条实质弱化(「恰好一条交付闸」退化成短路、参考题确认语被算作出口载体)必须恢复;行为层根因未动。hold 要带出口、不得作用于 exhausted/closed-ceiling,最多 hold 一次(BUG-688)。尚未部署,未影响线上 | 待执行 | `codex/rectification-tiebreak-hold-exit-20260914` |
|
||||
| `TASK-rectification-tiebreak-hold-exit-fix-20260914.md` | `PROGRESS-rectification-tiebreak-hold-exit-20260914.md` | **P0 回归修复单**:BUG-686 的风格题前置把耗尽与收口路径的交付也压住了,`npm test` 由 27 红涨到 33(6 条「该交付却不交付」)。后续 `6fd7925a` 只改断言把它们改绿:4 条文案放宽可接受,2 条实质弱化(「恰好一条交付闸」退化成短路、参考题确认语被算作出口载体)必须恢复;行为层根因未动。hold 要带出口、不得作用于 exhausted/closed-ceiling,最多 hold 一次(BUG-688)。尚未部署,未影响线上 | 待验收 | `codex/rectification-tiebreak-hold-exit-20260914` |
|
||||
|
||||
| `TASK-rectification-cluster-width-research-20260914.md` | `PROGRESS-rectification-cluster-width-research-20260914.md` | **研究单**:上一轮证明调权重改不动交付区间宽度——所有方案宽度中位数都等于整个搜索窗。先确认 sweep 的宽度口径是否含淘汰(M0),再画簇结构像(M1),最后量三个改法:放宽簇上限、按分差决定是否合并、交付区间改分位覆盖(M2)。真值覆盖率不得下降 | 待执行 | `codex/rectification-cluster-width-research-20260914` |
|
||||
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ The birth-time rectification session is the consultation transcript plus a house
|
||||
| `question-gap`, delivered | no current question, or the current question is a dead choice card, and the session already delivered a range (`completed_with_range` / `provisional_range` / adopt outcomes, or `tied_first`); range card or range line plus an exit note. Never “没有拿到下一个问题。” | enabled |
|
||||
| `verified_idle` | one closing line `postAdoptVerifyDone` under the still-visible range card (same assistant column); no spinner, no reload | enabled |
|
||||
| `choice-pending` | the answered card (`data-selected` fill, a top row “正在记录…”) and the same live row from “正在记录本次选择…” through the follow-up turn | enabled (typing queues), stop visible |
|
||||
| `candidates` | one range-delivery card titled “目前范围 …(对照了 N 件经历)”, a caption under the title that only names still-open lines (or “能问的都问完了”); optional “再答两道参考题微调排序” only when unused D9/D10 remain; if those were already asked and the top two are still within one point, a line “这两分钟按现有信息分不开,参考题已经用过。”; then up to three compare columns (highest posterior first; “更像这个” adopts); a closed “查看验证报告” fold. Unused style questions are asked before this card appears, whether or not the top two are tied. | enabled |
|
||||
| `candidates` | one range-delivery card titled “目前范围 …(对照了 N 件经历)”, a caption under the title that only names still-open lines (or “能问的都问完了”); optional “再答两道参考题微调排序” only when unused D9/D10 remain; if those were already asked and the top two are still within one point, a line “这两分钟按现有信息分不开,参考题已经用过。”; then up to three compare columns (highest posterior first; “更像这个” adopts); a closed “查看验证报告” fold. On a normal convergence, unused style questions are asked before this card. Exhausted and closed-ceiling exits still deliver the card if a style question cannot be rendered. | enabled |
|
||||
| `adopting` | “正在采用 HH:MM…” through the follow-up turn | enabled (typing queues), stop visible |
|
||||
| `confirmed` | “已确认校正时间:HH:MM” | enabled |
|
||||
| `readonly` | “该校正已结束,只能查看历史。” and “再次校正” | disabled |
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -554,7 +554,7 @@ function assistantAppendCalls(calls: Array<{ fn: string; args: Record<string, un
|
||||
function exitAppendCalls(calls: Array<{ fn: string; args: Record<string, unknown> }>) {
|
||||
return assistantAppendCalls(calls).filter((item) => (
|
||||
GATE_SENTENCE.test(String(item.args.p_assistant_message))
|
||||
|| /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了|只微调排序/.test(
|
||||
|| /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/.test(
|
||||
String(item.args.p_assistant_message),
|
||||
)
|
||||
));
|
||||
@@ -858,30 +858,21 @@ test("last closed-ceiling card concatenates the gate into one append", async ()
|
||||
expectedRevision: 6,
|
||||
});
|
||||
const gates = exitAppendCalls(accounting.calls);
|
||||
// 原值: 答完最后一道区分卡立刻 append 1 条含「目前范围」的交付闸
|
||||
// 新值: 交付旁白改收口「能问的都问完了」;若风格参考题未用完则先问参考题(0 条交付闸)
|
||||
// 原因: BUG-686 / BUG-687
|
||||
assert.ok(
|
||||
gates.length === 1
|
||||
|| /只微调排序|能问的都问完了|范围已经收到|目前范围/.test(applied.narration ?? ""),
|
||||
JSON.stringify({
|
||||
gates: gates.map((item) => item.args.p_assistant_message),
|
||||
narration: applied.narration,
|
||||
}),
|
||||
);
|
||||
// 原值: 恰好 1 条交付闸,旁白含「目前范围」
|
||||
// 新值: 恰好 1 条交付闸,旁白可含「范围已经收到 / 能问的都问完了」
|
||||
// 原因: BUG-688 恢复「恰好一条」;文案放宽来自 BUG-687
|
||||
assert.equal(gates.length, 1, JSON.stringify(gates.map((item) => item.args.p_assistant_message)));
|
||||
assert.doesNotMatch(applied.narration, /领域不限|做不了|材料不够|还差 \d+ 件/);
|
||||
assert.match(
|
||||
applied.narration,
|
||||
/范围还能再收一截|目前范围|就能开始筛|当前范围|范围已经收到|能问的都问完了|只微调排序/,
|
||||
/范围还能再收一截|目前范围|就能开始筛|当前范围|范围已经收到|能问的都问完了/,
|
||||
);
|
||||
assert.equal(
|
||||
(String(gates[0]?.args.p_assistant_message).match(
|
||||
/范围还能再收一截|目前范围|就能开始筛|范围已经收到|能问的都问完了/g,
|
||||
) ?? []).length > 0,
|
||||
true,
|
||||
);
|
||||
if (gates.length === 1) {
|
||||
assert.equal(
|
||||
(String(gates[0]?.args.p_assistant_message).match(
|
||||
/范围还能再收一截|目前范围|就能开始筛|范围已经收到|能问的都问完了|只微调排序/g,
|
||||
) ?? []).length > 0,
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("closed ceiling with a leftover discriminator persists the card not the gate", async () => {
|
||||
|
||||
@@ -784,20 +784,16 @@ test("T3: skipped persist still leaves a non-empty carrier; 没有了 delivers t
|
||||
declinedTopics: TARGETED_ALL_DECLINED,
|
||||
});
|
||||
const delivered = decideFromDossier(allDeclined, { birthDate: "1997-08-08" });
|
||||
// 原值: 全部定向补事拒答后立刻 offer_provisional_range / ready_to_adopt / complete_with_range
|
||||
// 新值: 风格参考题未用完时先 ask_candidate_discriminator
|
||||
// 原因: BUG-686 出卡前先问参考题
|
||||
// 原值: 风格题前置把 closed-ceiling 也 hold 成 ask_candidate_discriminator
|
||||
// 新值: 耗尽/收口路径直接交付
|
||||
// 原因: BUG-688 D2
|
||||
assert.ok(
|
||||
delivered.heldForTieBreak === true
|
||||
|| delivered.nextAction === "offer_provisional_range"
|
||||
delivered.nextAction === "offer_provisional_range"
|
||||
|| delivered.nextAction === "ready_to_adopt"
|
||||
|| delivered.nextAction === "complete_with_range",
|
||||
delivered.nextAction,
|
||||
);
|
||||
// 原值: canOfferRange true
|
||||
// 新值: 卡住参考题时 false
|
||||
// 原因: BUG-686
|
||||
assert.equal(delivered.canOfferRange, delivered.heldForTieBreak !== true);
|
||||
assert.equal(delivered.canOfferRange, true);
|
||||
const catalog = rectificationFollowupCatalog(allDeclined.latestResult, allDeclined.evidence);
|
||||
assert.equal(
|
||||
targetedCollectPool(
|
||||
@@ -821,11 +817,15 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
|
||||
declinedTopics: TARGETED_ALL_DECLINED,
|
||||
});
|
||||
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
|
||||
// 原值: offer_provisional_range / ready_to_adopt / complete_with_range
|
||||
// 新值: 未用的 D10 风格参考题先占位,nextAction=ask_candidate_discriminator
|
||||
// 原因: BUG-686 出卡前先问参考题
|
||||
assert.equal(decision.heldForTieBreak, true, decision.nextAction);
|
||||
assert.equal(decision.nextAction, "ask_candidate_discriminator");
|
||||
// 原值: heldForTieBreak 把 closed-ceiling 压成 ask_candidate_discriminator
|
||||
// 新值: 定向补事关完后交付范围卡
|
||||
// 原因: BUG-688 D2 耗尽/收口不得 hold
|
||||
assert.ok(
|
||||
decision.nextAction === "offer_provisional_range"
|
||||
|| decision.nextAction === "ready_to_adopt"
|
||||
|| decision.nextAction === "complete_with_range",
|
||||
decision.nextAction,
|
||||
);
|
||||
const delivery = rangeDeliveryForSnapshot({
|
||||
decisionReceipt: dossier.latestResult?.decisionReceipt,
|
||||
candidates: dossier.latestResult?.candidates,
|
||||
@@ -851,17 +851,28 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /能把 04:48 和 05:07 分开/);
|
||||
assert.doesNotMatch(delivery.narrow_hint ?? "", /这次给出|最终/);
|
||||
const publicAction = publicNextAction(decision);
|
||||
// 原值: can_offer_range true,nextUser 为出卡
|
||||
// 新值: 卡住参考题时不出卡
|
||||
// 原因: BUG-686
|
||||
assert.equal(publicAction.can_offer_range, false);
|
||||
assert.equal(publicAction.can_offer_range, true);
|
||||
assert.equal(rectificationQuestionGapState({
|
||||
liveQuestionVisible: false,
|
||||
questionMissing: true,
|
||||
questionLoadFailed: false,
|
||||
collectWaiting: false,
|
||||
busy: false,
|
||||
readonly: false,
|
||||
regenerating: false,
|
||||
snapshotLoaded: true,
|
||||
resumableCase: true,
|
||||
retryAttempts: 0,
|
||||
offerAwaitingReader: publicAction.can_offer_range,
|
||||
}), "idle");
|
||||
const idle = await persistNextInterviewIfIdle({
|
||||
accounting: idleHandlers(dossier).client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
});
|
||||
assert.match(idle.hostNarration ?? "", /只微调排序|目前范围|能问的都问完了/);
|
||||
assert.match(idle.hostNarration ?? "", /目前范围|范围已经收到|能问的都问完了/);
|
||||
assert.doesNotMatch(idle.hostNarration ?? "", /这次给出|最终/);
|
||||
assert.doesNotMatch(idle.hostNarration ?? "", /平时做事|月宿性格/);
|
||||
});
|
||||
|
||||
test("T0: GET-selected receipt probe is rejected until refresh merges it into inference_state", async () => {
|
||||
|
||||
@@ -121,6 +121,29 @@ function tiedDecision(overrides: Partial<Parameters<typeof decideRectification>[
|
||||
});
|
||||
}
|
||||
|
||||
/** Normal convergence: unique first place, methods covered, holdout passed. */
|
||||
function convergingDecision(overrides: Partial<Parameters<typeof decideRectification>[0]> = {}) {
|
||||
return decideRectification({
|
||||
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
|
||||
methodCoverageAll: true,
|
||||
trainingGateOpen: true,
|
||||
candidateScores: [
|
||||
{ time: "04:53", score: 24 },
|
||||
{ time: "05:00", score: 14 },
|
||||
{ time: "05:06", score: 13 },
|
||||
],
|
||||
discriminatorProbe: null,
|
||||
holdoutValidation: "passed",
|
||||
datedEventCount: 3,
|
||||
datedDomainCount: 3,
|
||||
targetedCollectExhausted: true,
|
||||
refreshExhausted: true,
|
||||
tieBreakFollowupReady: true,
|
||||
pendingTieBreak: true,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test("a tie-break snapshot turn is merged into messages with its question", () => {
|
||||
type Message = {
|
||||
role: "assistant";
|
||||
@@ -198,8 +221,8 @@ test("a delivery card plus an unanswered focus is not an idle gap", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("a first-place tie with unused personality questions holds delivery", () => {
|
||||
const decision = tiedDecision({ pendingTieBreak: true });
|
||||
test("a converging case with unused personality questions holds delivery", () => {
|
||||
const decision = convergingDecision();
|
||||
assert.equal(decision.heldForTieBreak, true);
|
||||
assert.notEqual(decision.nextAction, "complete_with_range");
|
||||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
||||
@@ -231,19 +254,40 @@ test("the same tie delivers after both personality questions are used", () => {
|
||||
});
|
||||
|
||||
test("a two-point lead still asks personality questions before delivery", () => {
|
||||
const decision = tiedDecision({
|
||||
pendingTieBreak: true,
|
||||
candidateScores: [
|
||||
{ time: "04:53", score: 16 },
|
||||
{ time: "05:00", score: 14 },
|
||||
{ time: "05:06", score: 13 },
|
||||
],
|
||||
});
|
||||
const decision = convergingDecision();
|
||||
assert.equal(decision.heldForTieBreak, true);
|
||||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), false);
|
||||
assert.equal(tieBreakPersonalityFollowup(planInput())?.choice_kind, "varga_style");
|
||||
});
|
||||
|
||||
test("pending style flag without a renderable followup delivers", () => {
|
||||
const decision = convergingDecision({
|
||||
pendingTieBreak: true,
|
||||
tieBreakFollowupReady: false,
|
||||
});
|
||||
assert.equal(decision.heldForTieBreak ?? false, false);
|
||||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
||||
});
|
||||
|
||||
test("an exhausted first-place tie delivers even if style questions remain", () => {
|
||||
const decision = tiedDecision({
|
||||
pendingTieBreak: true,
|
||||
tieBreakFollowupReady: true,
|
||||
});
|
||||
assert.equal(decision.heldForTieBreak ?? false, false);
|
||||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
||||
});
|
||||
|
||||
test("already held once without a followup delivers", () => {
|
||||
const decision = convergingDecision({
|
||||
pendingTieBreak: true,
|
||||
tieBreakFollowupReady: true,
|
||||
tieBreakAlreadyHeld: true,
|
||||
});
|
||||
assert.equal(decision.heldForTieBreak ?? false, false);
|
||||
assert.equal(sessionOutcomeAllowsDelivery(decision.sessionOutcome), true);
|
||||
});
|
||||
|
||||
test("a layer with only one rising sign does not occupy the style slot", () => {
|
||||
const onlyD10 = {
|
||||
...planInput(),
|
||||
@@ -262,7 +306,7 @@ test("a layer with only one rising sign does not occupy the style slot", () => {
|
||||
|
||||
test("holding for a tie-break does not mark a delivery turn", () => {
|
||||
resetDeliveryTurnGuardForTests();
|
||||
const decision = tiedDecision({ pendingTieBreak: true });
|
||||
const decision = convergingDecision();
|
||||
assert.equal(decision.heldForTieBreak, true);
|
||||
markDeliveryTurn({ caseId: "case-1", resultId: "result-1" });
|
||||
assert.equal(alreadyDelivered({
|
||||
|
||||
Reference in New Issue
Block a user