Files
Jyotisha/frontend/tests/rectification-delivery-report-facts.test.ts
T
Jesse_ChenandClaude Opus 5.5 8fc1a5bded fix(rectification): plain-word opening with examples in the stem, plain de-duplicated step receipt (BUG-1049, BUG-1050)
- BUG-1049 (recurrence of BUG-504): the opening stem carries six examples and
  an example answer again and is server-owned on the zero-evidence opening;
  the body is two plain sentences (no 大运/盘面/代表分钟/精确到秒, no year, no
  question). Stem de-dup compares whole sentences / near-equality instead of a
  12-char prefix, which had deleted the body's examples sentence since
  aa7ccb30 (BUG-604) + dd8f35f7 (BUG-648).
- BUG-1050: plain step labels; a finished step label shows once and
  「已完成 N 步」counts shown rows; failed rows read 「…未完成」 from the
  in-progress wording.
- Skill 10.0.30 -> 10.0.31 (OpeningPolicy); 10.0.30 kept as deprecated.
- VOICE / DESIGN / CHANGELOG / BUG_HISTORY / PROGRESS / real-device checklist
  and screenshots.

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

277 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 { 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)
// 原值: "10.0.30" / 新值: "10.0.31" / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.31");
// 原值: "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)
// 原值: /^version: 10\.0\.30$/ / 新值: /^version: 10\.0\.31$/ / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
assert.match(skill, /^version: 10\.0\.31$/m);
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});