Files
Jyotisha/frontend/tests/rectification-yearless-probe-downgrade-20260909.test.ts
T
Jesse_ChenandCursor 530f260f76
Independent Staging Quality Gate / validate (push) Successful in 12m9s
Independent Staging Quality Gate / publish (push) Successful in 2m17s
fix(rectification): ask remaining split lines one card at a time (BUG-661~663)
Dated-pool empty now yields existence cards per remaining layer, leftover-candidate copy, and an optional D9/D10 tie-break on the range card. Skill 10.0.25.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 16:50:25 +08:00

337 lines
12 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 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);
// 原值: "10.0.24"
// 新值: "10.0.25"
// 原因: BUG-661/662/663 定向补事改逐条点选,Skill 升版
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.25");
});
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 defers D9", () => {
const plan = personalityPlan({ eventProbes: [] });
// 原值: next_followup 是 D9 varga_styledropped 不含 yearless_deferred
// 新值: D9 记 yearless_deferrednext 不再是性格卡
// 原因: BUG-651 带年月池空即交付,性格题不得挡在结果前面
assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey);
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("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 \| 性格自评,只作排序参考,不参与淘汰 \|/);
});