Files
Jyotisha/frontend/tests/rectification-yearless-probe-downgrade-20260909.test.ts
T
Jesse_ChenandClaude Opus 5.5 ea21743b09 fix(rectification): stop spoken collect once the training gate opens (BUG-1084..1087)
Once the discriminator training gate is open, only choice cards are asked
and the range card goes out when they are exhausted; targeted lines, their
re-ask and guided windows no longer hold the card or invite more events.
Delivery body says how many choice questions were used instead of the event
fit percent; narration names an excluded cluster instead of "range
unchanged"; a delivered turn no longer carries a collect question.

Offline replay (v4, 3 radii x 2 directions): truth in range 20/20 in every
cell; guided-window injections give the same width in truth and opposite
directions, so red line 1 was revised by product to truth-in-range only.
Skill 10.0.31 -> 10.0.32 (10.0.31 kept as deprecated for pinned cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-29 10:06:57 +08:00

343 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.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
// 原值: "10.0.30" / 新值: "10.0.31" / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
// 原值: "10.0.31" / 新值: "10.0.32" / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.32");
});
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_style,dropped 不含 yearless_deferred
// 新值: D9 记 yearless_deferred,next 不再是性格卡
// 原因: 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 \| 性格自评,只作排序参考,不参与淘汰 \|/);
});