出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时 按 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
221 lines
8.0 KiB
TypeScript
221 lines
8.0 KiB
TypeScript
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_evidence(canAdopt 仍是引擎能力位,不变)
|
||
// 原因: 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);
|
||
// 原值: S3(ready_to_adopt 或 offer_provisional_range)、canAdopt=true
|
||
// 新值: ask_fact_collection(canAdopt 仍是引擎能力位,不变)
|
||
// 原因: 同上——覆盖家人与职业后,剩下五条线仍要问(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_evidence(canAdopt 仍是引擎能力位,不变)
|
||
// 原因: 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);
|
||
});
|