fix(rectification): invite-first collect, holdout at 4 events, Skill 10.0.23 (BUG-646–648)
Stop domain-wheel collecting and age-band years in prompts. Ask until the training gate, then discriminate until convergence, then deliver a range plus a concrete follow-up. Reserve holdout only with four dated events. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
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 { GENERIC_COLLECT_QUESTION, collectQuestionForDomain, withFirstDatedCollectInvite } from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { GENERIC_COLLECT_QUESTION } from "../src/lib/rectification-agentic/user-copy.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";
|
||||
@@ -101,6 +101,25 @@ const CLASSIC_COVERAGE = [
|
||||
{ status: "confirmed", domain: "horary", datePrecision: "day" as const, occurredFrom: "2024-01-01", occurredTo: null },
|
||||
];
|
||||
|
||||
function assertInviteCollect(plan: ReturnType<typeof buildMethodFollowupPlan>) {
|
||||
// 原值: 训练门关时按感情 → 事业 → 家人轮转
|
||||
// 新值: 收集池首条是「还有吗」邀请
|
||||
// 原因: 用户先说完,再从已说的事追问(BUG-648)
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
assert.equal(plan.next_followup?.method_id, "dasha_events");
|
||||
assert.equal(plan.next_followup?.domain, "other");
|
||||
assert.equal(plan.next_followup?.collection_key, "collect:invite:more");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
}
|
||||
|
||||
function assertNoLeftoverDatedCollect(plan: ReturnType<typeof buildMethodFollowupPlan>) {
|
||||
// 原值: 训练门开后仍按未覆盖领域采集
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: S2/S3 走选择题或交付(BUG-648)
|
||||
assert.notEqual(plan.next_followup?.intent, "collect_method_evidence");
|
||||
}
|
||||
|
||||
function datedEvidence(
|
||||
domain: string,
|
||||
year: string,
|
||||
@@ -420,11 +439,7 @@ test("eight-method routing asks relationship after dated education, not relocati
|
||||
occurredTo: null,
|
||||
}],
|
||||
});
|
||||
// 原值: relatives / family
|
||||
// 新值: d9_relationship / relationship
|
||||
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assert.equal(plan.next_followup?.domain, "relationship");
|
||||
assertInviteCollect(plan);
|
||||
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");
|
||||
@@ -435,12 +450,10 @@ test("dasha conflict probe does not jump ahead of method rotation before accepta
|
||||
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);
|
||||
// 原值: method_coverage / relatives 或 d9_relationship
|
||||
// 新值: 邀请「还有吗」,选择题不得抢在训练门前
|
||||
// 原因: 收集池在训练门开之前优先邀请(BUG-648)
|
||||
assertInviteCollect(plan);
|
||||
assert.notEqual(plan.next_followup?.source, "event_probe");
|
||||
});
|
||||
|
||||
@@ -449,12 +462,7 @@ test("known exam quality does not create a scoring card after one recorded event
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
|
||||
});
|
||||
|
||||
@@ -466,12 +474,7 @@ test("career known-event quality does not jump the adoption gate", () => {
|
||||
],
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
assert.notEqual(plan.next_followup?.source, "event_probe");
|
||||
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
|
||||
});
|
||||
@@ -481,12 +484,7 @@ test("encoded exam quality does not stamp another card and keeps method rotation
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
});
|
||||
|
||||
test("dasha conflict probe jumps after four scoreable events leave three training domains", () => {
|
||||
@@ -549,12 +547,7 @@ test("three scoreable events in one domain still rotate methods instead of rever
|
||||
],
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
});
|
||||
|
||||
test("occupation_note does not count toward reverse-inference event quality", () => {
|
||||
@@ -573,12 +566,7 @@ test("occupation_note does not count toward reverse-inference event quality", ()
|
||||
],
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
});
|
||||
|
||||
test("age-band probe does not jump ahead of uncovered relationship", () => {
|
||||
@@ -603,12 +591,7 @@ test("age-band probe does not jump ahead of uncovered relationship", () => {
|
||||
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);
|
||||
assertInviteCollect(plan);
|
||||
});
|
||||
|
||||
test("user-stop action records stated events when the ledger is empty", () => {
|
||||
@@ -663,13 +646,8 @@ test("user-stop action offers a range when candidates already exist", () => {
|
||||
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 ?? "", /自然语言/);
|
||||
assertInviteCollect(plan);
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -692,10 +670,7 @@ test("selectionAllowed with remaining method follow-up keeps collecting and offe
|
||||
nextFollowup: plan.next_followup,
|
||||
workingTime: "05:07",
|
||||
});
|
||||
// 原值: relatives
|
||||
// 新值: d9_relationship
|
||||
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assertInviteCollect(plan);
|
||||
assert.equal(action.id, "ask_method_followup");
|
||||
assert.equal(action.on_user_stop.id, "offer_provisional_range");
|
||||
});
|
||||
@@ -711,11 +686,10 @@ test("adopt_representative still asks remaining evidence collect this turn", ()
|
||||
}],
|
||||
sessionOutcome: "adopt_representative",
|
||||
});
|
||||
// 原值: next_followup null,感情采集进 deferred
|
||||
// 新值: 本轮仍问感情采集
|
||||
// 原因: 家人之后还有能区分分钟的经历时,不得因 canAdopt 早退(BUG-546)
|
||||
assert.equal(plan.next_followup?.method_id, "d9_relationship");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
// 原值: 本轮仍问感情采集
|
||||
// 新值: 训练门关时邀请不得因 adopt 早退
|
||||
// 原因: 邀请也是收集池条目(BUG-648)
|
||||
assertInviteCollect(plan);
|
||||
assert.equal(plan.deferred_followup, null);
|
||||
assert.equal(plan.session_outcome, "adopt_representative");
|
||||
});
|
||||
@@ -730,11 +704,10 @@ test("adopt_representative ignores leftover distinguish focus", () => {
|
||||
targetKind: "home_change",
|
||||
},
|
||||
});
|
||||
// 原值: next_followup null(采用早退)
|
||||
// 新值: 丢掉无用区分焦点后仍收财务经历
|
||||
// 原因: 未用的 dated collect 不得被 adopt 早退吃掉(BUG-546)
|
||||
assert.equal(plan.next_followup?.domain, "finance");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
// 原值: 丢掉无用区分焦点后仍收财务经历
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: S3 交付,不再用未覆盖领域挡住采用(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
assert.notEqual(plan.next_followup?.intent, "distinguish_candidates");
|
||||
assert.equal(plan.session_outcome, "adopt_representative");
|
||||
});
|
||||
@@ -750,11 +723,10 @@ test("declined relationship skips to career and leaves horary uncovered", () =>
|
||||
}],
|
||||
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");
|
||||
// 原值: d10_career / career
|
||||
// 新值: 拒答感情不得跳过邀请
|
||||
// 原因: 收集池邀请在产出期间排第一(BUG-648)
|
||||
assertInviteCollect(plan);
|
||||
assert.deepEqual([...plan.do_not_poll], []);
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered");
|
||||
});
|
||||
@@ -802,11 +774,10 @@ test("D9 differ keeps sign names for the type-table report and still forbids uni
|
||||
evidence: CLASSIC_COVERAGE,
|
||||
observations,
|
||||
});
|
||||
// 原值: next_followup null
|
||||
// 新值: 财务采集
|
||||
// 原因: 经典八法覆盖后仍收未用带年份域,D9 观察不再抢在前面(BUG-546)
|
||||
assert.equal(plan.next_followup?.method_id, "d2_finance");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
// 原值: 财务采集
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: 经典八法覆盖后进 S2/S3(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
|
||||
});
|
||||
|
||||
@@ -843,10 +814,9 @@ test("D24-only window change folds into education follow-up without a second lay
|
||||
observations,
|
||||
});
|
||||
// 原值: d5_education / education_style(D24 观察)
|
||||
// 新值: d2_finance 采集
|
||||
// 原因: 家人之后先收未用带年份经历,再进分盘观察(BUG-546)
|
||||
assert.equal(plan.next_followup?.method_id, "d2_finance");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: 家人之后先收未用带年份经历的口径已撤回(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
});
|
||||
|
||||
test("D11-only window change folds into finance follow-up without delaying adopt", () => {
|
||||
@@ -891,12 +861,10 @@ test("D11-only window change folds into finance follow-up without delaying adopt
|
||||
],
|
||||
observations,
|
||||
});
|
||||
// 原值: d2_finance / varga_observation(不挡采用)
|
||||
// 新值: d4_home 搬家采集
|
||||
// 原因: 财务已有带年份证据后,下一未用域是搬家(BUG-546)
|
||||
assert.equal(plan.next_followup?.method_id, "d4_home");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
// 原值: d4_home 搬家采集
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: 财务已有带年份证据后也不再盘问搬家(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
});
|
||||
|
||||
test("window scan displays KP sub-lord changes without opening confirmation", () => {
|
||||
@@ -986,7 +954,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
|
||||
execute(input: unknown): Promise<{
|
||||
conversation_summary: { missing_evidence_categories: string[] };
|
||||
method_followup_plan: {
|
||||
next_followup: { method_id: string; domain: string | null } | null;
|
||||
next_followup: { method_id: string; domain: string | null; intent?: string } | null;
|
||||
deferred_followup: { method_id: string; domain: string | null } | null;
|
||||
session_outcome: string;
|
||||
};
|
||||
@@ -1000,29 +968,17 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
|
||||
}>;
|
||||
}).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");
|
||||
// 原值: d9_relationship / relationship
|
||||
// 新值: 三件三类已开训练门,不再按方法轮转采集
|
||||
// 原因: SQL 缺类不得压过收集池/S2(BUG-648)
|
||||
assert.notEqual(projection.method_followup_plan.next_followup?.intent, "collect_method_evidence");
|
||||
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.engine_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 () => {
|
||||
@@ -1454,9 +1410,9 @@ test("rescore failure does not fail the evidence write", async () => {
|
||||
assert.ok(result.rescore.error_code);
|
||||
});
|
||||
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.22", () => {
|
||||
test("public tool surface stays at 14 and new cases bind 10.0.23", () => {
|
||||
assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14);
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
|
||||
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.23");
|
||||
const deprecated = resolveExactSkillPackage(
|
||||
"jyotish-birth-time-rectification",
|
||||
"10.0.2",
|
||||
@@ -1566,7 +1522,8 @@ test("public tool surface stays at 14 and new cases bind 10.0.22", () => {
|
||||
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.doesNotMatch(skill, /感情 → 事业 → 家人 → 职业 → 占问/);
|
||||
assert.match(skill, /外貌、体质、胎记或疤痕不得追问/);
|
||||
assert.match(skill, /KP 观察不计分、不挡提出门/);
|
||||
assert.match(skill, /唯一领先和宽度≤5只挡确认门/);
|
||||
@@ -1639,23 +1596,13 @@ test("family then unused dated collect then occupation then horary follow the me
|
||||
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
|
||||
],
|
||||
});
|
||||
// 原值: occupation
|
||||
// 新值: d2_finance
|
||||
// 原因: 家人之后按 dated 顺序收财务/搬家/健康,职业排在后面(BUG-546)
|
||||
assert.equal(afterFamily.next_followup?.method_id, "d2_finance");
|
||||
assert.equal(afterFamily.next_followup?.domain, "finance");
|
||||
// 原值: occupation → d2_finance 轮转
|
||||
// 新值: 四件四类已开训练门,不再轮转采集
|
||||
// 原因: S1 只问到训练门开(BUG-648)
|
||||
assertNoLeftoverDatedCollect(afterFamily);
|
||||
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/);
|
||||
assert.notEqual(afterFamily.next_followup?.domain, "finance");
|
||||
const afterDated = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
@@ -1667,8 +1614,8 @@ test("family then unused dated collect then occupation then horary follow the me
|
||||
{ status: "confirmed", domain: "health_pressure", datePrecision: "year", occurredFrom: "2017-01-01", occurredTo: null },
|
||||
],
|
||||
});
|
||||
assert.equal(afterDated.next_followup?.method_id, "occupation");
|
||||
assert.equal(afterDated.next_followup?.domain, "occupation");
|
||||
assert.notEqual(afterDated.next_followup?.domain, "finance");
|
||||
assert.notEqual(afterDated.next_followup?.domain, "relocation");
|
||||
const afterOccupation = buildMethodFollowupPlan({
|
||||
evidence: [
|
||||
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
|
||||
@@ -1710,11 +1657,10 @@ test("precision stage lagna_frame waits for uncovered career before asking anoth
|
||||
],
|
||||
precisionStage: "lagna_frame",
|
||||
});
|
||||
// 原值: relatives
|
||||
// 新值: d10_career
|
||||
// 原因: dated 分支移到家人之后,前两问恢复感情 / 事业
|
||||
assert.equal(plan.next_followup?.method_id, "d10_career");
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
// 原值: relatives → d10_career
|
||||
// 新值: 训练门关时邀请优先
|
||||
// 原因: 精度层不得抢在收集池前(BUG-648)
|
||||
assertInviteCollect(plan);
|
||||
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
|
||||
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
|
||||
});
|
||||
@@ -1724,12 +1670,10 @@ test("lagna_frame after classic coverage does not keep a tie in discrimination w
|
||||
evidence: CLASSIC_COVERAGE,
|
||||
precisionStage: "lagna_frame",
|
||||
});
|
||||
// 原值: precision_stage / dated_event
|
||||
// 新值: 财务采集
|
||||
// 原因: 未用带年份域仍能区分分钟时,精度层让路(BUG-546)
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
assert.equal(plan.next_followup?.domain, "finance");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
// 原值: precision_stage / dated_event → 财务采集
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: 精度层让路给领域轮转的口径已撤回(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
@@ -1742,7 +1686,6 @@ test("lagna_frame after classic coverage does not keep a tie in discrimination w
|
||||
{ time: "05:01", score: 33 },
|
||||
{ time: "05:02", score: 33 },
|
||||
],
|
||||
// 原断言 provisional_range(并列不采用)→ 新断言 adopt_representative。
|
||||
}), "adopt_representative");
|
||||
});
|
||||
|
||||
@@ -1752,12 +1695,10 @@ test("precision stage d4 asks home change not family, and d5 asks education", ()
|
||||
evidence: covered,
|
||||
precisionStage: "d4_refine",
|
||||
});
|
||||
// 原值: precision_stage / d4_home
|
||||
// 新值: 财务采集
|
||||
// 原因: 家人之后先收未用带年份域(BUG-546)
|
||||
assert.equal(d4.next_followup?.source, "method_coverage");
|
||||
assert.equal(d4.next_followup?.method_id, "d2_finance");
|
||||
assert.equal(d4.next_followup?.domain, "finance");
|
||||
// 原值: precision_stage / d4_home → 财务采集
|
||||
// 新值: 训练门开后不再轮转采集
|
||||
// 原因: 家人之后先收未用带年份域的口径已撤回(BUG-648)
|
||||
assertNoLeftoverDatedCollect(d4);
|
||||
assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/);
|
||||
const datedThenHome = [
|
||||
...covered,
|
||||
@@ -1784,11 +1725,10 @@ test("precision stage d4 asks home change not family, and d5 asks education", ()
|
||||
evidence: covered.filter((item) => item.domain !== "education"),
|
||||
precisionStage: "d5_refine",
|
||||
});
|
||||
// 原值: d5_education 精度层
|
||||
// 新值: 学业采集(education 尚未覆盖)
|
||||
assert.equal(d5.next_followup?.method_id, "d5_education");
|
||||
assert.equal(d5.next_followup?.domain, "education");
|
||||
assert.equal(d5.next_followup?.intent, "collect_method_evidence");
|
||||
// 原值: d5_education 精度层 / 学业采集
|
||||
// 新值: 训练门开后不再为未覆盖学业轮转采集
|
||||
// 原因: S2 不再把精度层改写成领域采集(BUG-648)
|
||||
assert.notEqual(d5.next_followup?.intent, "collect_method_evidence");
|
||||
assert.doesNotMatch(JSON.stringify(d4), UNIQUE_MINUTE_COPY);
|
||||
});
|
||||
|
||||
@@ -1971,22 +1911,10 @@ test("d9_refine after relationship still asks uncovered career first", () => {
|
||||
],
|
||||
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");
|
||||
// 原值: d10_career
|
||||
// 新值: 四件两类已开训练门,不再问事业采集
|
||||
// 原因: S1 只问到训练门开(BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
});
|
||||
|
||||
test("career evidence does not cover occupation method; occupation no longer blocks adopt", () => {
|
||||
@@ -1998,10 +1926,10 @@ test("career evidence does not cover occupation method; occupation no longer blo
|
||||
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null },
|
||||
],
|
||||
});
|
||||
// 原值: occupation
|
||||
// 新值: d2_finance
|
||||
// 原因: 家人之后先收财务经历;职业仍未覆盖,也不挡 adopt 能力(BUG-546)
|
||||
assert.equal(plan.next_followup?.method_id, "d2_finance");
|
||||
// 原值: occupation → d2_finance
|
||||
// 新值: 四件四类已开训练门,不再轮转采集
|
||||
// 原因: 职业仍未覆盖,也不挡 adopt(BUG-546 仍在;轮转采集撤回 BUG-648)
|
||||
assertNoLeftoverDatedCollect(plan);
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
|
||||
assert.equal(blockingMethodsCovered(plan.methods), true);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
@@ -2195,12 +2123,11 @@ test("answered duty language skips window D10 and does not re-ask covered educat
|
||||
evidence,
|
||||
contrastPacket: packet,
|
||||
});
|
||||
// 原值: education(yearless D24 直接变成学业采集)
|
||||
// 新值: finance
|
||||
// 原因: CLASSIC_COVERAGE 已有学业;BUG-590 跳过已覆盖领域,改收未覆盖的财务
|
||||
assert.equal(plan.next_followup?.domain, "finance");
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
// 原值: education(yearless D24)→ finance
|
||||
// 新值: 不再把已覆盖学业改写成财务采集;可问占问,不得再出 D10/D24 卡
|
||||
// 原因: yearless→领域轮转已撤(BUG-648)
|
||||
assert.notEqual(plan.next_followup?.domain, "education");
|
||||
assert.notEqual(plan.next_followup?.domain, "finance");
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d(10|24)/);
|
||||
});
|
||||
|
||||
@@ -2326,10 +2253,11 @@ test("yearless family contrast without a dated discriminator asks dated family c
|
||||
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);
|
||||
// 原值: yearless D12 → 家人采集,题干带 2021 年前后
|
||||
// 新值: 训练门开后不把年龄段采集探针写进题干
|
||||
// 原因: 撤回 BUG-642;yearless→领域采集已撤(BUG-648)
|
||||
assert.notEqual(plan.next_followup?.domain, "family");
|
||||
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后/);
|
||||
assert.equal(plan.dropped_probes.some((item) => (
|
||||
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
|
||||
)), true);
|
||||
@@ -2382,19 +2310,11 @@ test("family collect attaches the collection-probe year instead of a yearless D2
|
||||
}],
|
||||
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);
|
||||
// 原值: USER_COLLECT_QUESTION.family,不进题干
|
||||
// 新值: 带 2021 年前后线索,不含「记得大概哪年就行」
|
||||
// 原因: BUG-642
|
||||
assert.equal(plan.next_followup?.year_label, undefined);
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||||
const spoken = spokenFollowupForUser(plan.next_followup);
|
||||
assert.equal(spoken, withFirstDatedCollectInvite(collectQuestionForDomain("family", 2021) ?? ""));
|
||||
assert.match(spoken ?? "", /2021 年前后/);
|
||||
assert.doesNotMatch(spoken ?? "", /记得大概哪年就行/);
|
||||
// 原值: 家人采集带 2021 年前后
|
||||
// 新值: 训练门开后不把年龄段年份写进题干
|
||||
// 原因: 撤回 BUG-642(BUG-648)
|
||||
assert.notEqual(plan.next_followup?.domain, "family");
|
||||
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后/);
|
||||
});
|
||||
|
||||
test("already-open yearless family card is abandoned instead of kept as a scoring frame", () => {
|
||||
@@ -2445,10 +2365,14 @@ test("already-open yearless family card is abandoned instead of kept as a scorin
|
||||
},
|
||||
},
|
||||
});
|
||||
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);
|
||||
// 原值: yearless D12 → 家人采集,题干带 2021 年前后
|
||||
// 新值: 训练门开后不把年龄段采集探针写进题干,也不轮转家人
|
||||
// 原因: 撤回 BUG-642;yearless→领域采集已撤(BUG-648)
|
||||
assert.notEqual(plan.next_followup?.domain, "family");
|
||||
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /2021 年前后/);
|
||||
assert.equal(plan.dropped_probes.some((item) => (
|
||||
item.semantic_key.startsWith("varga.d12") && item.reason === "yearless_ungrounded_contrast"
|
||||
)), true);
|
||||
});
|
||||
|
||||
test("already-open dated career card stays ahead of a yearless D24 catalog row", () => {
|
||||
@@ -2592,22 +2516,24 @@ test("coverage-complete tie with encoded D24/D10 collects a dated move instead o
|
||||
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?.semantic_key ?? "", /varga\.d4/);
|
||||
assert.notEqual(plan.next_followup?.choice_kind, "event_quality");
|
||||
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");
|
||||
// 原值: 会话结果必须是 discriminate_candidates
|
||||
// 新值: 允许 S2 区分或方法层占问采集;不得再走迁居/学业轮转
|
||||
// 原因: yearless D4 改写成迁居采集已撤(BUG-648)
|
||||
assert.ok(
|
||||
plan.next_followup == null
|
||||
|| plan.next_followup.intent === "distinguish_candidates"
|
||||
|| plan.next_followup.method_id === "horary"
|
||||
|| plan.next_followup.method_id === "occupation",
|
||||
JSON.stringify({
|
||||
intent: plan.next_followup?.intent,
|
||||
method_id: plan.next_followup?.method_id,
|
||||
domain: plan.next_followup?.domain,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("provisional range still exposes method coverage followup", () => {
|
||||
@@ -2873,23 +2799,21 @@ test("three dated events with one holdout keep collecting instead of discriminat
|
||||
},
|
||||
];
|
||||
const gate = trainingScoreableGate(evidence);
|
||||
assert.equal(gate.open, false);
|
||||
assert.equal(gate.trainingCount, 2);
|
||||
assert.equal(gate.holdoutCount, 1);
|
||||
// 原值: holdout 从第 2 件起留 1 件,训练只剩 2,门关
|
||||
// 新值: ≥4 件才留 holdout,3 件全训练,门开
|
||||
// 原因: BUG-647
|
||||
assert.equal(gate.open, true);
|
||||
assert.equal(gate.trainingCount, 3);
|
||||
assert.equal(gate.holdoutCount, 0);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence,
|
||||
declinedTopics: [{ target_domain: "family", status: "declined" }],
|
||||
precisionStage: "lagna_frame",
|
||||
eventProbes: [CAREER_CONFLICT_PROBE],
|
||||
});
|
||||
// 原值: method_coverage / finance(3847e9c9 新增 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(plan.next_followup?.source, "event_probe");
|
||||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||||
assert.notEqual(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
@@ -2917,7 +2841,7 @@ test("three dated events with one holdout keep collecting instead of discriminat
|
||||
year: 2018,
|
||||
semanticKey: "career.2018",
|
||||
},
|
||||
}), "collect_evidence");
|
||||
}), "discriminate_candidates");
|
||||
});
|
||||
|
||||
test("adjacent education year does not re-ask enrollment after a recorded start", () => {
|
||||
@@ -2956,14 +2880,11 @@ test("adjacent education year does not re-ask enrollment after a recorded start"
|
||||
});
|
||||
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/);
|
||||
// 原值: relatives → d10_career,题干复述已记年份
|
||||
// 新值: 训练门关时邀请「还有吗」
|
||||
// 原因: 收集池邀请优先,不再用生日或邻年探针改写成事业采集(BUG-648)
|
||||
assertInviteCollect(plan);
|
||||
assert.doesNotMatch(spokenFollowupForUser(plan.next_followup) ?? "", /高考是 2015/);
|
||||
});
|
||||
|
||||
test("high information_gain leftover probe still blocks offering after coverage", () => {
|
||||
@@ -3132,7 +3053,7 @@ test("horary follow-up does not block propose once occupation is covered", () =>
|
||||
}), "validated_range");
|
||||
});
|
||||
|
||||
test("offer-candidates refuses while method coverage remains", async () => {
|
||||
test("offer-candidates may proceed once method coverage leftover collect is closed", async () => {
|
||||
const accounting = fakeAccounting({
|
||||
...receiptHandlers,
|
||||
get_agentic_rectification_case_dossier: () => dossierFixture({
|
||||
@@ -3156,19 +3077,13 @@ test("offer-candidates refuses while method coverage remains", async () => {
|
||||
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,
|
||||
);
|
||||
// 原值: 三件三类仍因方法覆盖挡 offer
|
||||
// 新值: 训练门开后 leftover 采集已关,offer 可走
|
||||
// 原因: BUG-648
|
||||
const result = await (tools["rectification-offer-candidates"] as unknown as {
|
||||
execute(input: unknown): Promise<unknown>;
|
||||
}).execute({ caseId: CASE_ID });
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
test("paused case with selection_allowed may offer the escape hatch", async () => {
|
||||
@@ -3212,7 +3127,11 @@ test("paused case with selection_allowed may offer the escape hatch", async () =
|
||||
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.ok(
|
||||
projection.session_outcome === "provisional_range_user_stopped"
|
||||
|| projection.session_outcome === "adopt_representative"
|
||||
|| projection.session_outcome === "completed_with_range",
|
||||
);
|
||||
assert.equal(
|
||||
accounting.calls.some((call) =>
|
||||
call.fn === "transition_agentic_rectification_case_status"
|
||||
|
||||
Reference in New Issue
Block a user