import assert from "node:assert/strict"; import test from "node:test"; import { buildPersonalReportSectionPlan, validatePersonalReportSectionPlan, } from "../src/lib/personal-report-plan.ts"; import { finalizeReportEvidenceBundleV2, type ReportEvidenceBundleV2, } from "../src/lib/report-evidence-bundle-v2.ts"; function bundleFixture(): ReportEvidenceBundleV2 { return finalizeReportEvidenceBundleV2({ schemaVersion: "report_evidence_bundle.v2", subject: { displayName: "测试用户", birthTimeStatus: "accepted", birthPlaceLabel: "北京" }, requestedThemes: ["career", "wealth", "timing"], reportType: "personal_full", presentationMode: "default", calculationProfile: { calculationHash: "a".repeat(64), calculationHashDerived: false, birthTimeStatus: "accepted", ayanamsa: "Lahiri", nodeMode: "mean", houseSystem: "whole_sign", vimshottari: null, narayana: null, }, skill: { name: "jyotish-personal-report", version: "1.0.0", sha256: "b".repeat(64), sourceCommit: null }, charts: [{ id: "D1", title: "本命盘 D1", ascendant: { sign: "Leo", degree: 20.5 }, houses: Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: "Leo", signDerived: false, occupants: [] })), planets: [], }], claimCards: [{ id: "ev-claim-career", theme: "career", section: "事业", conclusion: "事业结论", supportingFacts: [{ id: "ev-fact-career", label: "D10", value: "已执行", evidenceRef: "ev-tech-d10", status: "verified" }], counterFacts: [], executedTechniqueRefs: ["ev-tech-d10"], assertionLevel: "single_system_inference", timingBoundary: null, verificationQuestions: [], }], blockedSections: [ { id: "ev-blocked-wealth", theme: "wealth", section: "财富", reason: "缺少 D2", missingTechniqueRefs: ["ev-tech-d2"] }, { id: "ev-blocked-timing", theme: "timing", section: "应期", reason: "时间证据不足", missingTechniqueRefs: ["ev-tech-vimshottari"] }, ], conflicts: [], executionLedger: [ { id: "ev-tech-d10", technique: "D10", status: "verified", executed: true, note: "已执行" }, { id: "ev-tech-d2", technique: "D2", status: "blocked", executed: false, note: "未提供" }, { id: "ev-tech-vimshottari", technique: "Vimshottari", status: "blocked", executed: false, note: "未提供" }, ], evidenceRefs: [ { id: "ev-tech-d10", technique: "D10", status: "verified" }, { id: "ev-tech-d2", technique: "D2", status: "blocked" }, { id: "ev-tech-vimshottari", technique: "Vimshottari", status: "blocked" }, ], answerPolicy: { canAnswerPreciseTiming: false, birthTimePolicy: "accepted_directional_only", deterministicClaimsForbiddenFor: ["timing"], }, }); } test("section plan covers every requested theme exactly once and preserves blocked themes", () => { const bundle = bundleFixture(); const plan = buildPersonalReportSectionPlan(bundle, "standard"); const thematic = plan.sections.filter((section) => section.kind === "thematic"); assert.deepEqual(thematic.map((section) => section.theme), bundle.requestedThemes); assert.equal(thematic.find((section) => section.theme === "career")?.disposition, "write"); assert.equal(thematic.find((section) => section.theme === "wealth")?.disposition, "blocked"); assert.equal(thematic.find((section) => section.theme === "timing")?.disposition, "blocked"); assert.equal(plan.sections.find((section) => section.kind === "current_phase")?.disposition, "blocked"); }); test("four report depths use bounded configurable ranges instead of one-character completeness", () => { const bundle = bundleFixture(); const depths = ["concise", "standard", "deep", "research"] as const; const minimums = depths.map((depth) => { const plan = buildPersonalReportSectionPlan(bundle, depth); return plan.sections.find((section) => section.kind === "thematic")!.targetCharacters.min; }); assert.deepEqual(minimums, [120, 260, 480, 600]); assert.ok(minimums.every((minimum) => minimum > 1)); }); test("plan validation rejects omitted themes, wrong blocked disposition and dangling refs", () => { const bundle = bundleFixture(); const plan = buildPersonalReportSectionPlan(bundle, "standard"); assert.throws(() => validatePersonalReportSectionPlan({ ...plan, sections: plan.sections.filter((section) => section.theme !== "wealth"), }, bundle), /report_plan_theme_coverage_invalid:wealth/); assert.throws(() => validatePersonalReportSectionPlan({ ...plan, sections: plan.sections.map((section) => section.theme === "wealth" ? { ...section, disposition: "write" } : section), }, bundle), /report_plan_theme_disposition_invalid:wealth/); assert.throws(() => validatePersonalReportSectionPlan({ ...plan, sections: plan.sections.map((section) => section.theme === "career" ? { ...section, evidenceRefs: ["ev-missing"] } : section), }, bundle), /report_plan_dangling_evidence_ref:ev-missing/); });