Files
Jyotisha/frontend/tests/rectification-eight-method.test.ts
T
Jesse_Chen 0be51e65f5
Independent Staging Quality Gate / validate (push) Successful in 10m6s
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(rectification): ask relationship and career before dated collect
Move nextDatedCollectFollowup after family coverage so a new case still
asks D9 then D10. Dated third-event collect stays education → finance →
relocation → health. Admin input padding contract follows --space-3.

BUG-531 and BUG-532.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 19:06:54 +08:00

3176 lines
122 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 { readFileSync } from "node:fs";
import test from "node:test";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, spokenFollowupForUser, blockingMethodsCovered } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
mentionedVargaKeysFromLedgerEvidence,
buildCandidateContrastPacket,
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import {
internalObservationsFromWindowScan,
parseWindowScan,
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts";
import {
candidateRangeFingerprint,
evidenceLedgerFingerprint,
parseV9CaseDossier,
RectificationToolServiceError,
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
import {
resolveExactSkillPackage,
} from "../src/lib/skill-package-registry.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CASE_ID,
CANDIDATE_ID,
FOCUS_ID,
RESULT_ID,
SECOND_CANDIDATE_ID,
TURN_ID,
USER_ID,
candidateSnapshotFixture,
computeFixture,
conversationSummaryFixture,
dossierFixture,
activeFocusFixture,
fakeAccounting,
receiptHandlers,
} from "./rectification-v9-test-support.ts";
const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883";
const EDUCATION_ID = "44444444-4444-4444-8444-444444444441";
const RELATIONSHIP_ID = "44444444-4444-4444-8444-444444444442";
const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
// 本单回退 3847e9c9 把训练门未开的下一问改成家人的断言。见各断言旁三栏。
function collectDecision(candidates: readonly { time: string; relativeSupport?: number }[]) {
return decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport ?? 0 })),
});
}
function producedInferenceState(
candidates: readonly Readonly<{ id: string; time: string; relative_support: number }>[],
) {
const times = candidates.map((item) => item.time).sort();
return buildInferenceState({
range_start: times[0]!,
range_end: times[times.length - 1]!,
candidates: candidates.map((item) => ({
id: item.id,
time: item.time,
relative_support: item.relative_support,
})),
events: [],
probes: [],
});
}
const DYNAMIC_STYLE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
{ label: "明确没有发生", answer_class: "no" as const },
{ label: "不记得这段经历", answer_class: "unsure" as const },
];
const CLASSIC_COVERAGE = [
{ status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year" as const, occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year" as const, occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year" as const, occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "appearance", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "marks", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "horary", datePrecision: "day" as const, occurredFrom: "2024-01-01", occurredTo: null },
];
function datedEvidence(
domain: string,
year: string,
extra: {
eventKind?: string | null;
summary?: string | null;
datePrecision?: "year" | "month" | "day";
} = {},
) {
return {
status: "confirmed" as const,
domain,
datePrecision: extra.datePrecision ?? ("year" as const),
occurredFrom: `${year}-01-01`,
occurredTo: null,
...(extra.eventKind !== undefined ? { eventKind: extra.eventKind } : {}),
...(extra.summary !== undefined ? { summary: extra.summary } : {}),
};
}
const CAREER_CONFLICT_PROBE = {
year: 2018,
year_label: "2018 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "dasha_activation" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "distinguish" as const,
phase: "candidate_discriminator" as const,
information_gain: 0.21,
semantic_key: "career.2018.dasha_activation",
candidate_set_version: "set-test",
candidate_split_hash: "set-test:career:2018",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
};
const EDUCATION_QUALITY_PROBE = {
year: 2016,
year_label: "2016 年前后",
domain: "education" as const,
event_family: "高考或重要考试发挥明显失常、压力很大",
source: "known_event_quality" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2016 年前后。已有高考或考试经历。请写成一句自然语言,问那次是否发挥失常或压力特别大。",
role: "clarify" as const,
phase: "event_clarification" as const,
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "education.2016",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const CAREER_QUALITY_PROBE = {
year: 2020,
year_label: "2020 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "known_event_quality" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2020 年前后。已有相关经历。请写成一句自然语言,问入职、升职或职责明显加重有没有发生过。不得改年份。",
role: "clarify" as const,
phase: "event_clarification" as const,
choice_kind: "event_quality" as const,
information_gain: 0,
semantic_key: "career.2020",
style_options: DYNAMIC_STYLE_OPTIONS,
};
const ENGINE_SCORE = {
success: true,
endpoint: "rectification_v5_score",
result_id: "e4fbf2e0-85dc-5b42-a5a3-34e5dd4b7e62",
algorithm_version: "rectification-event-contract-v2",
event_contract_version: "rectification-event-contract-v2",
decision_policy_version: "rectification-candidate-policy-v2",
execution_ledger_version: "rectification-execution-ledger-v2",
candidate_decisions: [
{ candidate_id: CANDIDATE_ID, time: "04:50", rank: 1, relative_support: 57, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, time: "04:51", rank: 2, relative_support: 25, tied_minute_count: 2 },
{ candidate_id: THIRD_CANDIDATE_ID, time: "04:52", rank: 3, relative_support: 18, tied_minute_count: 2 },
],
decision_receipt: {
receipt_version: "candidate-decision-receipt-v2",
contract_version: "v2",
event_contract_version: "rectification-event-contract-v2",
policy_version: "rectification-candidate-policy-v2",
decision_policy_version: "rectification-candidate-policy-v2",
display_allowed: true,
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
accept_allowed: true,
confirm_allowed: false,
representative_candidate_id: CANDIDATE_ID,
representative_time: "04:50",
overall_confidence: "high",
margin_percent: 42.5,
},
execution_ledger: [
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d1-rashi", status: "executed", source: "python-engine" },
{ ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d9-navamsa", status: "executed", source: "python-engine" },
],
diagnostics: {
window_scan: {
scanned: true,
confirmation_allowed: false,
unique_minute_claim: false,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊座", "天蝎"],
},
},
};
const educationEvidence = {
id: EDUCATION_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "education_milestone",
domain: "education",
occurred_from: "2016-06-01",
occurred_to: null,
date_precision: "month",
summary: "2016年6月一次学业节点",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:06.000Z",
};
const relationshipEvidence = {
id: RELATIONSHIP_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "2018年一段感情开始",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
};
const familyEvidence = {
id: FAMILY_ID,
source_turn_id: TURN_ID,
subject: "family",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "2020年家人相关变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
};
const careerEvidence = {
id: CAREER_ID,
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-07-01",
occurred_to: null,
date_precision: "year",
summary: "2019年开始工作",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
};
const methodCoverageTieEvidence = [
{ ...educationEvidence, domain: "education" },
{
id: "44444444-4444-4444-8444-444444444442",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "感情变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
},
{
id: "44444444-4444-4444-8444-444444444443",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-01-01",
occurred_to: null,
date_precision: "year",
summary: "工作变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
},
{
id: "44444444-4444-4444-8444-444444444446",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "家人变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
},
{
id: "44444444-4444-4444-8444-444444444445",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "occupation_note",
domain: "occupation",
occurred_from: null,
occurred_to: null,
date_precision: "unknown",
summary: "长期一直是程序员",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:10.000Z",
},
];
function scoreableFingerprintForRawEvidence(evidence: unknown[]): string {
const parsed = parseV9CaseDossier(dossierFixture({ evidence }));
assert.ok(parsed);
return evidenceLedgerFingerprint(parsed.evidence);
}
function stubEngine(response: unknown) {
const previous = globalThis.fetch;
globalThis.fetch = (async () => ({
ok: true,
status: 200,
json: async () => response,
})) as unknown as typeof fetch;
return () => {
globalThis.fetch = previous;
};
}
test("eight-method routing asks relationship after dated education, not relocation", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
// 原值: relatives / family
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.domain, "relationship");
assert.equal(plan.stop_domain_rotation, true);
assert.deepEqual([...plan.not_in_rotation], ["relocation"]);
assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
});
test("dasha conflict probe does not jump ahead of method rotation before acceptance event quality", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016")],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
});
test("known exam quality does not create a scoring card after one recorded event", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016")],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
});
test("career known-event quality does not jump the adoption gate", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("career", "2020", { eventKind: "career_entry", datePrecision: "month" }),
datedEvidence("career", "2020", { eventKind: "career_exit", datePrecision: "month" }),
],
eventProbes: [CAREER_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
assert.notEqual(plan.next_followup?.source, "event_probe");
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
});
test("encoded exam quality does not stamp another card and keeps method rotation", () => {
const plan = buildMethodFollowupPlan({
evidence: [datedEvidence("education", "2016", { summary: "2016年高考发挥异常" })],
eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("dasha conflict probe jumps after four scoreable events leave three training domains", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
datedEvidence("relationship", "2018"),
datedEvidence("family", "2023"),
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后");
assert.deepEqual(plan.next_followup?.candidate_ids, ["05:00", "05:20"]);
assert.ok((plan.next_followup?.information_gain ?? 0) > 0);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "discriminate_candidates");
});
test("dasha conflict probe keeps the engine month on the choice card", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
datedEvidence("relationship", "2018"),
datedEvidence("family", "2023"),
],
eventProbes: [{
...CAREER_CONFLICT_PROBE,
month: 3,
year_label: "2018 年 3 月前后",
source: "dasha_boundary",
user_meaning: "时间范围锁定 2018 年 3 月前后;领域锁定 career。",
semantic_key: "career.2018.03.dasha_boundary",
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年 3 月前后");
assert.equal(plan.next_followup?.year_label, "2018 年 3 月前后");
assert.equal(plan.next_followup?.probe_month, 3);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2018 年 3 月前后/);
});
test("three scoreable events in one domain still rotate methods instead of reverse-inferring", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2012"),
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("occupation_note does not count toward reverse-inference event quality", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
{
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
],
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / relatives
// 新值: method_coverage / d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("age-band probe does not jump ahead of uncovered relationship", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}],
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "age_band",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。",
role: "reverse_verify",
}],
});
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.choice_frame, null);
});
test("user-stop action records stated events when the ledger is empty", () => {
const action = buildNextUserAction({
scorableCount: 0,
evidenceCount: 0,
hasLatestResult: false,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: null,
workingTime: "12:00",
});
assert.equal(action.id, "record_stated_events");
assert.equal(action.on_user_stop.id, "record_stated_events");
assert.match(action.on_user_stop.user_meaning, /batch/);
});
test("user-stop action offers a range when candidates already exist", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
const withCandidates = buildNextUserAction({
scorableCount: 1,
evidenceCount: 1,
hasLatestResult: true,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "12:00",
});
assert.equal(withCandidates.id, "ask_method_followup");
assert.equal(withCandidates.on_user_stop.id, "offer_provisional_range");
const withoutCandidates = buildNextUserAction({
scorableCount: 0,
evidenceCount: 1,
hasLatestResult: false,
selectionAllowed: false,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "12:00",
});
assert.equal(withoutCandidates.id, "ask_method_followup");
assert.equal(withoutCandidates.on_user_stop.id, "explain_current_window");
assert.match(withoutCandidates.on_user_stop.user_meaning, /12:00/);
assert.match(withoutCandidates.on_user_stop.user_meaning, /不要只说会话会保留/);
assert.equal(plan.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy");
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
assert.equal(plan.next_followup?.choice_frame, null);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("selectionAllowed with remaining method follow-up keeps collecting and offers a range on stop", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
});
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "collect_evidence",
nextFollowup: plan.next_followup,
workingTime: "05:07",
});
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d9_relationship");
assert.equal(action.id, "ask_method_followup");
assert.equal(action.on_user_stop.id, "offer_provisional_range");
});
test("adopt_representative defers method follow-up instead of asking this turn", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
sessionOutcome: "adopt_representative",
});
assert.equal(plan.next_followup, null);
// 原值: relatives
// 新值: d9_relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.deferred_followup?.method_id, "d9_relationship");
assert.equal(plan.session_outcome, "adopt_representative");
});
test("adopt_representative ignores leftover distinguish focus", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
sessionOutcome: "adopt_representative",
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "relocation",
targetKind: "home_change",
},
});
assert.equal(plan.next_followup, null);
assert.equal(plan.session_outcome, "adopt_representative");
});
test("declined relationship skips to career and leaves horary uncovered", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}],
declinedTopics: [{ target_domain: "relationship", status: "declined" }],
});
// 原值: relatives / family
// 新值: d10_career / career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.domain, "career");
assert.deepEqual([...plan.do_not_poll], []);
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
});
test("D9 differ keeps sign names for the type-table report and still forbids unique-minute claims", () => {
const scan = parseWindowScan({
scanned: true,
confirmation_allowed: true,
unique_minute_claim: true,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊座", "天蝎"],
type_table: "热情冲动",
transitions: [{
layer: "d9",
at: "05:14",
user_meaning: "白羊座在 05:14 换成天蝎",
}],
});
assert.ok(scan);
assert.equal(scan.confirmation_allowed, false);
assert.equal(scan.unique_minute_claim, false);
assert.equal(scan.d9_candidates_differ, true);
assert.deepEqual(scan.d9_sign_names, ["白羊座", "天蝎"]);
assert.deepEqual(scan.transitions, [{
layer: "d9",
at: "05:14",
user_meaning: "D9 在 05:14 发生变化",
}]);
const observations = internalObservationsFromWindowScan(scan);
assert.deepEqual(observations, [
{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" },
{ layer: "d10", candidates_differ: false, ask_theme: null },
{ layer: "d4", candidates_differ: false, ask_theme: null },
{ layer: "d5", candidates_differ: false, ask_theme: null },
{ layer: "d7", candidates_differ: false, ask_theme: null },
{ layer: "d12", candidates_differ: false, ask_theme: null },
{ layer: "d11", candidates_differ: false, ask_theme: null },
{ layer: "d30", candidates_differ: false, ask_theme: null },
]);
assert.doesNotMatch(JSON.stringify({ scan, observations }), UNIQUE_MINUTE_COPY);
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
observations,
});
assert.equal(plan.next_followup, null);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("D24-only window change folds into education follow-up without a second layer", () => {
const scan = parseWindowScan({
scanned: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
d5_lagna_count: 1,
d24_lagna_count: 2,
d5_candidates_differ: false,
d24_candidates_differ: true,
transitions: [
{ layer: "d24", at: "05:14" },
{ layer: "pada", at: "05:14" },
],
});
assert.ok(scan);
assert.equal(scan.d24_candidates_differ, true);
assert.equal(scan.d5_candidates_differ, false);
assert.deepEqual(scan.transitions, [
{ layer: "d24", at: "05:14", user_meaning: "D24 在 05:14 发生变化" },
{ layer: "pada", at: "05:14", user_meaning: "Nakshatra pada 在 05:14 发生变化" },
]);
const observations = internalObservationsFromWindowScan(scan);
assert.equal(observations.find((item) => item.layer === "d5")?.candidates_differ, true);
assert.equal(observations.find((item) => item.layer === "d5")?.ask_theme, "education_style");
assert.deepEqual(
observations.map((item) => item.layer),
["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"],
);
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
observations,
});
assert.equal(plan.next_followup?.method_id, "d5_education");
assert.equal(plan.next_followup?.ask_theme, "education_style");
});
test("D11-only window change folds into finance follow-up without delaying adopt", () => {
const scan = parseWindowScan({
scanned: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
d2_lagna_count: 1,
d11_lagna_count: 2,
d30_lagna_count: 2,
d2_candidates_differ: false,
d11_candidates_differ: true,
d30_candidates_differ: true,
transitions: [
{ layer: "d11", at: "05:14" },
{ layer: "d30", at: "05:14" },
{ layer: "bhava", at: "05:14" },
{ layer: "pranapada", at: "05:14" },
],
});
assert.ok(scan);
assert.equal(scan.d11_candidates_differ, true);
assert.equal(scan.d2_candidates_differ, false);
assert.deepEqual(scan.transitions, [
{ layer: "d11", at: "05:14", user_meaning: "D11 在 05:14 发生变化" },
{ layer: "d30", at: "05:14", user_meaning: "D30 在 05:14 发生变化" },
{ layer: "bhava", at: "05:14", user_meaning: "Bhava Lagna 在 05:14 发生变化" },
{ layer: "pranapada", at: "05:14", user_meaning: "Pranapada Lagna 在 05:14 发生变化" },
]);
const observations = internalObservationsFromWindowScan(scan);
assert.equal(observations.find((item) => item.layer === "d11")?.candidates_differ, true);
assert.equal(observations.find((item) => item.layer === "d11")?.ask_theme, "finance_change");
assert.equal(observations.find((item) => item.layer === "d30")?.ask_theme, "health_pressure");
assert.deepEqual(
observations.map((item) => item.layer),
["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"],
);
const plan = buildMethodFollowupPlan({
evidence: [
...CLASSIC_COVERAGE,
{ status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null },
],
observations,
});
assert.equal(plan.next_followup?.method_id, "d2_finance");
assert.equal(plan.next_followup?.source, "varga_observation");
});
test("window scan displays KP sub-lord changes without opening confirmation", () => {
assert.deepEqual(
["kp1", "kp4", "kp7", "kp10"].every((layer) => WINDOW_SCAN_DISPLAY_LAYER_ORDER.includes(layer as typeof WINDOW_SCAN_DISPLAY_LAYER_ORDER[number])),
true,
);
const scan = parseWindowScan({
scanned: true,
confirmation_allowed: true,
unique_minute_claim: true,
d9_lagna_count: 1,
d10_lagna_count: 1,
transitions: [
{ layer: "kp1", at: "05:14" },
{ layer: "kp10", at: "05:15" },
],
});
assert.ok(scan);
assert.equal(scan.confirmation_allowed, false);
assert.equal(scan.unique_minute_claim, false);
assert.deepEqual(scan.transitions, [
{ layer: "kp1", at: "05:14", user_meaning: "KP 1宫子主 在 05:14 发生变化" },
{ layer: "kp10", at: "05:15", user_meaning: "KP 10宫子主 在 05:15 发生变化" },
]);
});
test("read-case follows method plan and keeps D9/D10 type tables when SQL missing categories rotate", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, familyEvidence, careerEvidence],
conversationSummary: {
confirmed_evidence_summary: [],
pending_revisions: [],
active_focus: null,
declined_skipped_topics: [],
candidate_divergence_summary: null,
missing_evidence_categories: ["relocation", "health", "finance"],
last_result_policy: null,
summary_version: 1,
updated_at: "2026-08-12T10:00:06.000Z",
},
latestResult: {
...candidateSnapshotFixture({
confirmationAllowed: true,
representativeTime: "04:45",
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:45", relative_support: 40, tied_minute_count: 25 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:46", relative_support: 35, tied_minute_count: 25 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "04:47", relative_support: 25, tied_minute_count: 25 },
],
}),
selection_allowed: true,
confirmation_allowed: true,
decision_receipt: {
receipt_version: "candidate-decision-receipt-v2",
policy_version: "rectification-candidate-policy-v2",
selection_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
representative_candidate_id: CANDIDATE_ID,
overall_confidence: "medium",
window_scan: {
scanned: true,
confirmation_allowed: false,
unique_minute_claim: false,
d9_lagna_count: 2,
d10_lagna_count: 1,
d9_candidates_differ: true,
d10_candidates_differ: false,
d9_sign_names: ["白羊", "天蝎"],
},
},
},
}),
get_agentic_rectification_case_compute: () => computeFixture(),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{
conversation_summary: { missing_evidence_categories: string[] };
method_followup_plan: {
next_followup: { method_id: string; domain: string | null } | null;
deferred_followup: { method_id: string; domain: string | null } | null;
session_outcome: string;
};
internal_observations: Array<{ layer: string; ask_theme: string | null }>;
latest_result: {
confirmation_allowed: boolean;
indistinguishable_width_minutes: number;
window_scan: { d9_candidates_differ: boolean } | null;
session_outcome: string;
};
}>;
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
assert.deepEqual(projection.conversation_summary.missing_evidence_categories, ["relocation", "health", "finance"]);
// 原值: d2_finance / finance
// 新值: d9_relationship / relationship
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(projection.method_followup_plan.next_followup?.method_id, "d9_relationship");
assert.equal(projection.method_followup_plan.next_followup?.domain, "relationship");
assert.equal(projection.method_followup_plan.deferred_followup, null);
assert.equal(projection.method_followup_plan.session_outcome, "collect_evidence");
assert.equal(
(projection as { next_user_action?: { id?: string; on_user_stop?: { id?: string } } }).next_user_action?.id,
"ask_method_followup",
);
assert.equal(
(projection as { next_user_action?: { on_user_stop?: { id?: string } } }).next_user_action?.on_user_stop?.id,
"offer_provisional_range",
);
assert.equal(projection.latest_result.session_outcome, "collect_evidence");
assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style");
assert.equal(projection.latest_result.confirmation_allowed, false);
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true);
assert.doesNotMatch(JSON.stringify(projection), UNIQUE_MINUTE_COPY);
assert.doesNotMatch(JSON.stringify(projection), /A\/B\/C\/D/);
assert.match(JSON.stringify(projection.method_followup_plan.next_followup), /自然语言/);
});
test("accepted batch evidence resolves a spoken collect focus before the next question", async () => {
const restore = stubEngine(ENGINE_SCORE);
let collectFocusResolved = false;
try {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence],
latestResult: null,
conversationSummary: conversationSummaryFixture({
activeFocus: collectFocusResolved
? null
: activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "education",
expectedAnswerSchema: { collect: true, prompt: "有没有记得住时间的升学经历?" },
}),
}),
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
resolve_agentic_rectification_conversation_focus: (_fn, args) => {
collectFocusResolved = true;
return {
focus_id: args.p_focus_id,
status: args.p_status,
evidence_id: args.p_evidence_id,
idempotent: false,
};
},
persist_agentic_rectification_candidate_v2: () => ({
...candidateSnapshotFixture(),
cached: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string; executed_methods: string[]; error_code: string | null };
open_question: { prompt?: string } | null;
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "completed");
assert.ok(result.rescore.executed_methods.includes("d1-rashi"));
assert.equal(accounting.calls[0]?.fn, "get_agentic_rectification_case_dossier");
const resolvedFocus = accounting.calls.find((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.equal(resolvedFocus?.args.p_focus_id, FOCUS_ID);
assert.equal(resolvedFocus?.args.p_status, "resolved");
assert.equal(resolvedFocus?.args.p_evidence_id, EDUCATION_ID);
const recordIndex = accounting.calls.findIndex((call) => call.fn === "record_agentic_rectification_evidence_batch");
const resolveIndex = accounting.calls.findIndex((call) => call.fn === "resolve_agentic_rectification_conversation_focus");
assert.ok(recordIndex >= 0 && resolveIndex > recordIndex);
assert.doesNotMatch(result.open_question?.prompt ?? "", /升学经历/);
const persistCall = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
assert.ok(persistCall);
const receipt = persistCall.args.p_decision_receipt as { window_scan?: { d9_candidates_differ?: boolean; d9_sign_names?: unknown } };
assert.equal(receipt.window_scan?.d9_candidates_differ, true);
assert.deepEqual((receipt.window_scan as { d9_sign_names?: string[] } | undefined)?.d9_sign_names, ["白羊座", "天蝎"]);
const completedReceipt = accounting.calls.find((call) =>
call.fn === "insert_agentic_rectification_tool_receipt"
&& call.args.p_tool_name === "rectification-record-evidence-batch"
&& call.args.p_status === "completed"
);
assert.ok(completedReceipt);
assert.ok((completedReceipt.args.p_executed_methods as string[]).includes("d1-rashi"));
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_to_status === "candidate_ready"
),
false,
);
assert.doesNotMatch(JSON.stringify(result), UNIQUE_MINUTE_COPY);
} finally {
restore();
}
});
test("cached candidates retry a failed VedAstro validation without recomputing ranking", async () => {
const validationResponse = {
status: "passed",
can_confirm_exact_minute: true,
event_validation: {
search_events_primary_supports_local_winner: true,
},
minute_sensitive_validation: {
status: "passed",
},
};
const restore = stubEngine(validationResponse);
try {
const compute = computeFixture();
const evidence = [educationEvidence];
const evidenceFingerprint = evidenceLedgerFingerprint([{
id: educationEvidence.id,
sourceTurnId: educationEvidence.source_turn_id,
subject: educationEvidence.subject,
eventKind: educationEvidence.event_kind,
domain: educationEvidence.domain,
occurredFrom: educationEvidence.occurred_from,
occurredTo: educationEvidence.occurred_to,
datePrecision: educationEvidence.date_precision,
summary: educationEvidence.summary,
status: educationEvidence.status,
supersedesEvidenceId: educationEvidence.supersedes_evidence_id,
createdAt: educationEvidence.created_at,
}]);
const rangeFingerprint = candidateRangeFingerprint(
compute.candidate_range,
compute.baseline_profile_fingerprint,
);
const cached = {
...candidateSnapshotFixture({
representativeTime: "05:02",
evidenceLedgerFingerprint: evidenceFingerprint,
decisionReceipt: {
inference_state: producedInferenceState([
{ id: CANDIDATE_ID, time: "05:02", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "04:55", relative_support: 42 },
]),
gates: {
exact_confirmation: {
external_validation_status: "failed",
vedastro_event_validation: {
status: "failed",
search_events_primary_supports_local_winner: false,
can_confirm_exact_minute: false,
failure: { code: "timeout" },
},
},
},
},
}),
candidate_range_fingerprint: rangeFingerprint,
};
const refreshedReceipt = {
...cached.decision_receipt,
gates: {
exact_confirmation: {
external_validation_status: "passed",
vedastro_event_validation: {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
},
},
},
};
const parsedCached = parseV9CaseDossier(dossierFixture({ evidence, latestResult: cached }));
assert.equal(parsedCached?.latestResult?.selectionAllowed, true);
assert.equal(parsedCached?.latestResult?.evidenceLedgerFingerprint, evidenceFingerprint);
assert.equal(parsedCached?.latestResult?.candidateRangeFingerprint, rangeFingerprint);
assert.equal(readVedastroMinuteSensitiveStatus(parsedCached?.latestResult?.decisionReceipt), "failed");
assert.equal(authoritativeCandidateProjection(parsedCached!.latestResult!).candidates.length, 2);
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence,
latestResult: cached,
}),
get_agentic_rectification_case_compute: () => compute,
refresh_agentic_rectification_vedastro_validation: () => ({
result_id: RESULT_ID,
decision_receipt: refreshedReceipt,
}),
set_agentic_rectification_conversation_focus: (_fn, args) => ({
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-08-27T00:00:00.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续比较",
accounting: accounting.client as never,
});
const result = await (tools["rectification-compare-candidates"] as unknown as {
execute(input: unknown): Promise<{
cached: boolean;
candidates: Array<{ time: string; relativeSupport: number }>;
confirmation_gate: { blockers: Array<{ id: string; status: string }> };
}>;
}).execute({ caseId: CASE_ID });
const refreshCall = accounting.calls.find((call) =>
call.fn === "refresh_agentic_rectification_vedastro_validation"
);
assert.ok(refreshCall, JSON.stringify(accounting.calls.map((call) => call.fn)));
assert.deepEqual(refreshCall.args.p_validation, {
status: "passed",
search_events_primary_supports_local_winner: true,
can_confirm_exact_minute: true,
failure: null,
});
assert.equal(refreshCall.args.p_minute_sensitive_status, "passed");
assert.equal(
accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"),
false,
);
assert.equal(result.cached, true);
assert.deepEqual(result.candidates.map((item) => [item.time, item.relativeSupport]), [
["05:02", 58],
["04:55", 42],
]);
assert.ok(result.confirmation_gate, JSON.stringify(result));
assert.equal(
result.confirmation_gate.blockers.find((gate) => gate.id === "vedastro_minute_sensitive")?.status,
"passed",
);
} finally {
restore();
}
});
test("evidence batch returns the persisted choice prompt as open_question", async () => {
const restore = stubEngine({
...ENGINE_SCORE,
decision_receipt: {
...ENGINE_SCORE.decision_receipt,
discriminating_event_probes: [{
year: 2023,
year_label: "2023 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "年份锁定 2023 年前后。事件家族:搬家、离乡或长期异地。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 1.09,
semantic_key: "relocation.2023.dasha_activation",
candidate_set_version: "set-test",
candidate_split_hash: "set-test:relocation:2023",
candidate_ids: ["04:50", "04:51"],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:50"], conflicts: ["04:51"] },
{ answer_class: "no", supports: ["04:51"], conflicts: ["04:50"] },
],
style_options: DYNAMIC_STYLE_OPTIONS,
choice_kind: "existence",
}],
},
});
try {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, relationshipEvidence, familyEvidence, careerEvidence],
evidenceCount: 4,
latestResult: null,
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
persist_agentic_rectification_candidate_v2: (_fn, args) => ({
...candidateSnapshotFixture({
decisionReceipt: args.p_decision_receipt as Record<string, unknown>,
}),
cached: false,
}),
set_agentic_rectification_conversation_focus: (_fn, args) => ({
focus: {
id: FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-08-25T14:47:09.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string };
open_question: { prompt?: string } | null;
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "completed");
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
const schema = setFocus?.args.p_expected_answer_schema as { choice?: { prompt?: string } } | undefined;
assert.match(schema?.choice?.prompt ?? "", /2023 年前后/);
assert.match(result.open_question?.prompt ?? "", /2023 年前后/);
assert.match(result.open_question?.prompt ?? "", /搬家、离乡或长期异地/);
assert.doesNotMatch(result.open_question?.prompt ?? "", /高考/);
} finally {
restore();
}
});
test("rescore failure does not fail the evidence write", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence],
}),
get_agentic_rectification_case_compute: () => computeFixture(),
record_agentic_rectification_evidence_batch: () => ({
items: [{
index: 0,
outcome: "accepted",
evidence_id: EDUCATION_ID,
status: "confirmed",
idempotent: false,
clarification_fields: [],
error_code: null,
}],
accepted_count: 1,
needs_clarification_count: 0,
rejected_count: 0,
focus_id: null,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "2016年6月高考结束",
accounting: accounting.client as never,
});
const result = await (tools["rectification-record-evidence-batch"] as unknown as {
execute(input: unknown): Promise<{
accepted_count: number;
rescore: { status: string; error_code: string | null };
}>;
}).execute({
caseId: CASE_ID,
items: [{
quote: "2016年6月高考结束",
proposedKind: "education_milestone",
subject: "self",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06",
summary: "2016年6月一次学业节点",
}],
});
assert.equal(result.accepted_count, 1);
assert.equal(result.rescore.status, "failed");
assert.ok(result.rescore.error_code);
});
test("public tool surface stays at 14 and new cases bind 10.0.14", () => {
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.14");
const deprecated = resolveExactSkillPackage(
"jyotish-birth-time-rectification",
"10.0.2",
"8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30",
);
assert.equal(deprecated.status, "deprecated");
const plateauCandidates = [
{ candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 },
{ candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 },
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
];
const plateau = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
confirmationAllowed: true,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: producedInferenceState(plateauCandidates.map((item) => ({
id: item.candidateId,
time: item.time,
relative_support: item.relativeSupport,
}))),
execution_ledger: [
{ method: "vimshottari-dasha", status: "executed" },
{ method: "narayana-dasha", status: "executed" },
],
},
}, collectDecision(plateauCandidates));
assert.equal(plateau.confirmation_allowed, false);
assert.equal(plateau.unique_minute_claim, false);
assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/);
assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY);
assert.equal(plateau.session_outcome, "collect_evidence");
// Previously this asserted multi_adapter_consensus from execution names alone.
// The execution ledger does not prove same-range support or raw-result agreement.
assert.equal(plateau.rectification_label, "manual_pattern_consensus");
assert.deepEqual(plateau.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const collecting = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
execution_ledger: [
{ method: "vimshottari-dasha", status: "executed" },
{ method: "narayana-dasha", status: "executed" },
],
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: false,
datedEventCount: 2,
datedDomainCount: 2,
candidateScores: plateauCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
})),
}));
assert.equal(collecting.rectification_label, "user_history_verification_required");
assert.equal(collecting.evidence_stop_reason, "insufficient_dated_events");
assert.deepEqual(collecting.executed_methods, ["vimshottari-dasha", "narayana-dasha"]);
const exhaustedCandidates = plateauCandidates.map((item, index) => ({
...item,
relativeSupport: index < 2 ? 40 : 20,
}));
const completed = latestResultToolProjection({
resultId: RESULT_ID,
candidates: exhaustedCandidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
datedEventCount: 3,
datedDomainCount: 2,
candidateScores: exhaustedCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
})),
}));
assert.equal(completed.termination_copy, "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。");
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
assert.match(skill, /method_followup_plan/);
assert.match(skill, /感情 → 事业 → 家人 → 职业 → 占问/);
assert.match(skill, /外貌、体质、胎记或疤痕不得追问/);
assert.match(skill, /KP 观察不计分、不挡提出门/);
assert.match(skill, /唯一领先和宽度≤5只挡确认门/);
assert.match(skill, /D9\/D10 类型表是校时方法/);
assert.doesNotMatch(skill, /±5 分钟确定性/);
assert.doesNotMatch(skill, /KP 政策跳过不挡提出门/);
const tools = readFileSync(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url), "utf8");
assert.match(tools, /function agentVisibleLatestProjection/);
assert.match(tools, /candidate_contrast_packet: _packet/);
assert.match(tools, /current_probe: null/);
});
test("Mastra hides active candidates when the receipt range is corrupted", () => {
const candidates = [
{ candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 58, tiedMinuteCount: 1 },
{ candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 42, tiedMinuteCount: 1 },
];
const inferenceState = producedInferenceState([
{ id: CANDIDATE_ID, time: "05:00", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "05:07", relative_support: 42 },
]);
const latest = {
resultId: RESULT_ID,
candidates,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "05:00",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
inference_state: inferenceState,
},
};
const session = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
userStopped: true,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
holdoutValidation: "passed",
snapshotCurrent: true,
trainingGateOpen: true,
});
const valid = latestResultToolProjection(latest, session);
assert.deepEqual((valid.candidates as typeof candidates).map((item) => item.time), ["05:00", "05:07"]);
assert.equal(valid.selection_allowed, true);
const invalid = latestResultToolProjection({
...latest,
decisionReceipt: {
...latest.decisionReceipt,
inference_state: { ...inferenceState, credible_range: ["04:00", "04:10"] },
},
}, session);
assert.deepEqual(invalid.candidates, []);
assert.equal(invalid.representative_time, null);
assert.equal(invalid.selection_allowed, false);
});
test("family then occupation then horary follow the method plan without appearance or marks", () => {
const afterFamily = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
],
});
assert.equal(afterFamily.next_followup?.method_id, "occupation");
assert.equal(afterFamily.next_followup?.domain, "occupation");
assert.equal(afterFamily.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy");
assert.equal(afterFamily.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: afterFamily.next_followup,
methods: afterFamily.methods,
}), "collect_evidence");
assert.equal(afterFamily.next_followup?.choice_frame, null);
assert.match(afterFamily.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(afterFamily.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
const afterOccupation = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
],
});
assert.equal(afterOccupation.next_followup?.method_id, "horary");
assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /第 10 宫|D10|占问/);
assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /自然语言/);
assert.doesNotMatch(afterOccupation.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
assert.doesNotMatch(JSON.stringify(afterFamily), /外貌|疤痕|胎记/);
const afterHorary = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "horary", datePrecision: "day", occurredFrom: "2024-01-01", occurredTo: null },
],
});
assert.equal(afterHorary.next_followup, null);
});
test("precision stage lagna_frame waits for uncovered career before asking another dated event", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
],
precisionStage: "lagna_frame",
});
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("lagna_frame after classic coverage does not keep a tie in discrimination without a remaining split", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
precisionStage: "lagna_frame",
});
assert.equal(plan.next_followup?.source, "precision_stage");
assert.equal(plan.next_followup?.ask_theme, "dated_event");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods, { separated: true }), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
discriminatorProbe: null,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
// 原断言 provisional_range(并列不采用)→ 新断言 adopt_representative。
}), "adopt_representative");
});
test("precision stage d4 asks home change not family, and d5 asks education", () => {
const covered = CLASSIC_COVERAGE;
const d4 = buildMethodFollowupPlan({
evidence: covered,
precisionStage: "d4_refine",
});
assert.equal(d4.next_followup?.source, "precision_stage");
assert.equal(d4.next_followup?.method_id, "d4_home");
assert.equal(d4.next_followup?.domain, "relocation");
assert.equal(d4.next_followup?.ask_theme, "home_change");
assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/);
const legacyTheme = buildMethodFollowupPlan({
evidence: [...covered],
precisionStage: "theme_refine",
});
assert.equal(legacyTheme.next_followup?.ask_theme, "home_change");
const d5 = buildMethodFollowupPlan({
evidence: covered.filter((item) => item.domain !== "education"),
precisionStage: "d5_refine",
});
assert.equal(d5.next_followup?.method_id, "d5_education");
assert.equal(d5.next_followup?.domain, "education");
assert.doesNotMatch(JSON.stringify(d4), UNIQUE_MINUTE_COPY);
});
test("accepted representative time reverse-verifies predicted events then hands off", () => {
const careerProbe = {
year: 2018,
year_label: "2018 年前后",
domain: "career" as const,
event_family: "入职、升职或职责明显加重",
source: "dasha_activation" as const,
tracks: ["vimshottari", "narayana"] as const,
tracks_agree: true,
unique_minute_claim: false as const,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify" as const,
style_options: DYNAMIC_STYLE_OPTIONS,
};
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
accepted: true,
eventProbes: [
{
...careerProbe,
domain: "education",
source: "known_event_quality",
role: "distinguish",
event_family: "高考或重要考试发挥明显失常、压力很大",
},
careerProbe,
],
oosBlindPrompts: [{
domain: "family",
user_meaning: "校时还没用过家人这条线。有没有一件没提过、但记得大概时间的家人变化?",
used_for_scoring: false,
}],
activeFocus: {
intent: "collect_method_evidence",
targetDomain: "relationship",
targetKind: null,
},
});
assert.equal(plan.next_followup?.method_id, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.scoring, true);
assert.match(plan.next_followup?.choice_frame?.why ?? "", /2018 年前后/);
assert.equal(plan.deferred_followup, null);
assert.equal(plan.next_followup?.source, "reverse_verify");
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: plan.next_followup,
workingTime: "05:07",
accepted: true,
});
assert.equal(action.id, "verify_adopted_time");
assert.equal(action.on_user_stop.id, "start_consultation");
assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY);
});
test("accepted representative time without remaining probes hands off to consultation", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-06-01",
occurredTo: null,
}],
accepted: true,
eventProbes: [{
year: 2016,
year_label: "2016 年前后",
domain: "education",
event_family: "高考或重要考试发挥明显失常、压力很大",
source: "known_event_quality",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2016 年前后。已有高考经历。",
role: "distinguish",
}],
});
assert.equal(plan.next_followup, null);
assert.equal(plan.deferred_followup, null);
const action = buildNextUserAction({
scorableCount: 3,
evidenceCount: 3,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "adopt_representative",
nextFollowup: plan.next_followup,
workingTime: "05:07",
accepted: true,
});
assert.equal(action.id, "start_consultation");
assert.match(action.user_meaning, /看盘/);
assert.match(action.user_meaning, /改选/);
assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY);
});
test("confirmed relationship evidence skips generic D9 followups unless a real probe is bound", () => {
const evidence = CLASSIC_COVERAGE;
const precision = buildMethodFollowupPlan({ evidence, precisionStage: "d9_refine" });
const varga = buildMethodFollowupPlan({
evidence,
observations: [{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" }],
});
assert.notEqual(precision.next_followup?.domain, "relationship");
assert.notEqual(varga.next_followup?.domain, "relationship");
const probed = buildMethodFollowupPlan({
evidence,
precisionStage: "d9_refine",
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d9.05:00|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在关系盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:14"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d9.05:00|05:14",
informationGain: 1.4,
sourceFeatures: [{ technique: "D9", calculationResultId: RESULT_ID }],
domain: "relationship",
year: null,
semanticKey: "varga.d9.05:00|05:14",
choiceKind: "varga_style",
styleOptions: [
{ label: "相处更主动热情", answerClass: "yes", sign: "白羊座" },
{ label: "相处更深刻占有", answerClass: "weak_yes", sign: "天蝎座" },
],
}],
},
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2021,
year_label: "2021 年前后",
domain: "relationship",
event_family: "关系状态或相处方式明显变化",
semantic_key: "relationship.2021.dasha_activation",
candidate_split_hash: "set-test:relationship:2021",
}],
});
assert.equal(probed.next_followup?.domain, "relationship");
assert.equal(probed.next_followup?.source, "event_probe");
assert.equal(probed.next_followup?.semantic_key, "varga.d9.05:00|05:14");
assert.doesNotMatch(probed.next_followup?.semantic_key ?? "", /relationship\.2021/);
});
test("d9_refine after relationship still asks uncovered career first", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2020-06-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null },
],
precisionStage: "d9_refine",
});
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: false,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
});
test("career evidence does not cover occupation method; occupation no longer blocks adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
],
});
assert.equal(plan.next_followup?.method_id, "occupation");
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
assert.equal(blockingMethodsCovered(plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("draft occupation_note without a date covers occupation and does not adopt a tie", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "职业类型轨迹为技术开发",
},
],
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
// 原断言 sessionOutcome≠adopt_representative(并列不采用)→ 新断言 adopt_representative。
// 为什么:occupation 覆盖后并列候选仍应交付代表性采用;确认门保持关闭。
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("stale occupation collect focus does not keep interviewing after occupation is covered", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{
status: "draft",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "前端工程师",
},
],
activeFocus: {
intent: "collect_method_evidence",
targetDomain: "occupation",
targetKind: "occupation_note",
},
});
// 原断言 sessionOutcome≠adopt_representative → 新断言 adopt_representative。
// 为什么:职业已覆盖后不得继续追问;并列候选走代表性采用,而不是停在无出口访谈。
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
contrastPacket: {
candidateSetVersion: "05:00-05:02",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
probes: [{
probeId: "contrast:varga.d10.05:00|05:01|05:02",
candidateSetVersion: "05:00-05:02",
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01", "05:02"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00", "05:02"] },
{ outcomeId: "no", supportsCandidateIds: ["05:02"], conflictsCandidateIds: ["05:00", "05:01"] },
],
candidateSplitHash: "varga.d10.05:00|05:01|05:02",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.05:00|05:01|05:02",
choiceKind: "varga_style",
styleOptions: [
{ label: "做事偏领导推进", answerClass: "yes", sign: "白羊座" },
{ label: "做事偏研究转化", answerClass: "weak_yes", sign: "天蝎座" },
],
}],
},
});
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/);
assert.equal(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",
},
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("answered duty language skips window D10 and collects a dated education event instead of a yearless D24 card", () => {
const packet = {
candidateSetVersion: "05:00-05:07",
vargaDifferences: [
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
{ layer: "d24", signs: ["05:00", "05:06|05:07"] },
],
probes: [{
probeId: "contrast:varga.d24.05:00/05:06|05:07",
candidateSetVersion: "05:00-05:07",
question: "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:06", "05:07"] },
{ outcomeId: "no", supportsCandidateIds: ["05:06", "05:07"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:06|05:07",
informationGain: 0.16,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:06|05:07",
}],
};
const evidence = [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
{
status: "draft" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "医疗器械算法,第三个(技术执行)",
},
];
const plan = buildMethodFollowupPlan({
evidence,
contrastPacket: packet,
});
assert.equal(plan.next_followup?.domain, "education");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.choice_frame, null);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /记得住年份/);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/);
});
test("yearless D24 yields to a dated career dasha instead of borrowing the recorded education year", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2023,
year_label: "2023 年前后",
semantic_key: "career.2023.dasha_activation",
information_gain: 0.56,
candidate_split_hash: "set-test:career:2023",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
informationGain: 2.503258,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
choiceKind: "event_quality",
}],
},
candidatesSeparated: false,
});
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
assert.equal(plan.next_followup?.choice_frame?.period, "2023 年前后");
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /2023 年前后,有没有/);
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /2016 年前后/);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
});
const YEARLESS_D12 = {
probeId: "contrast:varga.d12.05:00/05:07",
candidateSetVersion: "05:00-05:07",
question: "当前几个候选在六亲盘上还分得开。请核对一段还没用进评分的家人前事。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d12.05:00/05:07",
informationGain: 3.5,
sourceFeatures: [{ technique: "D12", calculationResultId: RESULT_ID }],
domain: "family",
year: null,
semanticKey: "varga.d12.05:00/05:07",
choiceKind: "existence" as const,
};
test("yearless family contrast yields to a dated career discriminator", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("relationship", "2018"),
datedEvidence("relocation", "2014"),
datedEvidence("health_pressure", "2012"),
{
status: "confirmed" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
],
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2020,
year_label: "2020 年 4 月前后",
information_gain: 0.56,
semantic_key: "career.2020.dasha_activation",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:07",
vargaDifferences: [],
probes: [YEARLESS_D12],
},
candidatesSeparated: false,
});
assert.notEqual(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.choice_frame != null, true);
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2020 年 4 月前后/);
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /2020 年 4 月前后,有没有/);
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /那段时间/);
});
test("yearless family contrast without a dated discriminator asks dated family collect", () => {
const plan = buildMethodFollowupPlan({
evidence: [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary" && item.domain !== "family"),
datedEvidence("relocation", "2014"),
],
contrastPacket: {
candidateSetVersion: "05:00-05:07",
vargaDifferences: [],
probes: [YEARLESS_D12],
},
candidatesSeparated: false,
evidenceCollectionProbes: [{
year: 2021,
year_label: "2021 年前后",
domain: "family",
event_family: "家人结婚、添丁或住院",
source: "age_band",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
role: "collect",
phase: "evidence_collection",
information_gain: 0,
semantic_key: "family.2021",
}],
});
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.choice_frame, null);
assert.equal(plan.next_followup?.probe_year, 2021);
assert.equal(plan.dropped_probes.some((item) => (
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
)), true);
});
test("family collect attaches the collection-probe year instead of a yearless D24 card", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("career", "2020"),
datedEvidence("career", "2024"),
datedEvidence("relationship", "2024"),
datedEvidence("career", "2026"),
datedEvidence("relationship", "2024", { datePrecision: "day" }),
datedEvidence("relationship", "2025", { datePrecision: "day" }),
],
askedProbeKeys: ["relocation.2015.05.dasha_boundary"],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:14",
candidateSetVersion: "05:00-05:14",
question: "引擎给出的区分机会绑定 D24。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:14",
informationGain: 2.5,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:14",
}],
},
evidenceCollectionProbes: [{
year: 2021,
year_label: "2021 年前后",
domain: "family",
event_family: "家人结婚、添丁或住院",
source: "age_band",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
role: "collect",
phase: "evidence_collection",
information_gain: 0,
semantic_key: "family.2021",
}],
candidatesSeparated: false,
});
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.choice_frame, null);
assert.equal(plan.next_followup?.probe_year, 2021);
assert.match(plan.next_followup?.year_label ?? "", /2021/);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
assert.match(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后,家里/);
});
test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => {
const plan = buildMethodFollowupPlan({
evidence: [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary" && item.domain !== "family"),
datedEvidence("relocation", "2014"),
],
contrastPacket: {
candidateSetVersion: "05:00-05:07",
vargaDifferences: [],
probes: [YEARLESS_D12],
},
candidatesSeparated: false,
evidenceCollectionProbes: [{
year: 2021,
year_label: "2021 年前后",
domain: "family",
event_family: "家人结婚、添丁或住院",
source: "age_band",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
role: "collect",
phase: "evidence_collection",
information_gain: 0,
semantic_key: "family.2021",
}],
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "family",
targetKind: "family_event",
expectedAnswerSchema: {
semantic_key: "varga.d12.05:00/05:07",
choice: {
prompt: "那段时间,有没有家人相关的明显变化?",
option_a: "明确发生且时间吻合",
option_b: "发生过但程度较弱",
option_c: "明确没有发生",
option_d: "这段记不清楚",
options: DYNAMIC_STYLE_OPTIONS.map((option, index) => ({
key: (["A", "B", "C", "D"] as const)[index]!,
label: option.label,
answer_class: option.answer_class,
})),
},
},
},
});
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "family");
assert.equal(plan.next_followup?.choice_frame, null);
assert.equal(plan.next_followup?.probe_year, 2021);
});
test("already-open dated career card stays ahead of a yearless D24 catalog row", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2023,
year_label: "2023 年前后",
semantic_key: "career.2023.dasha_activation",
information_gain: 0.56,
candidate_split_hash: "set-test:career:2023",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
informationGain: 2.503258,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
choiceKind: "event_quality",
}],
},
candidatesSeparated: false,
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "career",
targetKind: "career_entry",
expectedAnswerSchema: {
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "set-test:career:2023",
},
},
});
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
});
const DUMP_COVERAGE = [
{
status: "confirmed" as const,
domain: "education",
datePrecision: "month" as const,
occurredFrom: "2016-09-01",
occurredTo: null,
eventKind: "education_start",
summary: "上大学",
},
{
status: "confirmed" as const,
domain: "education",
datePrecision: "month" as const,
occurredFrom: "2015-06-01",
occurredTo: null,
eventKind: "education_interruption",
summary: "高考失利复读",
},
{
status: "confirmed" as const,
domain: "relationship",
datePrecision: "day" as const,
occurredFrom: "2024-05-01",
occurredTo: null,
eventKind: "relationship_start",
summary: "开始一段感情",
},
{
status: "confirmed" as const,
domain: "relationship",
datePrecision: "day" as const,
occurredFrom: "2024-08-08",
occurredTo: null,
eventKind: "relationship_end",
summary: "感情结束",
},
{
status: "confirmed" as const,
domain: "career",
datePrecision: "day" as const,
occurredFrom: "2024-04-07",
occurredTo: null,
eventKind: "career_entry",
summary: "入职",
},
{
status: "confirmed" as const,
domain: "family",
datePrecision: "month" as const,
occurredFrom: "2016-05-01",
occurredTo: null,
eventKind: "family_event",
summary: "家人变化",
},
{
status: "confirmed" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "互联网程序员 / 前端 / Agent 开发",
},
];
const DUMP_TRANSITIONS = [
{ layer: "d4", at: "05:00" },
{ layer: "d4", at: "05:03" },
{ layer: "d10", at: "05:00" },
{ layer: "d10", at: "05:03" },
{ layer: "d24", at: "05:00" },
{ layer: "d24", at: "05:03" },
{ layer: "d9", at: "04:52" },
{ layer: "d9", at: "05:08" },
{ layer: "d5", at: "05:15" },
];
const DUMP_SCORES = [
{ time: "05:00", score: 34 },
{ time: "05:03", score: 33 },
{ time: "05:04", score: 33 },
];
test("coverage-complete tie with encoded D24/D10 collects a dated move instead of a yearless D4 card", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: DUMP_SCORES.map((item) => item.time),
transitions: DUMP_TRANSITIONS,
mentionedKeys: mentionedVargaKeysFromLedgerEvidence(DUMP_COVERAGE),
});
const plan = buildMethodFollowupPlan({
evidence: DUMP_COVERAGE,
contrastPacket: packet,
});
assert.equal(plan.next_followup?.domain, "relocation");
assert.equal(plan.next_followup?.kind_hint, "home_change");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.choice_frame, null);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /记得住时间的搬家/);
assert.doesNotMatch(plan.next_followup?.kind_hint ?? "", /education_start|relationship_end/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /大学哪年入学|高考是 \d{4}|哪年毕业/);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
discriminatorProbe: packet.probes[0] ?? null,
candidateScores: DUMP_SCORES,
}), "discriminate_candidates");
});
test("provisional range still exposes method coverage followup", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: DUMP_SCORES.map((item) => item.time),
transitions: DUMP_TRANSITIONS,
askedKeys: ["varga.d4", "varga.d5", "varga.d9", "varga.d10", "varga.d24"],
});
const plan = buildMethodFollowupPlan({
evidence: [
...DUMP_COVERAGE,
{
status: "confirmed" as const,
domain: "relocation",
datePrecision: "year" as const,
occurredFrom: "2016-09-01",
occurredTo: null,
eventKind: "home_change",
summary: "搬家离乡",
},
],
contrastPacket: packet,
sessionOutcome: "provisional_range",
});
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "horary");
assert.equal(plan.deferred_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: {
method_id: "d9_relationship",
intent: "distinguish_candidates",
ask_theme: "relationship_style",
domain: "relationship",
kind_hint: "relationship_change",
user_prompt_hint: "当前候选在关系主题上仍分不开。",
must_not_label: false,
choice_frame: null,
source: "varga_observation",
},
methods: plan.methods,
discriminatorProbe: null,
candidateScores: DUMP_SCORES,
// 原断言 provisional_range → 新断言 adopt_representative。
}), "adopt_representative");
const action = buildNextUserAction({
scorableCount: 6,
evidenceCount: 8,
hasLatestResult: true,
selectionAllowed: true,
sessionOutcome: "provisional_range",
nextFollowup: null,
workingTime: "05:00",
});
assert.equal(action.id, "offer_provisional_range");
});
test("structured paused state ends evidence collection without parsing user copy", () => {
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: null,
methods: buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
}).methods,
userStopped: true,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 33 },
{ time: "05:07", score: 33 },
],
// 原断言 provisional_range_user_stopped → 新断言 adopt_representative。
// 为什么:用户停止后仍可交付代表性采用;validated / 确认门保持关闭。
}), "adopt_representative");
});
test("same-year existence probes are skipped; a different-year dasha still ranks", () => {
const evidence = [
datedEvidence("education", "2016"),
datedEvidence("relationship", "2018"),
datedEvidence("career", "2015"),
datedEvidence("family", "2023"),
];
const skipped = buildMethodFollowupPlan({
evidence,
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2015,
year_label: "2015 年前后",
information_gain: 0.56,
semantic_key: "career.2015.dasha_activation",
candidate_split_hash: "set-test:career:2015",
}],
});
assert.notEqual(skipped.next_followup?.semantic_key, "career.2015.dasha_activation");
const dated = buildMethodFollowupPlan({
evidence,
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2018,
year_label: "2018 年前后",
information_gain: 0.56,
semantic_key: "career.2018.dasha_activation",
}],
});
assert.equal(dated.next_followup?.semantic_key, "career.2018.dasha_activation");
assert.match(dated.next_followup?.choice_frame?.period ?? "", /2018 年前后/);
const childhood = buildMethodFollowupPlan({
evidence,
birthDate: "1997-08-08",
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2012,
year_label: "2012 年 11 月前后",
information_gain: 1.3,
semantic_key: "career.2012.11.dasha_boundary",
candidate_split_hash: "set-test:career:2012",
}, {
...CAREER_CONFLICT_PROBE,
year: 2019,
year_label: "2019 年前后",
information_gain: 0.4,
semantic_key: "career.2019.dasha_activation",
candidate_split_hash: "set-test:career:2019",
}],
});
assert.notEqual(childhood.next_followup?.semantic_key, "career.2012.11.dasha_boundary");
assert.equal(childhood.next_followup?.semantic_key, "career.2019.dasha_activation");
const adjacentRelationship = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("career", "2020"),
datedEvidence("relationship", "2024"),
datedEvidence("family", "2023"),
],
eventProbes: [{
...CAREER_CONFLICT_PROBE,
domain: "relationship",
event_family: "开始一段认真关系、分手或结婚",
year: 2023,
year_label: "2023 年 5 月前后",
information_gain: 0.9,
semantic_key: "relationship.2023.05.dasha_boundary",
candidate_split_hash: "set-test:relationship:2023",
}],
});
assert.notEqual(adjacentRelationship.next_followup?.semantic_key, "relationship.2023.05.dasha_boundary");
const d24 = {
probeId: "contrast:varga.d24.05:00|05:07|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00|05:07|05:14",
informationGain: 2.5,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00|05:07|05:14",
choiceKind: "event_quality" as const,
};
const d10 = {
probeId: "contrast:varga.d10.05:00|05:07|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在事业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:14"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:07"], conflictsCandidateIds: ["05:00", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:14"], conflictsCandidateIds: ["05:00", "05:07"] },
],
candidateSplitHash: "varga.d10.05:00|05:07|05:14",
informationGain: 3.1,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.05:00|05:07|05:14",
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "做事偏领导推进", answerClass: "yes" as const, sign: "白羊座" },
{ label: "做事偏研究转化", answerClass: "weak_yes" as const, sign: "天蝎座" },
],
};
const highest = buildMethodFollowupPlan({
evidence,
eventProbes: [{
...CAREER_CONFLICT_PROBE,
information_gain: 0.56,
semantic_key: "career.2018.dasha_activation",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [d24, d10],
},
candidatesSeparated: false,
});
assert.equal(highest.next_followup?.semantic_key, d10.semanticKey);
assert.doesNotMatch(highest.next_followup?.semantic_key ?? "", /career\.2018/);
const d24Wins = buildMethodFollowupPlan({
evidence,
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [d24, { ...d10, informationGain: 1.1 }],
},
candidatesSeparated: false,
});
assert.equal(d24Wins.next_followup?.semantic_key, d10.semanticKey);
});
test("three dated events with one holdout keep collecting instead of discriminating", () => {
const evidence = [
datedEvidence("education", "2016"),
datedEvidence("career", "2020"),
datedEvidence("relationship", "2024"),
{
status: "confirmed" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
];
const gate = trainingScoreableGate(evidence);
assert.equal(gate.open, false);
assert.equal(gate.trainingCount, 2);
assert.equal(gate.holdoutCount, 1);
const plan = buildMethodFollowupPlan({
evidence,
declinedTopics: [{ target_domain: "family", status: "declined" }],
precisionStage: "lagna_frame",
eventProbes: [CAREER_CONFLICT_PROBE],
});
// 原值: method_coverage / finance3847e9c9 新增 domain 锁)
// 新值: method_coverage / finance
// 原因: dated 分支在家人之后;家人已拒答,下一件带年份仍是财务,不是轮转抢跑。
assert.equal(plan.next_followup?.source, "method_coverage");
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
assert.equal(plan.next_followup?.domain, "finance");
assert.notEqual(plan.next_followup?.source, "event_probe");
assert.notEqual(plan.next_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
evidence,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
discriminatorProbe: {
probeId: "p-cd",
candidateSetVersion: "set-test",
question: "2018 年前后事业是否有明显变化?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] },
{ outcomeId: "no", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "split",
informationGain: 0.4,
sourceFeatures: [{ technique: "dasha_activation", calculationResultId: null }],
domain: "career",
year: 2018,
semanticKey: "career.2018",
},
}), "collect_evidence");
});
test("adjacent education year does not re-ask enrollment after a recorded start", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "education",
eventKind: "education_start",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: null,
summary: "2016年9月上大学",
}, {
status: "confirmed",
domain: "relationship",
eventKind: "relationship_end",
datePrecision: "day",
occurredFrom: "2024-08-08",
occurredTo: "2024-08-08",
summary: "一段感情结束",
}],
eventProbes: [{
year: 2015,
year_label: "2015 年前后",
domain: "education",
event_family: "升学、高考、转学或学习环境变化",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2015 年前后。事件家族:升学、高考、转学或学习环境变化。请写成一句自然语言是/否题。不得改年份。",
role: "reverse_verify",
information_gain: 0.21,
semantic_key: "education.2015.dasha_activation",
}],
});
assert.notEqual(plan.next_followup?.domain, "education");
assert.notEqual(plan.next_followup?.source, "event_probe");
// 原值: relatives
// 新值: d10_career
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
assert.equal(plan.next_followup?.method_id, "d10_career");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2016 年入学/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2024 年感情结束/);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /不要再问这些事发生在哪一年/);
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /高考是 2015/);
});
test("high information_gain leftover probe still blocks offering after coverage", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.21,
semantic_key: "relocation.2018.dasha_activation",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "discriminate_candidates");
});
test("event_probe still discriminates after coverage when candidates remain tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "relocation",
event_family: "搬家、离乡或长期异地",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.21,
semantic_key: "relocation.2018.dasha_activation",
candidate_ids: ["05:00", "05:20"],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
],
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("accepted time reverse-verifies an uncovered year in a covered domain", () => {
const plan = buildMethodFollowupPlan({
evidence: [{
status: "confirmed",
domain: "career",
datePrecision: "day",
occurredFrom: "2024-04-07",
occurredTo: null,
}],
accepted: true,
eventProbes: [{
year: 2018,
year_label: "2018 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。",
role: "reverse_verify",
style_options: DYNAMIC_STYLE_OPTIONS,
}],
});
assert.equal(plan.next_followup?.source, "reverse_verify");
assert.equal(plan.next_followup?.domain, "career");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后");
});
test("declining occupation covers the method; declining horary is skipped_by_policy", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null },
{ status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
{ status: "confirmed", domain: "appearance", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
{ status: "confirmed", domain: "marks", datePrecision: "unknown", occurredFrom: null, occurredTo: null },
],
declinedTopics: [
{ target_domain: "occupation", status: "declined" },
{ target_domain: "horary", status: "declined" },
],
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "skipped_by_policy");
assert.equal(plan.next_followup, null);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "validated_range");
});
test("horary follow-up does not block propose once occupation is covered", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
});
assert.equal(plan.next_followup?.method_id, "horary");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "validated_range");
});
test("offer-candidates refuses while method coverage remains", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [educationEvidence, relationshipEvidence, familyEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence(
[educationEvidence, relationshipEvidence, familyEvidence],
),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
false,
);
});
test("paused case with selection_allowed may offer the escape hatch", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence, familyEvidence, careerEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence([
educationEvidence,
familyEvidence,
careerEvidence,
]),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
decisionReceipt: {
inference_state: producedInferenceState([
{ id: CANDIDATE_ID, time: "04:48", relative_support: 58 },
{ id: SECOND_CANDIDATE_ID, time: "04:49", relative_support: 42 },
]),
},
}),
}),
transition_agentic_rectification_case_status: () => ({
case_id: CASE_ID,
status: "candidate_ready",
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
assert.equal(projection.session_outcome, "provisional_range_user_stopped");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
false,
);
});
test("offer-candidates refuses a stored snapshot with a missing fingerprint", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: "paused",
evidence: [educationEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: null,
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
});
test("paused case resumes only when the Agent explicitly requests it", async () => {
let reads = 0;
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
status: reads++ === 0 ? "paused" : "collecting_evidence",
evidence: [],
}),
transition_agentic_rectification_case_status: (_fn, args) => ({
case_id: CASE_ID,
status: args.p_status,
idempotent: false,
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
userMessage: "继续校正",
accounting: accounting.client as never,
});
const projection = await (tools["rectification-read-case"] as unknown as {
execute(input: unknown): Promise<{ status: string }>;
}).execute({ caseId: CASE_ID, resume: true });
assert.equal(projection.status, "collecting_evidence");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "collecting_evidence"
),
true,
);
});
test("offer-candidates allows a 34/33/33 tie after method coverage when remaining minutes do not split", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: methodCoverageTieEvidence,
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence(methodCoverageTieEvidence),
decisionReceipt: {
propose_allowed: true,
window_scan: {
scanned: true,
d9_lagna_count: 3,
d10_lagna_count: 3,
d9_candidates_differ: true,
d10_candidates_differ: true,
d9_sign_names: ["天秤", "天蝎", "射手"],
d10_sign_names: ["巨蟹", "狮子", "处女"],
},
},
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "05:00", relative_support: 34, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "05:01", relative_support: 33, tied_minute_count: 1 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "05:02", relative_support: 33, tied_minute_count: 1 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
// 原断言 session_outcome=provisional_range(并列不采用)→ 新断言 adopt_representative。
assert.equal(projection.session_outcome, "adopt_representative");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
&& call.args.p_status === "candidate_ready"
),
true,
);
});