Files
Jyotisha/frontend/tests/rectification-delivery-report-facts.test.ts
T
66f087b588 feat(rectification): submit varga-resolution implementation for review
Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 08:04:04 +08:00

281 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)
// 原值: "10.0.31" / 新值: "10.0.32" / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
// 原值: "10.0.32" / 新值: "10.0.33" / 原因: 新案绑定持久盘型交付 Skill,历史精确身份不升级(BUG-1115~1117)
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.33");
// 原值: "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)
// 原值: /^version: 10\.0\.31$/ / 新值: /^version: 10\.0\.32$/ / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
// 原值: /^version: 10\.0\.32$/ / 新值: /^version: 10\.0\.33$/ / 原因: live Skill新增条件盘型口径,原历史分钟合同保留(BUG-1115~1117)
assert.match(skill, /^version: 10\.0\.33$/m);
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});