fix(rectification): keep dated precision cards after empty dated discriminator pool (BUG-651)

Staging gate 2558 failed because leftover tests still expected D9/D10 or leftover collect after the pool emptied. Discriminate sessions still deliver; dated precision cards still ask.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-11 15:22:55 +08:00
co-authored by Cursor
parent 035865d4c1
commit f870d3d757
13 changed files with 184 additions and 90 deletions
+1 -1
View File
@@ -10126,7 +10126,7 @@
- 用户现象:训练门已开、带年月选择题问完后,决策仍是 `discriminate_candidates`,界面没有结果卡。 - 用户现象:训练门已开、带年月选择题问完后,决策仍是 `discriminate_candidates`,界面没有结果卡。
- 触发条件:训练门开、`acceptance_allowed` 为真、带年月探针无可问,只剩 D9/D10 `varga_style` 或月宿边界题。 - 触发条件:训练门开、`acceptance_allowed` 为真、带年月探针无可问,只剩 D9/D10 `varga_style` 或月宿边界题。
- 根因:`!separation.sufficient` 把无年月性格题当成下一道区分题;`buildMethodFollowupPlan` 在带年月池空时走 `firstRenderableYearlessFollowup` / 性格卡。重设计单 S3「探针池空 → 交付」以带年月题为准,性格题不得挡在结果前面。 - 根因:`!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。 - 验证:`frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts``rectification-yearless-probe-downgrade-20260909.test.ts` 带年月池空不再出 D9。
- 防复发:不得把 `varga_style` / `nakshatra_boundary` 加回带年月区分池(BUG-629);不得让性格题参与淘汰。 - 防复发:不得把 `varga_style` / `nakshatra_boundary` 加回带年月区分池(BUG-629);不得让性格题参与淘汰。
- 相关记录:BUG-629、BUG-442、BUG-627、BUG-652 - 相关记录:BUG-629、BUG-442、BUG-627、BUG-652
@@ -22,15 +22,22 @@ T0 回放(第六题答后)。在未改代码的 `1fa994ea` 上跑同一夹
- T2 卡下「还可以再答两道参考题微调排序」入口 - 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 && npx tsc --noEmit` | **0 error** |
| `cd frontend && npm run lint` | **0 error** / 119 warning(既有) | | `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 | | `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% | | `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% |
@@ -1103,6 +1103,16 @@ function isYearlessPersonalityRow(row: RankedDiscriminator): boolean {
return discriminatorChoiceKind(row) === "varga_style"; 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: { export function yearlessPersonalityCanAsk(input: {
datedCount: number; datedCount: number;
candidatesSeparated: boolean; candidatesSeparated: boolean;
@@ -2492,13 +2502,24 @@ export function buildMethodFollowupPlan(input: {
if (!next) { if (!next) {
const precisionCard = takeRenderableDistinguish(precisionStageFollowup()); const precisionCard = takeRenderableDistinguish(precisionStageFollowup());
const datedPoolEmpty = rankedDiscriminators.length === 0; const datedPoolEmpty = rankedDiscriminators.length === 0;
const datedPrecision = Boolean(
precisionCard?.choice_frame && followupLocksDatedPeriod(precisionCard),
);
if (collectingByPool) { if (collectingByPool) {
next = null; 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; next = null;
} else if ((renderableYearless = firstRenderableYearlessFollowup())) { } else if ((renderableYearless = firstRenderableYearlessFollowup())) {
next = renderableYearless; next = renderableYearless;
} else if ( } else if (
// BUG-547: a renderable precision card beats dated collect, but only // BUG-547: a renderable precision card beats dated collect, but only
// after the training gate is open. lagna_frame can borrow an unrelated // after the training gate is open. lagna_frame can borrow an unrelated
// probe into choice_frame while holdout is still short. // probe into choice_frame while holdout is still short.
@@ -119,12 +119,15 @@ function collectWithInternalAdopt() {
test("public can_adopt is closed during collect even when internal capability is open", () => { test("public can_adopt is closed during collect even when internal capability is open", () => {
const decision = collectWithInternalAdopt(); 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); assert.equal(decision.canAdopt, true);
const fields = publicDecisionFields(decision); 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(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", () => { test("public can_adopt stays open only for adopt outcomes", () => {
@@ -1229,12 +1229,15 @@ test("family collect declined vs extra distinguish declined leaves the same adop
nextAction: publicNextAction(right), nextAction: publicNextAction(right),
birthDate: "1997-08-08", birthDate: "1997-08-08",
}); });
// 原值: 两侧都 persist 财务采集 // 原值: 两侧旁白逐字相同(同一条 2020 年工作锚点)
// 新值: 两侧同一套 S3 交付,不写 leftover 带年份焦点 // 新值: 两侧都是 S3 交付句式;「再收一截」锚点可随剩余采集池变化
// 原因: 训练门开后拒答家人与拒答额外区分题不再回到领域轮盘(BUG-648) // 原因: BUG-651 交付,拒答区分题不得把一侧打回采集
assert.equal(narratedLeft.hostNarration, narratedRight.hostNarration);
assertAdoptTemplate(narratedLeft.hostNarration); assertAdoptTemplate(narratedLeft.hostNarration);
assertAdoptTemplate(narratedRight.hostNarration);
assert.match(narratedLeft.hostNarration, /再收一截/);
assert.match(narratedRight.hostNarration, /再收一截/);
assert.equal(narratedLeft.persisted, false); assert.equal(narratedLeft.persisted, false);
assert.equal(narratedRight.persisted, false);
}); });
test("range-reading explain uses theme_sensitivity labels and the unique-minute boundary", () => { test("range-reading explain uses theme_sensitivity labels and the unique-minute boundary", () => {
@@ -680,11 +680,12 @@ test("occupation collect denial declines the focus and advances coverage to hora
{ target_domain: "occupation", status: "declined" }, { target_domain: "occupation", status: "declined" },
], ],
}), { askedProbeKeys: askedYearless }); }), { askedProbeKeys: askedYearless });
// 原值: leftover 学业采集 // 原值: leftover 学业采集;后改为 horary
// 新值: 方法层仍可问占问;不是学业/家人轮盘 // 新值: discriminate 路径带年月池空不再问占问/性格题
// 原因: leftover dated 关闭后 horary 仍可 collect_method_evidence // 原因: BUG-651 不得把性格题或 leftover 采集当下一道区分题
assert.equal(nextCollect.next_followup?.domain, "horary"); assert.notEqual(nextCollect.next_followup?.domain, "education");
assert.equal(nextCollect.next_followup?.intent, "collect_method_evidence"); 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 () => { test("collect denial persists the next stem on the turn and binds asked_turn_id", async () => {
@@ -454,13 +454,13 @@ test("collect_evidence keeps public can_adopt closed while selection_allowed fol
], ],
confirmationAllowed: false, confirmationAllowed: false,
}); });
// 旧:collect() 把 capability.canAdopt 原样投影成 can_adopt // 原值: sessionOutcome=collect_evidence,公开 can_adopt=false
// 新publicDecisionFields 再按 session_outcome 收紧;内部 canAdopt 仍可 true // 新值: 训练门开、无带年月探针 → adopt_representative,公开 can_adopt=true
// 保留语义:selection_allowed 不因此关闭;确认门仍 fail-closed // 原因: BUG-651 带年月池空即交付;确认门仍 fail-closed
assert.equal(decision.sessionOutcome, "collect_evidence"); assert.equal(decision.sessionOutcome, "adopt_representative");
assert.equal(decision.canAdopt, true); assert.equal(decision.canAdopt, true);
const fields = publicDecisionFields(decision); 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(fields.selection_allowed, decision.selectionAllowed);
}); });
@@ -387,14 +387,20 @@ test("nonterminal invariant 2: exhausted probes take the next available server-o
holdoutValidation: "unavailable" as const, holdoutValidation: "unavailable" as const,
datedMethodCollectOpen: true, datedMethodCollectOpen: true,
nakshatraBoundaryProbe: null, nakshatraBoundaryProbe: null,
nextAction: "ask_fact_collection", // 原值: ask_fact_collection
// 新值: ready_to_adopt
// 原因: BUG-651 训练门开且无带年月探针时剩余采集不得挡住交付
nextAction: "ready_to_adopt",
}, },
{ {
name: "nakshatra boundary", name: "nakshatra boundary",
holdoutValidation: "unavailable" as const, holdoutValidation: "unavailable" as const,
datedMethodCollectOpen: false, datedMethodCollectOpen: false,
nakshatraBoundaryProbe: nakshatraProbe, nakshatraBoundaryProbe: nakshatraProbe,
nextAction: "ask_candidate_discriminator", // 原值: ask_candidate_discriminator(月宿性格题)
// 新值: ready_to_adopt
// 原因: BUG-651 月宿不得挡在结果前面
nextAction: "ready_to_adopt",
}, },
{ {
name: "explicit range exit", name: "explicit range exit",
@@ -261,7 +261,7 @@ function deadlockDossier(): { dossier: DecisionDossier; state: ReturnType<typeof
return { dossier, state }; return { dossier, state };
} }
test("deadlock case: decision and followup agree and still render a D9/D10 style card", () => { test("deadlock case: dated pool empty delivers instead of a D9/D10 style card", () => {
const { dossier } = deadlockDossier(); const { dossier } = deadlockDossier();
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier)); const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier));
assert.equal(inspected.selected?.semanticKey, D9_STYLE_NO_OPTIONS.semantic_key); 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, ...catalog,
candidatesSeparated: false, candidatesSeparated: false,
}); });
assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), true); assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), false);
assert.equal(plan.next_followup?.intent, "distinguish_candidates"); assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
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( assert.equal(
plan.dropped_probes.some((item) => item.semantic_key.startsWith("varga.d9.") || item.semantic_key.startsWith("varga.d10.")), plan.dropped_probes.some((item) => item.semantic_key.startsWith("varga.d9.") || item.semantic_key.startsWith("varga.d10.")),
false, true,
JSON.stringify(plan.dropped_probes), JSON.stringify(plan.dropped_probes),
); );
const decision = decideFromDossier(dossier); const decision = decideFromDossier(dossier);
assert.equal(decision.nextAction, "ask_candidate_discriminator"); // 原值: ask_candidate_discriminator,出 D9/D10 性格卡
assert.equal(decision.sessionOutcome, "discriminate_candidates"); // 新值: 带年月池空即交付,性格题 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); assert.equal(decision.canConfirmExactMinute, false);
}); });
@@ -319,10 +325,18 @@ test("varga_style probes missing style_options complete the same way in inspect
probes: [probe], probes: [probe],
}, },
}); });
assert.equal(plan.next_followup?.semantic_key, probe.semanticKey); assert.notEqual(plan.next_followup?.semantic_key, probe.semanticKey);
assert.equal(plan.next_followup?.choice_kind, "varga_style"); assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
assert.ok(plan.next_followup?.choice_frame); // 原值: 补全 style_options 后仍把 D9 当下一道区分题
assert.equal(plan.dropped_probes.some((item) => item.semantic_key === probe.semanticKey), false); // 新值: 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", () => { 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 === "relatives")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(decision.nextAction, "ask_candidate_discriminator"); // 原值: 方法覆盖未齐仍问 D9 性格卡
assert.equal(plan.next_followup?.intent, "distinguish_candidates"); // 新值: 训练门开、带年月池空即交付
assert.ok(plan.next_followup?.choice_frame); // 原因: BUG-651 性格题不得挡在结果前面
assert.notEqual(plan.next_followup?.source, "method_coverage"); assert.ok(
assert.equal(decision.probe?.semanticKey, plan.next_followup?.semantic_key); 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", () => { 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, candidatesSeparated: false,
}); });
const decision = decideFromDossier(dossier); const decision = decideFromDossier(dossier);
assert.equal(decision.nextAction, "ask_candidate_discriminator"); // 原值: 下一问是 D9/D10decision.probe 对得上
assert.equal(decision.probe?.semanticKey, plan.next_followup?.semantic_key); // 新值: 性格题 deferreddecision.probe 与 next 都不是性格卡
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d(9|10)\./); // 原因: BUG-651
assert.notEqual(decision.probe?.semanticKey, D24.semantic_key); 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", () => { test("birthDate gating matches the real followup plan on an under-age event probe", () => {
@@ -2053,35 +2053,31 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
}], }],
}, },
}); });
assert.equal(plan.next_followup?.intent, "distinguish_candidates"); // 原值: 职业覆盖后仍问 D10 性格对照
assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/); // 新值: 性格题 deferred,不再作为淘汰区分题
assert.equal(conversationalSessionOutcome({ // 原因: BUG-651
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
const outcome = conversationalSessionOutcome({
selectionAllowed: true, selectionAllowed: true,
proposeAllowed: true, proposeAllowed: true,
confirmationAllowed: false, confirmationAllowed: false,
nextFollowup: plan.next_followup, nextFollowup: plan.next_followup,
methods: plan.methods, methods: plan.methods,
discriminatorProbe: { discriminatorProbe: null,
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",
},
candidateScores: [ candidateScores: [
{ time: "05:00", score: 34 }, { time: "05:00", score: 34 },
{ time: "05:01", score: 33 }, { time: "05:01", score: 33 },
{ time: "05:02", 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", () => { 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, 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", () => { test("three dated events with one holdout keep collecting instead of discriminating", () => {
@@ -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"); 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 = { const d10 = {
probeId: "contrast:varga.d10.巨蟹座/狮子座", probeId: "contrast:varga.d10.巨蟹座/狮子座",
candidateSetVersion: "04:47-05:15", 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], probes: [...YEARLESS_PACKET.probes, d10],
}, },
}); });
assert.equal(plan.next_followup?.intent, "distinguish_candidates"); assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
assert.ok(plan.next_followup?.choice_frame); assert.notEqual(plan.next_followup?.semantic_key, d10.semanticKey);
assert.equal(plan.next_followup?.semantic_key, d10.semanticKey); assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
assert.equal(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", () => { test("training gate closed still withholds yearless varga cards", () => {
@@ -166,10 +166,18 @@ test("unanchored D10 varga_style cards are dropped; anchored cards mention the l
contrastPacket: packet, contrastPacket: packet,
candidatesSeparated: false, candidatesSeparated: false,
}); });
assert.equal(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey); assert.notEqual(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey);
assert.equal(anchored.next_followup?.choice_kind, "varga_style"); assert.notEqual(anchored.next_followup?.choice_kind, "varga_style");
assert.match(anchored.next_followup?.user_prompt_hint ?? "", /2018 年 7 月/); // 原值: 有同年职业账本时把 D10 性格卡当下一问,题干提 2018 年 7 月
assert.doesNotMatch(anchored.next_followup?.choice_frame?.prompt ?? "", /2018/); // 新值: 即使有账本锚点,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", () => { 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( assert.ok(
decided.probe?.semanticKey === D10_LOW.semanticKey decided.probe?.semanticKey === D10_LOW.semanticKey
|| decided.nextAction === "ready_to_adopt" || 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}`,
); );
}); });
@@ -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 packet = d9WalkthroughPacket();
const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9.")); const probe = packet.probes.find((item) => item.semanticKey.startsWith("varga.d9."));
assert.ok(probe); assert.ok(probe);
@@ -184,18 +184,18 @@ test("walkthrough D9 probe question is in the 4-80 window and must survive frame
candidatesSeparated: false, candidatesSeparated: false,
}); });
const followup = plan.next_followup; const followup = plan.next_followup;
assert.equal(followup?.intent, "distinguish_candidates", JSON.stringify({ // 原值: 走查 D9 性格卡必须拼出 choice_frame
intent: followup?.intent, // 新值: D9 记 yearless_deferred,不再作为必答区分题
source: followup?.source, // 原因: BUG-651
method_id: followup?.method_id, assert.notEqual(followup?.choice_kind, "varga_style");
dropped: plan.dropped_probes, assert.notEqual(followup?.intent, "distinguish_candidates");
})); assert.equal(
assert.equal(followup?.method_id, "d9_relationship"); plan.dropped_probes.some((item) => (
const frame = followup?.choice_frame ?? null; item.semantic_key.startsWith("varga.d9.") && item.reason === "yearless_deferred"
assert.ok(frame, "D9 probe must assemble a choice_frame"); )),
assert.equal(frame?.prompt, D9_QUESTION); true,
const copy = frame ? serverOwnedChoiceCopy(frame) : null; JSON.stringify(plan.dropped_probes),
assert.ok(copy?.prompt, "assembled D9 copy must keep the probe question"); );
}); });
test("D9 probe shape with unwired prompt makes the choice invariants red", async () => { test("D9 probe shape with unwired prompt makes the choice invariants red", async () => {