211 lines
8.2 KiB
TypeScript
211 lines
8.2 KiB
TypeScript
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 { 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 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,
|
|
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("skill 10.0.19 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");
|
|
assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.19");
|
|
assert.match(skill, /^version: 10\.0\.19$/m);
|
|
assert.match(skill, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
assert.match(comparison, new RegExp(SKILL_SIGN_SENTENCE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
});
|