Files
Jyotisha/frontend/tests/rectification-holdout-renderable.test.ts
T
Jesse_ChenandClaude Fable 5.1 dc732825d8 fix(rectification): 有题就接着问,题问完才出卡;引导窗口不再硬贴领域
出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时
按 D2 保住「题源全空就按现行规则出卡」——门槛只在还有题可问时挡住
出卡,precision_gate_met 改成只上报(新挂在决策与公开投影上),不再
单独决定时机。引导窗口题在无领域轨道上改问开放题,一个时间窗只问一
次;录入卡提交的是「YYYY 年 M 月,<领域>方面有一件事」,不再是题干
的三选一列表。记忆化 golden 只补一个新键并冻结墙钟。离线回放改成注
入真值方向的边界事件,另跑一组反方向对照。Skill 10.0.28。

BUG-747~752

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 12:16:55 +00:00

265 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { applyHoldoutAnswer } from "../src/lib/rectification-agentic/core/build-state.ts";
import { decideFromDossier, rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import type { InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
const COVERED_EVIDENCE = [
{
id: "e-edu",
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
eventKind: "education_milestone",
},
{
id: "e-career-a",
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2018-01-01",
occurredTo: null,
eventKind: "career_entry",
},
{
id: "e-career-b",
status: "confirmed",
domain: "career",
datePrecision: "year",
occurredFrom: "2020-01-01",
occurredTo: null,
eventKind: "career_change",
},
{
id: "e-rel",
status: "confirmed",
domain: "relationship",
datePrecision: "year",
occurredFrom: "2024-01-01",
occurredTo: null,
eventKind: "relationship_start",
},
{
id: "e-fam",
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2023-01-01",
occurredTo: null,
eventKind: "family_event",
},
{
id: "e-occ",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
{
id: "e-fin",
status: "confirmed",
domain: "finance",
datePrecision: "year",
occurredFrom: "2017-01-01",
occurredTo: null,
eventKind: "income_change",
},
{
id: "e-reloc",
status: "confirmed",
domain: "relocation",
datePrecision: "year",
occurredFrom: "2019-01-01",
occurredTo: null,
eventKind: "home_change",
},
{
id: "e-health",
status: "confirmed",
domain: "health_pressure",
datePrecision: "year",
occurredFrom: "2021-01-01",
occurredTo: null,
eventKind: "self_health_event",
},
] as const;
function producedDossier(
evidence: readonly Readonly<{
id: string;
status: string;
domain: string;
datePrecision: string;
occurredFrom: string | null;
occurredTo: string | null;
eventKind?: string | null;
}>[],
options: { previous?: InferenceState | null } = {},
) {
const state = buildCaseInferenceState({
range: { start_time: "04:55", end_time: "05:02" },
candidates: [
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
],
evidence,
probes: [],
previous: options.previous,
});
const latest = {
resultId: "55555555-5555-4555-8555-555555555555",
candidates: state.candidates.map((item) => ({
candidateId: item.id,
time: item.time,
rank: item.rank,
relativeSupport: Math.round(item.posterior_score),
})),
representativeTime: state.representative_time,
evidenceLedgerFingerprint: evidenceLedgerFingerprint(evidence as never),
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
};
return {
state,
dossier: {
evidence,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: latest,
case: { acceptedTime: null },
},
};
}
function holdoutFollowup(dossier: ReturnType<typeof producedDossier>["dossier"]) {
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
return buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: "validate_holdout",
...catalog,
candidatesSeparated: true,
});
}
test("sticky holdout with unknown precision stays review-only", () => {
const { state, dossier: dated } = producedDossier(COVERED_EVIDENCE);
const holdout = state.events.find((item) => item.usage === "holdout");
assert.ok(holdout);
assert.notEqual(holdout.year, null);
const datedDecision = decideFromDossier(dated);
// 原断言 nextAction=ask_holdout_validation → 新断言 ready_to_adopt。
// 为什么:dated holdout 只挡确认门;覆盖完成即可代表性采用。
assert.equal(datedDecision.nextAction, "ready_to_adopt");
assert.equal(datedDecision.canAdopt, true);
assert.equal(datedDecision.canConfirmExactMinute, false);
const undated = COVERED_EVIDENCE.map((item) => (
item.id === holdout.id
? { ...item, datePrecision: "unknown", occurredFrom: null, occurredTo: null }
: item
));
const { state: sticky, dossier } = producedDossier(undated, { previous: state });
const stickyHoldout = sticky.events.find((item) => item.id === holdout.id);
assert.equal(stickyHoldout?.usage, "holdout");
assert.equal(stickyHoldout?.year, null);
assert.equal(stickyHoldout?.precision, "unknown");
const decision = decideFromDossier(dossier);
assert.notEqual(decision.nextAction, "ask_holdout_validation");
// 原值: offer_provisional_range / ready_to_adopt、adopt_representative、canAdopt=true
// 新值: ask_fact_collection、collect_evidence
// 原因: D5 + D6——holdout 变成无日期后这条线回到采集池,线没问完不交付;
// 题名要的「stays review-only」(不进 validated_range)仍成立;
// canAdopt 是引擎能力位,公开侧由 canOfferRange 收起(BUG-751
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.notEqual(decision.sessionOutcome, "validated_range");
assert.equal(decision.validated, false);
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
});
test("dated holdout asks validation with a renderable followup card", () => {
const { dossier } = producedDossier(COVERED_EVIDENCE);
const decision = decideFromDossier(dossier);
// 原断言 nextAction=ask_holdout_validation → 新断言 ready_to_adopt。
// 为什么:默认决策不再用 holdout 挡住 provisional 采用;确认门仍关。
// holdout 卡片在 validate_holdout 会话里仍可渲染,供确认路径使用。
assert.equal(decision.nextAction, "ready_to_adopt");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
const plan = holdoutFollowup(dossier);
// BUG-580: 账本已覆盖 holdout 候选领域时不再出盘外核对卡,直接交付。
assert.equal(plan.next_followup, null);
});
test("passed holdout is a validated range, not a unique minute", () => {
const { state, dossier } = producedDossier(COVERED_EVIDENCE);
const passed = applyHoldoutAnswer(state, "yes");
const decided = decideFromDossier({
...dossier,
latestResult: {
...dossier.latestResult,
decisionReceipt: {
...dossier.latestResult.decisionReceipt,
inference_state: passed,
},
},
});
assert.equal(decided.sessionOutcome, "validated_range");
assert.equal(decided.canAdopt, true);
assert.equal(decided.validated, true);
assert.equal(decided.canConfirmExactMinute, false);
assert.notEqual(decided.nextAction, "ask_holdout_validation");
});
test("failed holdout keeps existing non-validated close or discriminate path", () => {
const { state, dossier } = producedDossier(COVERED_EVIDENCE);
const failed = applyHoldoutAnswer(state, "no");
assert.equal(failed.holdout_passed, false);
const decided = decideFromDossier({
...dossier,
latestResult: {
...dossier.latestResult,
decisionReceipt: {
...dossier.latestResult.decisionReceipt,
inference_state: failed,
},
},
});
assert.equal(decided.holdoutValidation, "failed");
assert.notEqual(decided.nextAction, "ask_holdout_validation");
assert.notEqual(decided.sessionOutcome, "validated_range");
assert.equal(decided.validated, false);
assert.equal(decided.canConfirmExactMinute, false);
});
test("ask_holdout_validation is only returned when the same dossier can build a holdout followup", () => {
const fixtures = [
producedDossier(COVERED_EVIDENCE).dossier,
producedDossier(COVERED_EVIDENCE.filter((item) => item.domain !== "occupation")).dossier,
];
for (const dossier of fixtures) {
const decision = decideFromDossier(dossier);
if (decision.nextAction !== "ask_holdout_validation") continue;
const plan = holdoutFollowup(dossier);
assert.ok(plan.next_followup, "ask_holdout_validation requires a holdout followup");
assert.ok(plan.next_followup.choice_frame, "ask_holdout_validation requires a renderable card");
assert.equal(decision.canConfirmExactMinute, false);
}
});