fix(rectification): persist the next interview after a choice tap
Closing a discriminator used to leave GET without a card after refresh. Write the next dated question in the same request, skip childhood career and move probes, and do not continue a read-only turn when that question is already persisted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, spokenFollowupForUser } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||||
import {
|
||||
askedKeysFromLedgerEvidence,
|
||||
@@ -1741,20 +1741,25 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
|
||||
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
|
||||
],
|
||||
probes: [{
|
||||
probeId: "contrast:varga.d10.巨蟹|狮子|处女",
|
||||
probeId: "contrast:varga.d10.05:00|05:01|05:02",
|
||||
candidateSetVersion: "05:00-05:02",
|
||||
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "supports_巨蟹", supportsCandidateIds: ["巨蟹"], conflictsCandidateIds: ["狮子", "处女"] },
|
||||
{ outcomeId: "supports_狮子", supportsCandidateIds: ["狮子"], conflictsCandidateIds: ["巨蟹", "处女"] },
|
||||
{ outcomeId: "supports_处女", supportsCandidateIds: ["处女"], conflictsCandidateIds: ["巨蟹", "狮子"] },
|
||||
{ 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.巨蟹|狮子|处女",
|
||||
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.巨蟹|狮子|处女",
|
||||
semanticKey: "varga.d10.05:00|05:01|05:02",
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [
|
||||
{ label: "做事偏领导推进", answerClass: "yes", sign: "白羊座" },
|
||||
{ label: "做事偏研究转化", answerClass: "weak_yes", sign: "天蝎座" },
|
||||
],
|
||||
}],
|
||||
},
|
||||
});
|
||||
@@ -1789,7 +1794,7 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
|
||||
}), "discriminate_candidates");
|
||||
});
|
||||
|
||||
test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
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: [
|
||||
@@ -1829,11 +1834,13 @@ test("answered duty language skips window D10 and uses remaining D24", () => {
|
||||
contrastPacket: packet,
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "education");
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:06|05:07");
|
||||
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("low-gain career event probe does not outrank a renderable high-gain D24 contrast probe", () => {
|
||||
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: [{
|
||||
@@ -1866,15 +1873,183 @@ test("low-gain career event probe does not outrank a renderable high-gain D24 co
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
|
||||
assert.equal(plan.next_followup?.choice_frame?.option_d_answer_class, "unsure");
|
||||
assert.equal(plan.next_followup?.choice_frame?.period, "当前这几个候选");
|
||||
assert.match(plan.next_followup?.choice_frame?.prompt ?? "", /学业|考试发挥|学习压力/);
|
||||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /入职、升职/);
|
||||
assert.ok((plan.next_followup?.selection_score ?? 0) > 0.56);
|
||||
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/);
|
||||
});
|
||||
|
||||
test("already-open low-gain career card yields to the high-gain D24 catalog winner", () => {
|
||||
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 collects a dated family event", () => {
|
||||
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,
|
||||
});
|
||||
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.match(plan.next_followup?.user_prompt_hint ?? "", /记得住时间/);
|
||||
assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/);
|
||||
});
|
||||
|
||||
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 does not keep 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,
|
||||
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.notEqual(plan.next_followup?.choice_frame?.prompt, "那段时间,有没有家人相关的明显变化?");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.domain, "family");
|
||||
});
|
||||
|
||||
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: [{
|
||||
@@ -1916,8 +2091,8 @@ test("already-open low-gain career card yields to the high-gain D24 catalog winn
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
|
||||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /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 = [
|
||||
@@ -2004,7 +2179,7 @@ const DUMP_SCORES = [
|
||||
{ time: "05:04", score: 33 },
|
||||
];
|
||||
|
||||
test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollment or breakup", () => {
|
||||
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),
|
||||
@@ -2018,9 +2193,12 @@ test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollme
|
||||
});
|
||||
assert.equal(plan.next_followup?.domain, "relocation");
|
||||
assert.equal(plan.next_followup?.kind_hint, "home_change");
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /varga\.d4/);
|
||||
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.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /那段时间/);
|
||||
assert.equal(conversationalSessionOutcome({
|
||||
selectionAllowed: true,
|
||||
proposeAllowed: true,
|
||||
@@ -2114,7 +2292,7 @@ test("structured paused state ends evidence collection without parsing user copy
|
||||
});
|
||||
|
||||
|
||||
test("covered-domain existence probes are skipped; the highest remaining discriminator wins", () => {
|
||||
test("same-year existence probes are skipped; a different-year dasha still ranks", () => {
|
||||
const evidence = [
|
||||
datedEvidence("education", "2016"),
|
||||
datedEvidence("relationship", "2018"),
|
||||
@@ -2125,12 +2303,69 @@ test("covered-domain existence probes are skipped; the highest remaining discrim
|
||||
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.notEqual(skipped.next_followup?.domain, "career");
|
||||
assert.notEqual(skipped.next_followup?.source, "event_probe");
|
||||
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",
|
||||
@@ -2195,7 +2430,7 @@ test("covered-domain existence probes are skipped; the highest remaining discrim
|
||||
},
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(d24Wins.next_followup?.semantic_key, d24.semanticKey);
|
||||
assert.equal(d24Wins.next_followup?.semantic_key, d10.semanticKey);
|
||||
});
|
||||
|
||||
test("three dated events with one holdout keep collecting instead of discriminating", () => {
|
||||
|
||||
Reference in New Issue
Block a user