Files
Jyotisha/frontend/tests/rectification-yearless-probe-downgrade-20260909.test.ts
T
Jesse_ChenandCursor 6c9a089620
Independent Staging Quality Gate / validate (push) Successful in 13m54s
Independent Staging Quality Gate / publish (push) Successful in 10m50s
fix(rectification): refresh remaining probes and targeted collect before delivering range (BUG-653/654)
Dated-choice exhaustion is not convergence. Refresh probes from remaining
active candidates, then ask a targeted collect, then deliver. Skill 10.0.24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 18:28:39 +08:00

334 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);
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.24");
});
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 \| 性格自评,只作排序参考,不参与淘汰 \|/);
});