Files
Jyotisha/frontend/tests/rectification-yearless-probe-downgrade-20260909.test.ts
T
Jesse_ChenandCursor 52db714ba9 fix(rectification): equal-weight collect, year-cue questions, classifier no/unsure (BUG-641–643)
Ask finance and health like other domains, put year-cued collect questions first, and let the classifier distinguish decline vs skip. Skill 10.0.22.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 21:53:10 +08:00

329 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import { applyAnswerToState, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts";
import {
PROBE_WEIGHT,
SCORE_DELTA,
STRONG_CONFLICT_ELIMINATION_COUNT,
type ConflictProbe,
} from "../src/lib/rectification-agentic/core/types.ts";
import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { buildSkillVerificationPacket } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts";
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
const TIMES = ["05:00", "05:10"] as const;
function dated(
id: string,
domain: string,
eventKind: string,
occurredFrom: string,
) {
return {
id,
status: "confirmed" as const,
domain,
datePrecision: "month" as const,
occurredFrom,
occurredTo: null,
eventKind,
};
}
const LEDGER = [
dated("e-edu", "education", "education_start", "2016-09-01"),
dated("e-rel", "relationship", "relationship_start", "2024-05-01"),
dated("e-family", "family", "family_event", "2016-05-01"),
dated("e-reloc", "relocation", "home_change", "2022-10-01"),
];
const D9_STYLE: CandidateDiscriminatorProbe = {
probeId: "contrast:varga.d9.巨蟹座/狮子座",
candidateSetVersion: "05:00-05:10",
question: "亲密关系里更接近下面哪一种相处方式?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:10"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:10"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d9.巨蟹座/狮子座",
informationGain: 1.4,
sourceFeatures: [{ technique: "D9", calculationResultId: null }],
domain: "relationship",
year: null,
semanticKey: "varga.d9.巨蟹座/狮子座",
choiceKind: "varga_style",
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answerClass: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" },
],
};
const CAREER_2023_05: DiscriminatingEventProbe = {
year: 2023,
year_label: "2023 年 5 月前后",
month: 5,
domain: "career",
event_family: "入职或职责变化",
source: "dasha_boundary",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "2023 年 5 月前后有没有入职或换工作?",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.9,
semantic_key: "career.2023.05.dasha_boundary",
candidate_split_hash: "career.2023.05",
candidate_ids: [...TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
style_options: [
{ label: "明确发生且时间吻合", answer_class: "yes" },
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
{ label: "明确没有发生", answer_class: "no" },
{ label: "这段记不清楚", answer_class: "unsure" },
],
choice_kind: "existence",
};
function personalityPlan(extra: Partial<Parameters<typeof buildMethodFollowupPlan>[0]> = {}) {
return buildMethodFollowupPlan({
evidence: LEDGER,
sessionOutcome: "discriminate_candidates",
candidatesSeparated: false,
topCandidateTimes: [...TIMES],
contrastPacket: {
candidateSetVersion: "05:00-05:10",
vargaDifferences: [],
probes: [D9_STYLE],
},
...extra,
});
}
function clockProbe(input: {
id: string;
semanticKey: string;
domain: string;
year: number;
source: string;
choiceKind?: ConflictProbe["choice_kind"];
yesSupports: readonly string[];
yesConflicts: readonly string[];
}): ConflictProbe {
return {
id: input.id,
semantic_key: input.semanticKey,
candidate_split_hash: `${input.semanticKey}:${input.yesSupports.join(",")}`,
domain: input.domain,
year: input.year,
question: input.semanticKey,
candidate_ids: [...input.yesSupports, ...input.yesConflicts],
expected_outcomes: [
{ answer_class: "yes", supports: input.yesSupports, conflicts: input.yesConflicts },
{ answer_class: "weak_yes", supports: input.yesConflicts, conflicts: input.yesSupports },
{ answer_class: "no", supports: [], conflicts: [] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.4,
source: input.source,
...(input.choiceKind ? { choice_kind: input.choiceKind } : {}),
};
}
const D9_PROBE = clockProbe({
id: "probe:varga.d9",
semanticKey: "varga.d9.巨蟹座/狮子座",
domain: "relationship",
year: 0,
source: "varga_contrast",
choiceKind: "varga_style",
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
test("SCORE_DELTA stays ±2/±1 and yearless weight is half", () => {
assert.deepEqual(SCORE_DELTA, {
support: 2,
weak_support: 1,
neutral: 0,
weak_conflict: -1,
conflict: -2,
});
assert.equal(PROBE_WEIGHT.dated, 1);
assert.equal(PROBE_WEIGHT.yearless, 0.5);
assert.equal(STRONG_CONFLICT_ELIMINATION_COUNT, 3);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.22");
});
test("D9 answer B moves scores by ±1 and does not count toward elimination", () => {
const applied = applyProbeOutcome(
{ "05:00": 20, "05:10": 20 },
D9_PROBE,
"weak_yes",
);
assert.equal(applied.kind, "tie_break");
assert.equal(applied.deltas["05:10"], 1);
assert.equal(applied.deltas["05:00"], -1);
assert.equal(applied.strong_conflict_counts["05:00"], 0);
assert.equal(applied.strong_conflict_counts["05:10"], 0);
assert.deepEqual(applied.eliminated_ids, []);
});
test("three yearless conflicts never eliminate; three dated conflicts still do", () => {
const yearless = [1, 2, 3].map((index) => clockProbe({
id: `probe:varga.d9.${index}`,
semanticKey: `varga.d9.style.${index}`,
domain: "relationship",
year: 0,
source: "varga_contrast",
choiceKind: "varga_style",
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
}));
let scores: Record<string, number> = { "05:00": 20, "05:10": 20 };
let counts: Record<string, number> = { "05:00": 0, "05:10": 0 };
let eliminated = new Set<string>();
for (const probe of yearless) {
const applied = applyProbeOutcome(scores, probe, "yes", {
eliminatedIds: eliminated,
strongConflictCounts: counts,
});
scores = { ...applied.scores };
counts = { ...applied.strong_conflict_counts };
eliminated = new Set(applied.eliminated_ids);
}
assert.equal(counts["05:10"], 0);
assert.equal(eliminated.has("05:10"), false);
const dated = [1, 2, 3].map((index) => clockProbe({
id: `probe:career.${2019 + index}`,
semanticKey: `career.${2019 + index}.dasha_boundary`,
domain: "career",
year: 2019 + index,
source: "dasha_boundary",
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
}));
for (const probe of dated) {
const applied = applyProbeOutcome(scores, probe, "yes", {
eliminatedIds: eliminated,
strongConflictCounts: counts,
});
scores = { ...applied.scores };
counts = { ...applied.strong_conflict_counts };
eliminated = new Set(applied.eliminated_ids);
assert.equal(applied.kind, "informative");
assert.equal(applied.deltas["05:00"], 2);
assert.equal(applied.deltas["05:10"], -2);
}
assert.ok(counts["05:10"] >= STRONG_CONFLICT_ELIMINATION_COUNT);
assert.equal(eliminated.has("05:10"), true);
});
test("nakshatra_boundary is yearless: ±1 and no conflict count", () => {
const probe = clockProbe({
id: "probe:nakshatra",
semanticKey: "nakshatra.boundary.a/b",
domain: "other",
year: 0,
source: "nakshatra_boundary",
choiceKind: "varga_style",
yesSupports: ["05:00"],
yesConflicts: ["05:10"],
});
const applied = applyProbeOutcome({ "05:00": 20, "05:10": 20 }, probe, "yes");
assert.equal(applied.kind, "tie_break");
assert.equal(applied.deltas["05:00"], 1);
assert.equal(applied.deltas["05:10"], -1);
assert.equal(applied.strong_conflict_counts["05:10"], 0);
});
test("an askable 2023.05 career probe keeps D9 deferred", () => {
const plan = personalityPlan({ eventProbes: [CAREER_2023_05] });
assert.equal(plan.next_followup?.semantic_key, CAREER_2023_05.semantic_key);
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
assert.equal(
plan.dropped_probes.some((item) => (
item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred"
)),
true,
JSON.stringify(plan.dropped_probes),
);
});
test("dated distinguish empty and two unseparated candidates asks D9", () => {
const plan = personalityPlan({ eventProbes: [] });
assert.equal(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
assert.equal(plan.next_followup?.choice_kind, "varga_style");
assert.ok(plan.next_followup?.choice_frame);
assert.equal(
plan.dropped_probes.some((item) => item.reason === "yearless_deferred"),
false,
);
});
test("a sole remaining candidate defers D9 as yearless_deferred", () => {
const plan = personalityPlan({
eventProbes: [],
topCandidateTimes: ["05:00"],
});
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
assert.equal(
plan.dropped_probes.some((item) => (
item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred"
)),
true,
JSON.stringify(plan.dropped_probes),
);
});
test("already-separated candidates do not ask D9", () => {
const plan = personalityPlan({
eventProbes: [],
candidatesSeparated: true,
});
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
assert.notEqual(plan.next_followup?.choice_kind, "varga_style");
});
test("asInferenceState round-trips a varga_style tie_break round", () => {
const before = buildInferenceState({
range_start: "05:00",
range_end: "05:10",
candidates: [
{ id: "05:00", time: "05:00", relative_support: 20 },
{ id: "05:10", time: "05:10", relative_support: 18 },
],
events: [{ id: "e-rel", domain: "relationship", year: 2024, precision: "year" }],
probes: [D9_PROBE],
});
const after = applyAnswerToState(before, D9_PROBE.id, "weak_yes");
assert.equal(after.rounds.at(-1)?.kind, "tie_break");
const loaded = asInferenceState(JSON.parse(JSON.stringify(after)));
assert.ok(loaded);
assert.equal(loaded.rounds.at(-1)?.kind, "tie_break");
assert.equal(loaded.answered_probes.length, 1);
});
test("verification report marks D9/D10 and nakshatra as reference", () => {
const packet = buildSkillVerificationPacket({
representativeTime: "05:00",
widthMinutes: 10,
candidates: [
{ time: "05:00", rank: 1, relativeSupport: 20 },
{ time: "05:10", rank: 2, relativeSupport: 18 },
],
});
assert.match(packet.markdown, /\| D9 \/ D10 类型对照 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/);
assert.match(packet.markdown, /\| 月宿边界 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/);
});