Files
Jyotisha/frontend/tests/rectification-yearless-probe-downgrade-20260909.test.ts
T
Jesse_ChenandClaude Fable 5.1 dc732825d8 fix(rectification): 有题就接着问,题问完才出卡;引导窗口不再硬贴领域
出卡时机只看题源有没有空:撤回「门槛达标就短路采集线」的写法,同时
按 D2 保住「题源全空就按现行规则出卡」——门槛只在还有题可问时挡住
出卡,precision_gate_met 改成只上报(新挂在决策与公开投影上),不再
单独决定时机。引导窗口题在无领域轨道上改问开放题,一个时间窗只问一
次;录入卡提交的是「YYYY 年 M 月,<领域>方面有一件事」,不再是题干
的三选一列表。记忆化 golden 只补一个新键并冻结墙钟。离线回放改成注
入真值方向的边界事件,另跑一组反方向对照。Skill 10.0.28。

BUG-747~752

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 12:16:55 +00:00

340 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.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: "10.0.27" / 新值: "10.0.28" / 原因: D6 改写出卡句后 Skill bump
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.28");
});
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 \| 性格自评,只作排序参考,不参与淘汰 \|/);
});