Files
Jyotisha/frontend/tests/rectification-delivery-report-facts.test.ts
T
Jesse_ChenandClaude Opus 5.5 4e6e8d87ef feat(rectification): 少问几道就出卡,交付卡以范围为主(D1–D4)
- D1 引导窗口题每个校正最多 2 道(前端 GUIDED_WINDOW_CASE_LIMIT;引擎
  GUIDED_COLLECT_LIMIT 未动:event_probes.py 属冻结评分身份,改它需重新冻结)
- D2 七条定向线与跳过线重问问完即出卡,没问到的引导窗口不再挡卡,出卡后也不再挂窗口题
- D3 卡头加副标题「最可能 HH:MM」
- D4 前两列相差 ≥5 个百分点才显示相对可能性,否则一句「这几个时刻目前区分不开……」
- 离线回放 scripts/research/fewer_probes_card_replay.py:真值不降、宽度中位 ±1 分钟、提问 11.4→7.8
- Skill 10.0.29 → 10.0.30;DESIGN / VOICE / CHANGELOG / PROGRESS / 真机清单

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 14:27:12 +08:00

275 lines
11 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 { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts";
import { signFromTransitions } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { dashaAgreementAmongActive } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import {
OPEN_ENGINE_CAPABILITY_CEILING,
CANDIDATE_ID,
RESULT_ID,
SECOND_CANDIDATE_ID,
} from "./rectification-v9-test-support.ts";
const ELIMINATED_059_ID = "88888888-8888-4888-8888-888888888884";
const ELIMINATED_506_ID = "88888888-8888-4888-8888-888888888885";
const SKILL_SIGN_SENTENCE = "分盘上升只抄 `skill_verification_report.sign_by_candidate`,不得自行按换升时刻推算";
const ENGINE_CANDIDATES = [
{ candidateId: CANDIDATE_ID, time: "04:51", rank: 1, relativeSupport: 38, tiedMinuteCount: 29 },
{ candidateId: SECOND_CANDIDATE_ID, time: "04:53", rank: 2, relativeSupport: 36, tiedMinuteCount: 29 },
{ candidateId: ELIMINATED_059_ID, time: "04:59", rank: 3, relativeSupport: 14, tiedMinuteCount: 29 },
{ candidateId: ELIMINATED_506_ID, time: "05:06", rank: 4, relativeSupport: 12, tiedMinuteCount: 29 },
] as const;
function accidentInferenceState() {
const times = ENGINE_CANDIDATES.map((item) => item.time);
return {
algorithm_version: INFERENCE_ALGORITHM_VERSION,
candidate_set_id: candidateSetId("04:45", "05:13", times),
revision: 1,
phase: "discrimination" as const,
result_status: "credible_range" as const,
range_start: "04:45",
range_end: "05:13",
candidates: [
{
id: CANDIDATE_ID,
time: "04:51",
cluster_range: ["04:51", "04:53"] as const,
prior_score: 38,
posterior_score: 48,
probability: 0.4,
status: "active" as const,
rank: 2,
strong_conflict_count: 0,
},
{
id: SECOND_CANDIDATE_ID,
time: "04:53",
cluster_range: ["04:51", "04:53"] as const,
prior_score: 36,
posterior_score: 52,
probability: 0.52,
status: "active" as const,
rank: 1,
strong_conflict_count: 0,
},
{
id: ELIMINATED_059_ID,
time: "04:59",
cluster_range: ["04:59", "04:59"] as const,
prior_score: 14,
posterior_score: 8,
probability: 0.04,
status: "eliminated" as const,
rank: 3,
strong_conflict_count: 3,
},
{
id: ELIMINATED_506_ID,
time: "05:06",
cluster_range: ["05:06", "05:06"] as const,
prior_score: 12,
posterior_score: 6,
probability: 0.04,
status: "eliminated" as const,
rank: 4,
strong_conflict_count: 3,
},
],
events: [],
probes: [],
answered_probes: [],
rounds: [],
entropy: 1.1,
representative_time: "04:53",
credible_range: ["04:51", "04:53"] as const,
};
}
function twelveHouses(sign: string) {
return Array.from({ length: 12 }, (_, index) => ({
house: index + 1,
sign,
occupants: [] as string[],
}));
}
function accidentProjection() {
return latestResultToolProjection({
resultId: RESULT_ID,
candidates: ENGINE_CANDIDATES,
selectionAllowed: true,
confirmationAllowed: false,
representativeTime: "05:06",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
decisionReceipt: {
acceptance_allowed: true,
selection_allowed: true,
propose_allowed: true,
confirmation_allowed: false,
house_table: { time: "05:06", lagna: "狮子座", houses: twelveHouses("狮子座") },
house_tables_by_time: {
"04:53": { time: "04:53", lagna: "巨蟹座", houses: twelveHouses("巨蟹座") },
"05:06": { time: "05:06", lagna: "狮子座", houses: twelveHouses("狮子座") },
},
natal_recast: {
time: "05:06",
lagna: "狮子座",
user_meaning: "本命宫位已按 05:06 重算(上升 狮子座)。",
unique_minute_claim: false,
confirmation_allowed: false,
},
inference_state: accidentInferenceState(),
dasha_agreement: {
status: "conflict",
vimshottari_top: "05:06",
narayana_top: "04:59",
user_meaning: "主限更偏向 05:06,分盘大运更偏向 04:59。冲突时不能按更高把握收口。",
},
window_scan: {
scanned: true,
d9_lagna_count: 1,
d10_lagna_count: 2,
d9_candidates_differ: false,
d10_candidates_differ: true,
d9_sign_names: ["天秤座"],
d10_sign_names: ["巨蟹座", "狮子座"],
transitions: [
{
layer: "d10",
at: "05:00",
from_sign: "巨蟹座",
to_sign: "狮子座",
user_meaning: "D10 在 05:00 发生变化",
},
],
},
},
}, decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: ENGINE_CANDIDATES.map((item) => ({ time: item.time, score: item.relativeSupport })),
}));
}
function reportPacket(value: unknown): {
width_minutes: number | null;
dasha_agreement: { status: string; vimshottari_top: string | null; narayana_top: string | null; user_meaning: string } | null;
sign_by_candidate: Record<string, { d9: string | null; d10: string | null }>;
markdown: string;
} {
assert.ok(value && typeof value === "object" && !Array.isArray(value));
const row = value as Record<string, unknown>;
assert.equal(typeof row.markdown, "string");
assert.ok(row.sign_by_candidate && typeof row.sign_by_candidate === "object");
return row as ReturnType<typeof reportPacket>;
}
test("D10 still Cancer at 04:53 when the scan only flips to Leo at 05:00", () => {
const transitions = [
{ layer: "d10" as const, at: "05:00", from_sign: "巨蟹座", to_sign: "狮子座", user_meaning: "D10 在 05:00 发生变化" },
];
assert.equal(signFromTransitions(transitions, "d10", "04:51"), "巨蟹座");
assert.equal(signFromTransitions(transitions, "d10", "04:53"), "巨蟹座");
assert.equal(signFromTransitions(transitions, "d10", "05:00"), "狮子座");
});
test("delivery report uses the inference window and active dasha tops, not the pre-inference engine span", () => {
const projection = accidentProjection();
const report = reportPacket(projection.skill_verification_report);
assert.equal(report.width_minutes, 3);
assert.equal(projection.engine_indistinguishable_width_minutes, 29);
assert.equal(projection.indistinguishable_width_minutes, undefined);
const dasha = report.dasha_agreement;
assert.ok(dasha);
const tops = [dasha.vimshottari_top, dasha.narayana_top].filter(Boolean);
for (const time of tops) {
assert.ok(time === "04:51" || time === "04:53", time ?? "");
}
assert.notEqual(dasha.status, "conflict");
assert.doesNotMatch(report.markdown, /29 分钟/);
assert.doesNotMatch(report.markdown, /05:06/);
assert.doesNotMatch(report.markdown, /04:59/);
assert.match(report.markdown, /3 分钟/);
assert.match(report.markdown, /不是已确认的唯一出生分钟/);
const pre = projection.dasha_agreement_pre_inference as { vimshottari_top?: string; narayana_top?: string } | null;
assert.equal(pre?.vimshottari_top, "05:06");
assert.equal(pre?.narayana_top, "04:59");
const live = projection.dasha_agreement as { vimshottari_top?: string | null; narayana_top?: string | null };
assert.notEqual(live.vimshottari_top, "05:06");
assert.notEqual(live.narayana_top, "04:59");
});
test("delivery report gives 04:53 D10 as Cancer instead of letting the model infer Leo", () => {
const projection = accidentProjection();
const report = reportPacket(projection.skill_verification_report);
assert.equal(report.sign_by_candidate["04:53"]?.d10, "巨蟹座");
assert.equal(report.sign_by_candidate["04:51"]?.d10, "巨蟹座");
assert.doesNotMatch(report.markdown, /04:53[^\n]*狮子座/);
assert.match(report.markdown, /04:53 \| .*巨蟹座/);
});
test("same active cluster dasha tops agree even when the peak is not a representative", () => {
const engine = {
status: "conflict" as const,
vimshottari_top: "04:54",
narayana_top: "04:53",
user_meaning: "主限更偏向 04:54,分盘大运更偏向 04:53。冲突时不能按更高把握收口。",
};
const clusters = [{ time: "04:53", cluster_range: ["04:50", "04:55"] as const }];
const clustered = dashaAgreementAmongActive(engine, ["04:53"], clusters);
assert.equal(clustered?.status, "agree");
const minuteOnly = dashaAgreementAmongActive(engine, ["04:53"]);
assert.equal(minuteOnly?.status, "partial");
});
test("public house table follows the inference representative minute", () => {
const projection = accidentProjection();
assert.equal(projection.representative_time, "04:53");
assert.equal((projection.house_table as { time?: string } | undefined)?.time, "04:53");
const recast = projection.natal_recast as { time?: string; user_meaning?: string } | undefined;
assert.equal(recast?.time, "04:53");
assert.match(String(recast?.user_meaning), /04:53/);
assert.equal(projection.representative_time_pre_inference, "05:06");
assert.equal((projection.house_table_pre_inference as { time?: string } | undefined)?.time, "05:06");
const source = readFileSync(fileURLToPath(new URL("../src/mastra/rectification-v9-tools.ts", import.meta.url)), "utf8");
assert.match(source, /representative_time_pre_inference: _preTime/);
assert.match(source, /house_table_pre_inference: _preHouse/);
assert.match(source, /natal_recast_pre_inference: _preNatal/);
});
test("skill 10.0.26 forbids computing varga signs from transition times", () => {
const skillDir = fileURLToPath(new URL("../../skills/jyotish-birth-time-rectification", import.meta.url));
const skill = readFileSync(`${skillDir}/SKILL.md`, "utf8");
const comparison = readFileSync(`${skillDir}/references/candidate-comparison.md`, "utf8");
// 原值: "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)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.30");
// 原值: "10.0.25"
// 原值: "10.0.26"
// 新值: "10.0.27"
// 原因: 出卡精度门槛 + 引导式补经历
// 原因: BUG-668 定向补事第四选项写进 Skill
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
assert.match(skill, /^version: 10\.0\.30$/m);
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});