Files
Jyotisha/frontend/tests/rectification-probe-year-dedupe-20260906.test.ts
T
Jesse_ChenandCursor 8ae17a2630 fix(rectification): drop same-year probes after a dated answer (BUG-592)
Point-choice used one compare packet, so a May existence card and the same-domain year card both stayed eligible. Hard-exclude same domain+year; keep adjacent years downranked.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 15:47:25 +08:00

550 lines
21 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts";
import { inspectDiscriminatorProbes } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
import { decideAfterInferenceChange } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import type { DecisionDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import {
buildMethodFollowupPlan,
datedLedgerAnchor,
existenceProbeAsked,
remainingReverseVerifyProbes,
sameYearProbeAsked,
type MethodFollowupEvidence,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import type { CandidateContrastPacket } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { askedSemanticKeysForEngine } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { engineRequestBody, toEngineEvents } from "../src/lib/rectification-agentic/v9/engine-client.ts";
import { EXISTENCE_STYLE_OPTIONS } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts";
function existenceProbe(
domain: DiscriminatingEventProbe["domain"],
year: number,
extra: { month?: number; source?: DiscriminatingEventProbe["source"]; key?: string; gain?: number } = {},
): DiscriminatingEventProbe {
const month = extra.month;
const source = extra.source ?? "dasha_boundary";
const key = extra.key
?? (month
? `${domain}.${year}.${String(month).padStart(2, "0")}.${source}`
: `${domain}.${year}.${source}`);
return {
year,
year_label: month ? `${year}${month} 月前后` : `${year} 年前后`,
month,
domain,
event_family: domain === "relocation" ? "搬家或长期住到外地" : "入职、升职或职责明显加重",
source,
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: `时间范围锁定 ${year} 年。`,
role: "distinguish",
information_gain: extra.gain ?? 1.1,
semantic_key: key,
candidate_split_hash: key,
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"] },
],
choice_kind: "existence",
};
}
function dated(
id: string,
domain: string,
eventKind: string,
occurredFrom: string,
): MethodFollowupEvidence {
return {
id,
status: "confirmed",
domain,
datePrecision: "month",
occurredFrom,
occurredTo: null,
eventKind,
};
}
const D10_STYLE: CandidateContrastPacket["probes"][number] = {
probeId: "contrast:varga.d10.巨蟹座/狮子座",
candidateSetVersion: "05:00-05:20",
question: "平时做事,你更接近下面哪一种?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d10.巨蟹座/狮子座",
informationGain: 1.4,
sourceFeatures: [{ technique: "D10", calculationResultId: null }],
domain: "career",
year: null,
semanticKey: "varga.d10.巨蟹座/狮子座",
choiceKind: "varga_style",
styleOptions: [
{ label: "做事以照顾人为主,在意团队里的感受", answerClass: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" },
],
};
test("existenceProbeAsked treats the same career year and nearby years as already asked", () => {
const asked = ["career.2018.05.dasha_boundary"];
assert.equal(existenceProbeAsked(asked, "career", 2018), true);
assert.equal(existenceProbeAsked(asked, "career", 2017), true);
assert.equal(existenceProbeAsked(asked, "career", 2019), true);
assert.equal(existenceProbeAsked(asked, "career", 2020), false);
assert.equal(existenceProbeAsked(asked, "finance", 2018), false);
});
test("remaining reverse-verify probes drop same-domain nearby years after a month probe", () => {
const remaining = remainingReverseVerifyProbes(
[
existenceProbe("career", 2018, { month: 5 }),
existenceProbe("career", 2018, { source: "dasha_activation" }),
existenceProbe("career", 2017, { month: 5 }),
existenceProbe("career", 2019, { month: 5 }),
existenceProbe("career", 2020, { month: 5 }),
],
[],
new Set(),
new Set(["career.2018.05.dasha_boundary"]),
);
const years = remaining.filter((item) => item.domain === "career").map((item) => item.year);
assert.equal(years.includes(2017), false);
assert.equal(years.includes(2018), false);
assert.equal(years.includes(2019), false);
assert.equal(years.includes(2020), true);
});
test("datedLedgerAnchor names the confirmed same-domain month", () => {
const anchor = datedLedgerAnchor([
dated("e-career-month", "career", "career_entry", "2018-07-01"),
], "career");
assert.ok(anchor);
assert.equal(anchor?.label, "2018 年 7 月");
assert.equal(datedLedgerAnchor([
dated("e-edu", "education", "education_start", "2016-09-01"),
], "career"), null);
});
test("unanchored D10 varga_style cards are dropped; anchored cards mention the ledger month", () => {
const baseEvidence = [
dated("e-edu", "education", "education_start", "2016-09-01"),
dated("e-edu-2", "education", "education_completion", "2020-06-01"),
dated("e-rel", "relationship", "relationship_start", "2024-05-01"),
dated("e-fin", "finance", "finance_loss", "2021-01-01"),
];
const packet = {
candidateSetVersion: "05:00-05:20",
vargaDifferences: [] as const,
probes: [D10_STYLE],
};
const unanchored = buildMethodFollowupPlan({
evidence: baseEvidence,
contrastPacket: packet,
candidatesSeparated: false,
});
assert.equal(
unanchored.dropped_probes.some((item) => item.reason === "unanchored_varga_style"),
true,
);
assert.notEqual(unanchored.next_followup?.semantic_key, D10_STYLE.semanticKey);
const anchored = buildMethodFollowupPlan({
evidence: [...baseEvidence, dated("e-career", "career", "career_entry", "2018-07-01")],
contrastPacket: packet,
candidatesSeparated: false,
});
assert.equal(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey);
assert.equal(anchored.next_followup?.choice_kind, "varga_style");
assert.match(anchored.next_followup?.user_prompt_hint ?? "", /2018 年 7 月/);
assert.doesNotMatch(anchored.next_followup?.choice_frame?.prompt ?? "", /2018/);
});
test("after a D9-style answer the compare request body stays legal", () => {
const hash = "04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:06,05:08,05:13,05:15:varga.d9.04:47|04:51/05:00|05:06|04:59|04:53/05:08|05:13|05:15";
assert.equal(hash.length, 128);
const receipt = {
inference_state: {
answered_probes: [{
probe_id: "contrast:varga.d9.相处",
semantic_key: "varga.d9.巨蟹座/狮子座",
candidate_split_hash: hash,
answer_class: "yes",
classified_from: "choice",
}],
},
};
const asked = askedSemanticKeysForEngine(receipt, []);
assert.equal(asked.includes(hash), false);
assert.ok(asked.every((key) => key.length <= 120 && !key.includes(":varga.")));
const body = engineRequestBody({
baselineBirthSnapshot: {
birth_date: "1997-08-08",
latitude: 36.42,
longitude: 114.21,
timezone_offset: 8,
},
candidateRange: { start_time: "04:45", end_time: "05:15" },
events: toEngineEvents([{
id: "00000000-0000-4000-8000-000000000001",
sourceTurnId: "33333333-3333-4333-8333-333333333333",
subject: "self",
eventKind: "education_start",
domain: "education",
occurredFrom: "2016-09-01",
occurredTo: "2016-09-30",
datePrecision: "month",
summary: "大学入学",
}]),
askedProbeKeys: asked,
});
const keys = (body.asked_probe_keys as string[] | undefined) ?? [];
assert.ok(keys.every((key) => key.length <= 120 && !key.includes(":varga.")));
});
const COVERED_FOR_DISCRIMINATE: MethodFollowupEvidence[] = [
dated("e-edu", "education", "education_start", "2016-09-01"),
dated("e-rel", "relationship", "relationship_start", "2018-05-01"),
dated("e-career", "career", "career_entry", "2020-04-01"),
dated("e-fam", "family", "family_event", "2019-03-01"),
{
id: "e-occ",
status: "confirmed",
domain: "occupation",
datePrecision: "unknown",
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
},
];
const CAREER_2023_05_KEY = "career.2023.05.dasha_boundary";
const CAREER_2023_ACTIVATION_KEY = "career.2023.dasha_activation";
const CAREER_2024_04_KEY = "career.2024.04.dasha_boundary";
const CAREER_2023_05 = existenceProbe("career", 2023, { month: 5, gain: 1.1, key: CAREER_2023_05_KEY });
const CAREER_2023_ACTIVATION = existenceProbe("career", 2023, {
source: "dasha_activation",
gain: 0.46,
key: CAREER_2023_ACTIVATION_KEY,
});
const CAREER_2024_04 = existenceProbe("career", 2024, { month: 4, gain: 0.97, key: CAREER_2024_04_KEY });
const RELOCATION_2015_05 = existenceProbe("relocation", 2015, { month: 5, gain: 0.74 });
const D9_STYLE: CandidateContrastPacket["probes"][number] = {
...D10_STYLE,
probeId: "contrast:varga.d9.巨蟹座/狮子座",
question: "亲密关系里更接近下面哪一种相处方式?",
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
informationGain: 1.53,
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
domain: "relationship",
semanticKey: "varga.d9.巨蟹座/狮子座",
styleOptions: [
{ label: "相处里更主动,也更愿意把关系往前推", answerClass: "yes", sign: "巨蟹座" },
{ label: "相处里更克制,先把分寸看清楚", answerClass: "weak_yes", sign: "狮子座" },
],
};
const D10_LOW: CandidateContrastPacket["probes"][number] = {
...D10_STYLE,
informationGain: 0.4,
};
function contrastFromEvent(probe: DiscriminatingEventProbe): CandidateContrastPacket["probes"][number] {
const yes = probe.expected_outcomes?.find((row) => row.answer_class === "yes")?.supports ?? ["05:00"];
const no = probe.expected_outcomes?.find((row) => row.answer_class === "no")?.supports ?? ["05:20"];
const key = probe.semantic_key ?? `${probe.domain}.${probe.year}`;
return {
probeId: `probe:${key}`,
candidateSetVersion: "05:00-05:20",
question: probe.year_label,
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: [...yes], conflictsCandidateIds: [...no] },
{ outcomeId: "weak_yes", supportsCandidateIds: [], conflictsCandidateIds: [] },
{ outcomeId: "no", supportsCandidateIds: [...no], conflictsCandidateIds: [...yes] },
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
],
candidateSplitHash: probe.candidate_split_hash ?? key,
informationGain: probe.information_gain ?? 0,
sourceFeatures: [{ technique: "Vimshottari", calculationResultId: null }],
domain: probe.domain,
year: probe.year,
semanticKey: key,
choiceKind: "existence",
};
}
function accidentPacket(probes: readonly CandidateContrastPacket["probes"][number][]): CandidateContrastPacket {
return {
candidateSetVersion: "05:00-05:20",
vargaDifferences: [],
probes,
};
}
function conflictFromContrast(probe: CandidateContrastPacket["probes"][number]): ConflictProbe {
const yes = probe.expectedOutcomes.find((row) => row.outcomeId === "yes")?.supportsCandidateIds ?? ["05:00"];
const no = probe.expectedOutcomes.find((row) => row.outcomeId === "no")?.supportsCandidateIds ?? ["05:20"];
return {
id: probe.probeId,
semantic_key: probe.semanticKey,
candidate_split_hash: probe.candidateSplitHash,
domain: probe.domain ?? "career",
year: probe.year && probe.year > 0 ? probe.year : 0,
question: probe.question,
candidate_ids: [...new Set([...yes, ...no])],
expected_outcomes: [
{ answer_class: "yes", supports: [...yes], conflicts: [...no] },
{ answer_class: "weak_yes", supports: [], conflicts: [] },
{ answer_class: "no", supports: [...no], conflicts: [...yes] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: probe.informationGain,
source: probe.choiceKind === "varga_style" ? "varga_contrast" : "dasha_boundary",
choice_kind: probe.choiceKind ?? "existence",
style_options: probe.styleOptions
? probe.styleOptions.map((item) => ({
label: item.label,
answer_class: item.answerClass,
...(item.sign ? { sign: item.sign } : {}),
}))
: [...EXISTENCE_STYLE_OPTIONS],
};
}
function answered(probe: { probeId?: string; id?: string; semanticKey?: string; semantic_key?: string; candidateSplitHash?: string; candidate_split_hash?: string }) {
return {
probe_id: probe.probeId ?? probe.id ?? "",
semantic_key: probe.semanticKey ?? probe.semantic_key ?? "",
candidate_split_hash: probe.candidateSplitHash ?? probe.candidate_split_hash ?? "",
answer_class: "no" as const,
classified_from: "choice" as const,
};
}
function accidentState(
remaining: readonly CandidateContrastPacket["probes"][number][],
asked: readonly ReturnType<typeof answered>[],
) {
const times = ["05:00", "05:20"] as const;
const probes = remaining.map(conflictFromContrast);
const raw = {
algorithm_version: INFERENCE_ALGORITHM_VERSION,
candidate_set_id: candidateSetId("05:00", "05:20", times),
revision: asked.length + 1,
phase: "discrimination" as const,
result_status: "discriminating" as const,
range_start: "05:00",
range_end: "05:20",
candidates: times.map((time, index) => ({
id: time,
time,
cluster_range: [time, time] as const,
prior_score: time === "05:00" ? 20 : 10,
posterior_score: time === "05:00" ? 20 : 10,
probability: time === "05:00" ? 0.67 : 0.33,
status: "active" as const,
rank: index + 1,
strong_conflict_count: 0,
})),
events: [
{ id: "e-edu", domain: "education", year: 2016, precision: "month" as const, usage: "training" as const },
{ id: "e-rel", domain: "relationship", year: 2018, precision: "month" as const, usage: "training" as const },
{ id: "e-career", domain: "career", year: 2020, precision: "month" as const, usage: "training" as const },
{ id: "e-fam", domain: "family", year: 2019, precision: "month" as const, usage: "holdout" as const },
],
probes: [
...asked.map((item) => remaining.find((probe) => probe.semanticKey === item.semantic_key)).filter(Boolean).map((probe) => conflictFromContrast(probe as CandidateContrastPacket["probes"][number])),
...probes,
],
answered_probes: asked,
rounds: [],
last_inference_round: null,
entropy: 1.2,
representative_time: "05:00",
credible_range: ["05:00", "05:20"] as const,
holdout_passed: null,
};
const loaded = asInferenceState(raw);
assert.ok(loaded, "accident inference fixture must pass asInferenceState");
return loaded;
}
function accidentDossier(
remaining: readonly CandidateContrastPacket["probes"][number][],
asked: readonly ReturnType<typeof answered>[],
): DecisionDossier {
const state = accidentState(remaining, asked);
return {
evidence: COVERED_FOR_DISCRIMINATE,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: {
resultId: "55555555-5555-4555-8555-555555555555",
selectionAllowed: true,
confirmationAllowed: false,
evidenceLedgerFingerprint: "fp-same-year",
candidates: [
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "05:00", rank: 1, relativeSupport: 20 },
{ candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2", time: "05:20", rank: 2, relativeSupport: 10 },
],
representativeTime: "05:00",
decisionReceipt: {
accept_allowed: true,
acceptance_allowed: true,
propose_allowed: true,
selection_allowed: true,
confirmation_allowed: false,
inference_state: state,
},
},
case: { acceptedTime: null, status: "active" },
};
}
test("sameYearProbeAsked is exact domain+year; nearby years stay on existenceProbeAsked", () => {
const asked = ["career.2023.05.dasha_boundary"];
assert.equal(sameYearProbeAsked(asked, "career", 2023), true);
assert.equal(sameYearProbeAsked(asked, "career", 2024), false);
assert.equal(sameYearProbeAsked(asked, "relocation", 2023), false);
assert.equal(sameYearProbeAsked(asked, "career", 0), false);
assert.equal(existenceProbeAsked(asked, "career", 2024), true);
});
test("after a May 2023 career probe, the same-year activation card is dropped not ranked", () => {
const packet = accidentPacket([
contrastFromEvent(CAREER_2023_05),
contrastFromEvent(CAREER_2023_ACTIVATION),
]);
const inspected = inspectDiscriminatorProbes(packet, {
askedKeys: [CAREER_2023_05_KEY],
});
assert.notEqual(inspected.selected?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
assert.equal(
inspected.dropped.some((item) => (
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
)),
true,
);
const decided = decideAfterInferenceChange({
dossier: accidentDossier(
[contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2023_ACTIVATION)],
[answered(contrastFromEvent(CAREER_2023_05))],
),
state: accidentState(
[contrastFromEvent(CAREER_2023_ACTIVATION)],
[answered(contrastFromEvent(CAREER_2023_05))],
),
userStopped: false,
birthDate: "1997-08-08",
});
assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
assert.equal(
decided.droppedProbes.some((item) => (
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
)),
true,
);
});
test("adjacent-year career probes stay eligible after a 2023 answer", () => {
const packet = accidentPacket([
contrastFromEvent(CAREER_2023_ACTIVATION),
contrastFromEvent(CAREER_2024_04),
]);
const inspected = inspectDiscriminatorProbes(packet, {
askedKeys: [CAREER_2023_05_KEY],
});
assert.equal(inspected.selected?.semanticKey, CAREER_2024_04.semantic_key);
assert.equal(
inspected.dropped.some((item) => (
item.semantic_key === CAREER_2024_04.semantic_key && item.reason === "same_year_asked"
)),
false,
);
const decided = decideAfterInferenceChange({
dossier: accidentDossier(
[contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)],
[answered(contrastFromEvent(CAREER_2023_05))],
),
state: accidentState(
[contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)],
[answered(contrastFromEvent(CAREER_2023_05))],
),
userStopped: false,
birthDate: "1997-08-08",
});
assert.equal(decided.probe?.semanticKey, CAREER_2024_04.semantic_key);
});
test("buildMethodFollowupPlan drops the same-year activation after the May card", () => {
const plan = buildMethodFollowupPlan({
evidence: COVERED_FOR_DISCRIMINATE,
contrastPacket: accidentPacket([
contrastFromEvent(CAREER_2023_05),
contrastFromEvent(CAREER_2023_ACTIVATION),
contrastFromEvent(CAREER_2024_04),
]),
askedProbeKeys: [CAREER_2023_05_KEY],
candidatesSeparated: false,
birthDate: "1997-08-08",
});
assert.notEqual(plan.next_followup?.semantic_key, CAREER_2023_ACTIVATION.semantic_key);
assert.equal(
plan.dropped_probes.some((item) => (
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
)),
true,
);
assert.equal(plan.next_followup?.semantic_key, CAREER_2024_04.semantic_key);
});
test("accident replay: fifth card is not the 2023 activation after four answers", () => {
const pool = [
D9_STYLE,
contrastFromEvent(CAREER_2023_05),
contrastFromEvent(CAREER_2024_04),
contrastFromEvent(RELOCATION_2015_05),
contrastFromEvent(CAREER_2023_ACTIVATION),
D10_LOW,
];
const asked = [
answered(D9_STYLE),
answered(contrastFromEvent(CAREER_2023_05)),
answered(contrastFromEvent(CAREER_2024_04)),
answered(contrastFromEvent(RELOCATION_2015_05)),
];
const remaining = [
contrastFromEvent(CAREER_2023_ACTIVATION),
D10_LOW,
];
const decided = decideAfterInferenceChange({
dossier: accidentDossier(pool, asked),
state: accidentState(remaining, asked),
userStopped: false,
birthDate: "1997-08-08",
});
assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key);
assert.equal(
decided.droppedProbes.some((item) => (
item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked"
)),
true,
);
assert.ok(
decided.probe?.semanticKey === D10_LOW.semanticKey
|| decided.nextAction === "ready_to_adopt"
|| decided.sessionOutcome === "adopt_representative",
);
});