Files
Jyotisha/frontend/tests/rectification-coverage-collect.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

221 lines
8.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 { decideFromDossier, rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import {
blockingMethodsCovered,
buildMethodFollowupPlan,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import type { InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
const TRAINING_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",
},
] as const;
const FAMILY_EVIDENCE = {
id: "e-fam",
status: "confirmed",
domain: "family",
datePrecision: "year",
occurredFrom: "2023-01-01",
occurredTo: null,
eventKind: "family_event",
} as const;
const OCCUPATION_EVIDENCE = {
id: "e-occ",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
} 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;
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
} = {},
) {
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: {
inference_state: state,
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
},
};
return {
state,
dossier: {
evidence,
conversationSummary: {
activeFocus: null,
declinedSkippedTopics: options.declinedTopics ?? [],
},
latestResult: latest,
case: { acceptedTime: null },
},
};
}
function collectionFollowup(
dossier: ReturnType<typeof producedDossier>["dossier"],
sessionOutcome: "collect_evidence" | "validate_holdout" | "adopt_representative" = "collect_evidence",
) {
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
return buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome,
...catalog,
candidatesSeparated: true,
});
}
test("separated scores with uncovered relatives stay in collection and still ask a blocking method", () => {
const { dossier } = producedDossier(TRAINING_EVIDENCE);
const gate = trainingScoreableGate(TRAINING_EVIDENCE);
assert.equal(gate.open, true);
assert.equal(gate.trainingCount, 3);
assert.ok(gate.trainingDomainCount >= 2);
const decision = decideFromDossier(dossier);
assert.equal(decision.separation.sufficient, true);
// 原值: offer_provisional_range
// 新值: ask_fact_collection
// 原因: D5 让七条线全部轮到 + D6 要求所有线问完才交付;未覆盖的
// relatives / 搬家等仍是待问的线,出卡门槛只是必要条件(BUG-751)
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.canConfirmExactMinute, false);
const plan = collectionFollowup(dossier);
assert.equal(blockingMethodsCovered(plan.methods), false);
assert.notEqual(plan.next_followup?.method_id, "relatives");
assert.notEqual(plan.next_followup?.domain, "family");
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
});
test("covering family without occupation still opens adopt after scores already separated", () => {
const familyOnly = producedDossier([...TRAINING_EVIDENCE, FAMILY_EVIDENCE]);
const familyDecision = decideFromDossier(familyOnly.dossier);
// 原值: ready_to_adopt / adopt_representative / canAdopt=true
// 新值: ask_fact_collection / collect_evidencecanAdopt 仍是引擎能力位,不变)
// 原因: D5 + D6——财务、搬家、健康、职业四条线还没问,交付要等问完(BUG-751)
assert.equal(familyDecision.nextAction, "ask_fact_collection");
assert.equal(familyDecision.sessionOutcome, "collect_evidence");
assert.equal(familyDecision.canAdopt, true);
assert.equal(familyDecision.canConfirmExactMinute, false);
});
test("covering family and occupation still opens adopt after scores already separated", () => {
const { state, dossier: collecting } = producedDossier(TRAINING_EVIDENCE);
// 原值: offer_provisional_range
// 新值: ask_fact_collection
// 原因: D5 + D6——七条线还没问完(BUG-751)
assert.equal(decideFromDossier(collecting).nextAction, "ask_fact_collection");
const covered = [...TRAINING_EVIDENCE, FAMILY_EVIDENCE, OCCUPATION_EVIDENCE];
const { dossier } = producedDossier(covered, { previous: state });
const decision = decideFromDossier(dossier);
// 原值: S3ready_to_adopt 或 offer_provisional_range)、canAdopt=true
// 新值: ask_fact_collectioncanAdopt 仍是引擎能力位,不变)
// 原因: 同上——覆盖家人与职业后,剩下五条线仍要问(BUG-751)
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
assert.equal(decision.separation.sufficient, true);
});
test("declining family and occupation also opens adopt after scores already separated", () => {
const declinedTopics = [
{ targetDomain: "family" },
{ target_domain: "occupation" },
];
const { dossier } = producedDossier(TRAINING_EVIDENCE, { declinedTopics });
const plan = collectionFollowup(dossier);
assert.equal(blockingMethodsCovered(plan.methods), true);
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "covered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
const decision = decideFromDossier(dossier);
// 原值: ready_to_adopt / adopt_representative / canAdopt=true
// 新值: ask_fact_collection / collect_evidencecanAdopt 仍是引擎能力位,不变)
// 原因: D5 + D6——只拒答了家人与职业,其余五条线仍在引导池里(BUG-751)
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
});