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; markdown: string; } { assert.ok(value && typeof value === "object" && !Array.isArray(value)); const row = value as Record; assert.equal(typeof row.markdown, "string"); assert.ok(row.sign_by_candidate && typeof row.sign_by_candidate === "object"); return row as ReturnType; } 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, "\\$&"))); });