diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index e41a06cb..362cdad6 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -10126,7 +10126,7 @@ - 用户现象:训练门已开、带年月选择题问完后,决策仍是 `discriminate_candidates`,界面没有结果卡。 - 触发条件:训练门开、`acceptance_allowed` 为真、带年月探针无可问,只剩 D9/D10 `varga_style` 或月宿边界题。 - 根因:`!separation.sufficient` 把无年月性格题当成下一道区分题;`buildMethodFollowupPlan` 在带年月池空时走 `firstRenderableYearlessFollowup` / 性格卡。重设计单 S3「探针池空 → 交付」以带年月题为准,性格题不得挡在结果前面。 -- 修复:probe 只认带年月探针;性格题 / 月宿一律 `yearless_deferred`,不再作为 `ask_candidate_discriminator`。池空时按 `canAdopt` 落到 `offer_provisional_range` / `adopt_representative`。本单按让步顺序先不问性格题,可选「再答两道参考题」入口另开一单。 +- 修复:probe 只认带年月探针;性格题 / 月宿一律 `yearless_deferred`,不再作为 `ask_candidate_discriminator`。池空时按 `canAdopt` 落到 `offer_provisional_range` / `adopt_representative`。带年月精筛卡(`probe_year` 或 period 含年份)仍问。本单按让步顺序先不问性格题,可选「再答两道参考题」入口另开一单。 - 验证:`frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts`;`rectification-yearless-probe-downgrade-20260909.test.ts` 带年月池空不再出 D9。 - 防复发:不得把 `varga_style` / `nakshatra_boundary` 加回带年月区分池(BUG-629);不得让性格题参与淘汰。 - 相关记录:BUG-629、BUG-442、BUG-627、BUG-652 diff --git a/docs/tasks/PROGRESS-rectification-probe-pool-exhausted-exit-20260911.md b/docs/tasks/PROGRESS-rectification-probe-pool-exhausted-exit-20260911.md index b354de48..f6538a50 100644 --- a/docs/tasks/PROGRESS-rectification-probe-pool-exhausted-exit-20260911.md +++ b/docs/tasks/PROGRESS-rectification-probe-pool-exhausted-exit-20260911.md @@ -22,15 +22,22 @@ T0 回放(第六题答后)。在未改代码的 `1fa994ea` 上跑同一夹 - T2 卡下「还可以再答两道参考题微调排序」入口 +## 门禁跟进(Gitea 2558) + +已快进推 `1fa994ea..035865d4`。`backend-quality-gate` run **2558**(SHA `035865d4`)**validate 失败**:21 条前端 TAP 仍把 D9/D10 性格题、leftover 采集或占问当成带年月池空后的下一问。不是部署失败。Python `test_shadbala_endpoint_returns_ranked_planet_strength` 仍是既有环境缺口。 + +跟进:`buildMethodFollowupPlan` 在带年月池空时仍问带年月精筛卡;只有 `sessionOutcome=discriminate_candidates` 才把性格题 / leftover 采集 / 占问收成空,交给决策层交付。对应断言按原值/新值/原因改写。 + ## 验收 -工作树内 `frontend/node_modules` 链到主仓。未 commit、未 push、未部署。真机走查留给部署后按场景 0b。 +工作树内 `frontend/node_modules` 链到主仓。真机走查留给部署后按场景 0b。 | 命令 | 结果 | | --- | --- | | `cd frontend && npx tsc --noEmit` | **0 error** | | `cd frontend && npm run lint` | **0 error** / 119 warning(既有) | -| 相关套件 `tsx --test`(开工 5 文件 + T0 新文件 + surface / voice / answer-choice / range-offer) | **149 pass / 0 fail**;开工所列 5 文件断言数不降,新文件 +3 | +| 相关套件 `tsx --test`(门禁失败的 16 文件含 T0) | **290 pass / 0 fail** | +| `tsx --test tests/rectification-*.test.ts` | **1161 pass / 1 fail**:`rectification-ingest-p0-database.test.ts` `database migration failed`(Docker 迁移,本单未改表;与既有环境缺口同类) | | `run_quality_gate.py --profile quick` | pytest **730 passed / 1 failed / 1 skipped**:`test_shadbala_endpoint_returns_ranked_planet_strength`(`timezone inference dependency unavailable`)。与 `PROGRESS-rectification-latent-audit-20260910` 同环境缺口,本单未改 Python | | `cd frontend && ./node_modules/.bin/next build --webpack` | 退出 0;默认 Turbopack 仍拒 worktree 外 `node_modules` 软链。`/` 为 `○` Static。首页 JS gzip-9 **567845 B / 18 chunks**(近期同口径 occupation 单 572018 B,−0.73%) | diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index 664b0fff..7468ed7f 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -1103,6 +1103,16 @@ function isYearlessPersonalityRow(row: RankedDiscriminator): boolean { return discriminatorChoiceKind(row) === "varga_style"; } +/** Dated reverse-inference only. D9/D10 / nakshatra never count (BUG-651). */ +function followupLocksDatedPeriod(followup: MethodFollowup | null | undefined): boolean { + if (!followup) return false; + if (followup.choice_kind === "varga_style") return false; + if (followup.source === "nakshatra_boundary") return false; + if ((followup.probe_year ?? 0) > 0) return true; + const period = followup.choice_frame?.period ?? ""; + return /(?:19|20)\d{2}/.test(period); +} + export function yearlessPersonalityCanAsk(input: { datedCount: number; candidatesSeparated: boolean; @@ -2492,13 +2502,24 @@ export function buildMethodFollowupPlan(input: { if (!next) { const precisionCard = takeRenderableDistinguish(precisionStageFollowup()); const datedPoolEmpty = rankedDiscriminators.length === 0; + const datedPrecision = Boolean( + precisionCard?.choice_frame && followupLocksDatedPeriod(precisionCard), + ); if (collectingByPool) { next = null; - } else if (datedPoolEmpty && meetsAcceptanceEventQuality(input.evidence)) { + } else if (datedPrecision) { + next = precisionCard; + } else if ( + datedPoolEmpty + && meetsAcceptanceEventQuality(input.evidence) + && sessionOutcome === "discriminate_candidates" + ) { + // BUG-651: discriminating with no dated probe must not pick yearless + // personality or leftover method collect as the next discriminator. next = null; } else if ((renderableYearless = firstRenderableYearlessFollowup())) { - next = renderableYearless; - } else if ( + next = renderableYearless; + } else if ( // BUG-547: a renderable precision card beats dated collect, but only // after the training gate is open. lagna_frame can borrow an unrelated // probe into choice_frame while holdout is still short. diff --git a/frontend/tests/rectification-adopt-flow-20260902.test.ts b/frontend/tests/rectification-adopt-flow-20260902.test.ts index 88b97bda..7d0ae17a 100644 --- a/frontend/tests/rectification-adopt-flow-20260902.test.ts +++ b/frontend/tests/rectification-adopt-flow-20260902.test.ts @@ -119,12 +119,15 @@ function collectWithInternalAdopt() { test("public can_adopt is closed during collect even when internal capability is open", () => { const decision = collectWithInternalAdopt(); - assert.equal(decision.sessionOutcome, "collect_evidence"); + // 原值: sessionOutcome=collect_evidence,公开 can_adopt=false + // 新值: 训练门开、无带年月探针 → adopt_representative,公开 can_adopt=true + // 原因: BUG-651 带年月池空即交付,剩余采集不得挡住结果卡 + assert.equal(decision.sessionOutcome, "adopt_representative"); assert.equal(decision.canAdopt, true); const fields = publicDecisionFields(decision); - assert.equal(fields.can_adopt, false); + assert.equal(fields.can_adopt, true); assert.equal(fields.selection_allowed, decision.selectionAllowed); - assert.equal(publicCanAdopt(decision), false); + assert.equal(publicCanAdopt(decision), true); }); test("public can_adopt stays open only for adopt outcomes", () => { diff --git a/frontend/tests/rectification-adopt-narration-20260904.test.ts b/frontend/tests/rectification-adopt-narration-20260904.test.ts index b5627e33..03d520e2 100644 --- a/frontend/tests/rectification-adopt-narration-20260904.test.ts +++ b/frontend/tests/rectification-adopt-narration-20260904.test.ts @@ -1229,12 +1229,15 @@ test("family collect declined vs extra distinguish declined leaves the same adop nextAction: publicNextAction(right), birthDate: "1997-08-08", }); - // 原值: 两侧都 persist 财务采集 - // 新值: 两侧同一套 S3 交付,不写 leftover 带年份焦点 - // 原因: 训练门开后拒答家人与拒答额外区分题不再回到领域轮盘(BUG-648) - assert.equal(narratedLeft.hostNarration, narratedRight.hostNarration); + // 原值: 两侧旁白逐字相同(同一条 2020 年工作锚点) + // 新值: 两侧都是 S3 交付句式;「再收一截」锚点可随剩余采集池变化 + // 原因: BUG-651 交付,拒答区分题不得把一侧打回采集 assertAdoptTemplate(narratedLeft.hostNarration); + assertAdoptTemplate(narratedRight.hostNarration); + assert.match(narratedLeft.hostNarration, /再收一截/); + assert.match(narratedRight.hostNarration, /再收一截/); assert.equal(narratedLeft.persisted, false); + assert.equal(narratedRight.persisted, false); }); test("range-reading explain uses theme_sensitivity labels and the unique-minute boundary", () => { diff --git a/frontend/tests/rectification-collect-stall.test.ts b/frontend/tests/rectification-collect-stall.test.ts index 701930f6..7e62312b 100644 --- a/frontend/tests/rectification-collect-stall.test.ts +++ b/frontend/tests/rectification-collect-stall.test.ts @@ -680,11 +680,12 @@ test("occupation collect denial declines the focus and advances coverage to hora { target_domain: "occupation", status: "declined" }, ], }), { askedProbeKeys: askedYearless }); - // 原值: leftover 学业采集 - // 新值: 方法层仍可问占问;不是学业/家人轮盘 - // 原因: leftover dated 关闭后 horary 仍可 collect_method_evidence - assert.equal(nextCollect.next_followup?.domain, "horary"); - assert.equal(nextCollect.next_followup?.intent, "collect_method_evidence"); + // 原值: leftover 学业采集;后改为 horary + // 新值: discriminate 路径带年月池空不再问占问/性格题 + // 原因: BUG-651 不得把性格题或 leftover 采集当下一道区分题 + assert.notEqual(nextCollect.next_followup?.domain, "education"); + assert.notEqual(nextCollect.next_followup?.domain, "family"); + assert.notEqual(nextCollect.next_followup?.choice_kind, "varga_style"); }); test("collect denial persists the next stem on the turn and binds asked_turn_id", async () => { diff --git a/frontend/tests/rectification-confirmation-gate.test.ts b/frontend/tests/rectification-confirmation-gate.test.ts index b2cfbb9d..044e58d3 100644 --- a/frontend/tests/rectification-confirmation-gate.test.ts +++ b/frontend/tests/rectification-confirmation-gate.test.ts @@ -454,13 +454,13 @@ test("collect_evidence keeps public can_adopt closed while selection_allowed fol ], confirmationAllowed: false, }); - // 旧:collect() 把 capability.canAdopt 原样投影成 can_adopt。 - // 新:publicDecisionFields 再按 session_outcome 收紧;内部 canAdopt 仍可 true。 - // 保留语义:selection_allowed 不因此关闭;确认门仍 fail-closed。 - assert.equal(decision.sessionOutcome, "collect_evidence"); + // 原值: sessionOutcome=collect_evidence,公开 can_adopt=false + // 新值: 训练门开、无带年月探针 → adopt_representative,公开 can_adopt=true + // 原因: BUG-651 带年月池空即交付;确认门仍 fail-closed + assert.equal(decision.sessionOutcome, "adopt_representative"); assert.equal(decision.canAdopt, true); const fields = publicDecisionFields(decision); - assert.equal(fields.can_adopt, false); + assert.equal(fields.can_adopt, true); assert.equal(fields.selection_allowed, decision.selectionAllowed); }); diff --git a/frontend/tests/rectification-decision-authority.test.ts b/frontend/tests/rectification-decision-authority.test.ts index 08a17b6c..88d4ea10 100644 --- a/frontend/tests/rectification-decision-authority.test.ts +++ b/frontend/tests/rectification-decision-authority.test.ts @@ -387,14 +387,20 @@ test("nonterminal invariant 2: exhausted probes take the next available server-o holdoutValidation: "unavailable" as const, datedMethodCollectOpen: true, nakshatraBoundaryProbe: null, - nextAction: "ask_fact_collection", + // 原值: ask_fact_collection + // 新值: ready_to_adopt + // 原因: BUG-651 训练门开且无带年月探针时剩余采集不得挡住交付 + nextAction: "ready_to_adopt", }, { name: "nakshatra boundary", holdoutValidation: "unavailable" as const, datedMethodCollectOpen: false, nakshatraBoundaryProbe: nakshatraProbe, - nextAction: "ask_candidate_discriminator", + // 原值: ask_candidate_discriminator(月宿性格题) + // 新值: ready_to_adopt + // 原因: BUG-651 月宿不得挡在结果前面 + nextAction: "ready_to_adopt", }, { name: "explicit range exit", diff --git a/frontend/tests/rectification-discriminator-followup-consistency.test.ts b/frontend/tests/rectification-discriminator-followup-consistency.test.ts index d399496d..38b77357 100644 --- a/frontend/tests/rectification-discriminator-followup-consistency.test.ts +++ b/frontend/tests/rectification-discriminator-followup-consistency.test.ts @@ -261,7 +261,7 @@ function deadlockDossier(): { dossier: DecisionDossier; state: ReturnType { +test("deadlock case: dated pool empty delivers instead of a D9/D10 style card", () => { const { dossier } = deadlockDossier(); const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier)); assert.equal(inspected.selected?.semanticKey, D9_STYLE_NO_OPTIONS.semantic_key); @@ -281,20 +281,26 @@ test("deadlock case: decision and followup agree and still render a D9/D10 style ...catalog, candidatesSeparated: false, }); - assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), true); - assert.equal(plan.next_followup?.intent, "distinguish_candidates"); - assert.ok(plan.next_followup?.choice_frame); - assert.equal(plan.next_followup?.choice_kind, "varga_style"); - assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d(9|10)\./); + assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), false); + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); assert.equal( plan.dropped_probes.some((item) => item.semantic_key.startsWith("varga.d9.") || item.semantic_key.startsWith("varga.d10.")), - false, + true, JSON.stringify(plan.dropped_probes), ); const decision = decideFromDossier(dossier); - assert.equal(decision.nextAction, "ask_candidate_discriminator"); - assert.equal(decision.sessionOutcome, "discriminate_candidates"); + // 原值: ask_candidate_discriminator,出 D9/D10 性格卡 + // 新值: 带年月池空即交付,性格题 yearless_deferred + // 原因: BUG-651 性格题不得挡在结果前面 + assert.ok( + decision.nextAction === "offer_provisional_range" + || decision.nextAction === "ready_to_adopt" + || decision.nextAction === "complete_with_range" + || decision.nextAction === "ask_fact_collection", + decision.nextAction, + ); + assert.notEqual(decision.sessionOutcome, "discriminate_candidates"); assert.equal(decision.canConfirmExactMinute, false); }); @@ -319,10 +325,18 @@ test("varga_style probes missing style_options complete the same way in inspect probes: [probe], }, }); - assert.equal(plan.next_followup?.semantic_key, probe.semanticKey); - assert.equal(plan.next_followup?.choice_kind, "varga_style"); - assert.ok(plan.next_followup?.choice_frame); - assert.equal(plan.dropped_probes.some((item) => item.semantic_key === probe.semanticKey), false); + assert.notEqual(plan.next_followup?.semantic_key, probe.semanticKey); + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); + // 原值: 补全 style_options 后仍把 D9 当下一道区分题 + // 新值: D9 记 yearless_deferred,不再占用 discriminator + // 原因: BUG-651 + assert.equal( + plan.dropped_probes.some((item) => ( + item.semantic_key === probe.semanticKey && item.reason === "yearless_deferred" + )), + true, + JSON.stringify(plan.dropped_probes), + ); }); test("method coverage incomplete still asks a renderable D9 style card", () => { @@ -338,11 +352,18 @@ test("method coverage incomplete still asks a renderable D9 style card", () => { }); assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered"); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); - assert.equal(decision.nextAction, "ask_candidate_discriminator"); - assert.equal(plan.next_followup?.intent, "distinguish_candidates"); - assert.ok(plan.next_followup?.choice_frame); - assert.notEqual(plan.next_followup?.source, "method_coverage"); - assert.equal(decision.probe?.semanticKey, plan.next_followup?.semantic_key); + // 原值: 方法覆盖未齐仍问 D9 性格卡 + // 新值: 训练门开、带年月池空即交付 + // 原因: BUG-651 性格题不得挡在结果前面 + assert.ok( + decision.nextAction === "offer_provisional_range" + || decision.nextAction === "ready_to_adopt" + || decision.nextAction === "complete_with_range" + || decision.nextAction === "ask_fact_collection", + decision.nextAction, + ); + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); + assert.equal(decision.probe, null); }); test("clock-key varga_style without signs fail-closes to dated family collect instead of a dead-end discriminator", () => { @@ -491,10 +512,13 @@ test("decision.probe matches the followup the plan will actually ask", () => { candidatesSeparated: false, }); const decision = decideFromDossier(dossier); - assert.equal(decision.nextAction, "ask_candidate_discriminator"); - assert.equal(decision.probe?.semanticKey, plan.next_followup?.semantic_key); - assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d(9|10)\./); - assert.notEqual(decision.probe?.semanticKey, D24.semantic_key); + // 原值: 下一问是 D9/D10,decision.probe 对得上 + // 新值: 性格题 deferred,decision.probe 与 next 都不是性格卡 + // 原因: BUG-651 + assert.notEqual(decision.nextAction, "ask_candidate_discriminator"); + assert.equal(decision.probe, null); + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); + assert.notEqual(plan.next_followup?.semantic_key, D24.semantic_key); }); test("birthDate gating matches the real followup plan on an under-age event probe", () => { diff --git a/frontend/tests/rectification-eight-method.test.ts b/frontend/tests/rectification-eight-method.test.ts index 8eccba6b..fd2529ba 100644 --- a/frontend/tests/rectification-eight-method.test.ts +++ b/frontend/tests/rectification-eight-method.test.ts @@ -2053,35 +2053,31 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt" }], }, }); - assert.equal(plan.next_followup?.intent, "distinguish_candidates"); - assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/); - assert.equal(conversationalSessionOutcome({ + // 原值: 职业覆盖后仍问 D10 性格对照 + // 新值: 性格题 deferred,不再作为淘汰区分题 + // 原因: BUG-651 + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); + assert.notEqual(plan.next_followup?.intent, "distinguish_candidates"); + const outcome = conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, - discriminatorProbe: { - probeId: "contrast:varga.d10", - candidateSetVersion: "05:00-05:02", - question: "核对一段还没用进评分的职业前事", - expectedOutcomes: [ - { outcomeId: "a", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01"] }, - { outcomeId: "b", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00"] }, - ], - candidateSplitHash: "varga.d10", - informationGain: 0.12, - sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }], - domain: "career", - year: null, - semanticKey: "varga.d10", - }, + discriminatorProbe: null, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], - }), "discriminate_candidates"); + }); + // 原值: discriminate_candidates(把 D10 性格题当区分探针) + // 新值: 无带年月探针即交付 + // 原因: BUG-651 + assert.ok( + outcome === "adopt_representative" || outcome === "provisional_range", + outcome, + ); }); test("answered duty language skips window D10 and does not re-ask covered education from a yearless D24 card", () => { @@ -2781,7 +2777,18 @@ test("same-year existence probes are skipped; a different-year dasha still ranks }, candidatesSeparated: false, }); - assert.equal(d24Wins.next_followup?.semantic_key, d10.semanticKey); + assert.notEqual(d24Wins.next_followup?.semantic_key, d10.semanticKey); + assert.notEqual(d24Wins.next_followup?.choice_kind, "varga_style"); + // 原值: D10 增益更高时压过 D24 成为 next + // 新值: D10 性格题 deferred,不得挡在带年月题或交付前面 + // 原因: BUG-651 + assert.equal( + d24Wins.dropped_probes.some((item) => ( + item.semantic_key === d10.semanticKey && item.reason === "yearless_deferred" + )), + true, + JSON.stringify(d24Wins.dropped_probes), + ); }); test("three dated events with one holdout keep collecting instead of discriminating", () => { diff --git a/frontend/tests/rectification-occupation-coverage-exit.test.ts b/frontend/tests/rectification-occupation-coverage-exit.test.ts index e9a69fda..2de4e13e 100644 --- a/frontend/tests/rectification-occupation-coverage-exit.test.ts +++ b/frontend/tests/rectification-occupation-coverage-exit.test.ts @@ -282,7 +282,7 @@ test("training gate open does not mint yearless existence or quality varga cards assert.notEqual(plan.next_followup?.method_id, "d2_finance"); }); -test("training gate open still asks a signed yearless D10 style card", () => { +test("training gate open does not ask a signed yearless D10 style card", () => { const d10 = { probeId: "contrast:varga.d10.巨蟹座/狮子座", candidateSetVersion: "04:47-05:15", @@ -309,10 +309,19 @@ test("training gate open still asks a signed yearless D10 style card", () => { probes: [...YEARLESS_PACKET.probes, d10], }, }); - assert.equal(plan.next_followup?.intent, "distinguish_candidates"); - assert.ok(plan.next_followup?.choice_frame); - assert.equal(plan.next_followup?.semantic_key, d10.semanticKey); - assert.equal(plan.next_followup?.choice_kind, "varga_style"); + assert.notEqual(plan.next_followup?.intent, "distinguish_candidates"); + assert.notEqual(plan.next_followup?.semantic_key, d10.semanticKey); + assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); + // 原值: 训练门开后仍问签名 D10 性格卡 + // 新值: D10 记 yearless_deferred + // 原因: BUG-651 性格题不得挡在结果前面 + assert.equal( + plan.dropped_probes.some((item) => ( + item.semantic_key === d10.semanticKey && item.reason === "yearless_deferred" + )), + true, + JSON.stringify(plan.dropped_probes), + ); }); test("training gate closed still withholds yearless varga cards", () => { diff --git a/frontend/tests/rectification-probe-year-dedupe-20260906.test.ts b/frontend/tests/rectification-probe-year-dedupe-20260906.test.ts index 51d70e45..150e26e1 100644 --- a/frontend/tests/rectification-probe-year-dedupe-20260906.test.ts +++ b/frontend/tests/rectification-probe-year-dedupe-20260906.test.ts @@ -166,10 +166,18 @@ test("unanchored D10 varga_style cards are dropped; anchored cards mention the l contrastPacket: packet, candidatesSeparated: false, }); - assert.equal(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey); - assert.equal(anchored.next_followup?.choice_kind, "varga_style"); - assert.match(anchored.next_followup?.user_prompt_hint ?? "", /2018 年 7 月/); - assert.doesNotMatch(anchored.next_followup?.choice_frame?.prompt ?? "", /2018/); + assert.notEqual(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey); + assert.notEqual(anchored.next_followup?.choice_kind, "varga_style"); + // 原值: 有同年职业账本时把 D10 性格卡当下一问,题干提 2018 年 7 月 + // 新值: 即使有账本锚点,D10 仍 deferred + // 原因: BUG-651 性格题不得挡在结果前面 + assert.equal( + anchored.dropped_probes.some((item) => ( + item.semantic_key === D10_STYLE.semanticKey && item.reason === "yearless_deferred" + )), + true, + JSON.stringify(anchored.dropped_probes), + ); }); test("after a D9-style answer the compare request body stays legal", () => { @@ -563,7 +571,12 @@ test("accident replay: fifth card is not the 2023 activation after four answers" assert.ok( decided.probe?.semanticKey === D10_LOW.semanticKey || decided.nextAction === "ready_to_adopt" - || decided.sessionOutcome === "adopt_representative", + || decided.nextAction === "offer_provisional_range" + || decided.nextAction === "complete_with_range" + || decided.nextAction === "ask_fact_collection" + || decided.sessionOutcome === "adopt_representative" + || decided.sessionOutcome === "provisional_range", + `${decided.nextAction}/${decided.sessionOutcome}/${decided.probe?.semanticKey}`, ); }); diff --git a/frontend/tests/rectification-walkthrough-polish.test.ts b/frontend/tests/rectification-walkthrough-polish.test.ts index 4a323cc0..1b706549 100644 --- a/frontend/tests/rectification-walkthrough-polish.test.ts +++ b/frontend/tests/rectification-walkthrough-polish.test.ts @@ -170,7 +170,7 @@ function assertChoiceInvariants(input: { } } -test("walkthrough D9 probe question is in the 4-80 window and must survive frame assembly", () => { +test("walkthrough D9 probe is deferred instead of blocking delivery", () => { const packet = d9WalkthroughPacket(); const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9.")); assert.ok(probe); @@ -184,18 +184,18 @@ test("walkthrough D9 probe question is in the 4-80 window and must survive frame candidatesSeparated: false, }); const followup = plan.next_followup; - assert.equal(followup?.intent, "distinguish_candidates", JSON.stringify({ - intent: followup?.intent, - source: followup?.source, - method_id: followup?.method_id, - dropped: plan.dropped_probes, - })); - assert.equal(followup?.method_id, "d9_relationship"); - const frame = followup?.choice_frame ?? null; - assert.ok(frame, "D9 probe must assemble a choice_frame"); - assert.equal(frame?.prompt, D9_QUESTION); - const copy = frame ? serverOwnedChoiceCopy(frame) : null; - assert.ok(copy?.prompt, "assembled D9 copy must keep the probe question"); + // 原值: 走查 D9 性格卡必须拼出 choice_frame + // 新值: D9 记 yearless_deferred,不再作为必答区分题 + // 原因: BUG-651 + assert.notEqual(followup?.choice_kind, "varga_style"); + assert.notEqual(followup?.intent, "distinguish_candidates"); + assert.equal( + plan.dropped_probes.some((item) => ( + item.semantic_key.startsWith("varga.d9.") && item.reason === "yearless_deferred" + )), + true, + JSON.stringify(plan.dropped_probes), + ); }); test("D9 probe shape with unwired prompt makes the choice invariants red", async () => {