import assert from "node:assert/strict"; import test from "node:test"; import { safeParseServerReportDocument, computeEvidenceHash } from "../src/lib/personal-report-contract.server-core.ts"; import { computeReportEvidenceBundleHash, finalizeReportEvidenceBundleV2, validateReportEvidenceBundleV2, type ReportEvidenceBundleV2, } from "../src/lib/report-evidence-bundle-v2.ts"; import { REPORT_STABLE_CODES, ReportEvidenceInsufficientError, applyReportGuard, assembleReportDocument, buildReportEvidenceBundleV2, buildReportEvidencePacket, canonicalSerialize, computeRequestFingerprint, findForbiddenDeterministicClaims, generatePersonalReport, redactDeterministicSentences, resolveSkillSnapshot, sha256Hex, type SkillSnapshot, } from "../src/lib/personal-report-generation.ts"; import { PRECISE_TIMING_PATTERNS } from "../src/lib/personal-report-generation.ts"; import type { PersonalReportAgentOutput, ReportAgentPort, ReportEvidencePacket, } from "../src/mastra/personal-report.ts"; // --------------------------------------------------------------------------- // Fixtures: a real-shaped workflow response matching the Python main chain // (object-map planets/houses/sections, varga_full D9_Navamsa/D10_Dasamsa) // --------------------------------------------------------------------------- function pythonStyleChartPayload() { const signNames = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]; const ascIndex = 4; // Leo const planetsMap: Record> = { Sun: { sign: "Leo", degree: 142.5, degree_raw: 142.5, degree_in_sign: 22.5, house: 1, retrograde: false, speed: 1.0 }, Moon: { sign: "Virgo", degree: 172.5, degree_raw: 172.5, degree_in_sign: 22.5, house: 2, retrograde: false, speed: 13.0 }, Mars: { sign: "Libra", degree: 202.5, degree_raw: 202.5, degree_in_sign: 22.5, house: 3, retrograde: false, speed: 0.6 }, Mercury: { sign: "Scorpio", degree: 232.5, degree_raw: 232.5, degree_in_sign: 22.5, house: 4, retrograde: true, speed: -0.5 }, Jupiter: { sign: "Sagittarius", degree: 262.5, degree_raw: 262.5, degree_in_sign: 22.5, house: 5, retrograde: false, speed: 0.2 }, Venus: { sign: "Capricorn", degree: 292.5, degree_raw: 292.5, degree_in_sign: 22.5, house: 6, retrograde: false, speed: 1.1 }, Saturn: { sign: "Aquarius", degree: 322.5, degree_raw: 322.5, degree_in_sign: 22.5, house: 7, retrograde: true, speed: -0.1 }, Rahu: { sign: "Pisces", degree: 352.5, degree_raw: 352.5, degree_in_sign: 22.5, house: 8, retrograde: true, speed: -0.05 }, Ketu: { sign: "Virgo", degree: 172.5, degree_raw: 172.5, degree_in_sign: 22.5, house: 2, retrograde: true, speed: -0.05 }, }; const housesMap: Record> = {}; for (let index = 0; index < 12; index += 1) { housesMap[`house_${index + 1}`] = { cusp_sign: signNames[(ascIndex + index) % 12], cusp_degree: 140.5 + index * 30, lord: signNames[(ascIndex + index) % 12], }; } const d9Planets: Record> = { Sun: { sign: "Leo", sign_idx: 4 }, Moon: { sign: "Virgo", sign_idx: 5 }, Mars: { sign: "Cancer", sign_idx: 3 }, }; const d10Planets: Record> = { Sun: { sign: "Taurus", sign_idx: 1 }, Moon: { sign: "Gemini", sign_idx: 2 }, }; return { success: true, chart: { ascendant: { sign: "Leo", degree: 20.5, degree_raw: 140.5, lon: 140.5, sign_cn: "狮子座", lord: "Sun" }, planets: planetsMap, houses: housesMap, dasha: { mahadashas: [ { lord: "Moon", start: "2019-01-01", end: "2029-01-01" }, { lord: "Mars", start: "2029-01-01", end: "2036-01-01" }, ], }, modules: { varga_full: { D9_Navamsa: { _meta: { div: 9 }, Ascendant: { sign: "Leo", sign_idx: 4 }, ...d9Planets, _dignity: {}, }, D10_Dasamsa: { _meta: { div: 10 }, Ascendant: { sign: "Taurus", sign_idx: 1 }, ...d10Planets, }, }, narayana_dasha: { periods: [{ lord: "Sun", start: "2023-01-01", end: "2026-01-01" }], }, }, }, consumer_context: { route: "general", core_status: "ready", available_layers: ["D1", "D9", "D10", "Vimshottari", "Narayana"], missing_route_layers: [], hard_blockers: [], answer_policy: { can_answer_precise_timing: true, deterministic_claims_forbidden_for: [], }, }, machine_evidence_packet: { conflicts: [ { techniques: ["Vimshottari", "Narayana"], summary: "两个大运系统给出的阶段边界不一致" }, ], sections: { D1: { status: "used", source_path: "chart.planets+chart.ascendant" }, D9: { status: "used", source_path: "modules.varga_full.D9" }, D10: { status: "used", source_path: "modules.varga_full.D10" }, D2: { status: "missing", source_path: "modules.varga_full.D2" }, planet_degrees: { status: "used", source_path: "chart.planets" }, house_degrees: { status: "used", source_path: "chart.houses" }, dasha_boundaries: { status: "used", source_path: "modules.dasha" }, narayana_dasha: { status: "used", source_path: "modules.narayana_dasha" }, external_oracle_status: { status: "used", source_path: "vedastro_official.runtime_truth" }, vedastro_official_raw_response: { status: "missing", source_path: "vedastro_official.raw_response" }, }, }, }; } function buildPacket(overrides: Partial = {}): ReportEvidencePacket { const workflow = overrides.workflow ?? pythonStyleChartPayload(); return buildReportEvidencePacket({ workflow, subject: { displayName: "测试用户", birthTimeStatus: "confirmed", birthPlaceLabel: "北京", }, requestedThemes: ["career", "marriage", "wealth", "timing"], reportType: "personal_full", presentationMode: "default", candidateRange: null, skillSnapshot: { name: "jyotish-vedic-astrology", version: "6.9.14", sha256: "a".repeat(64), sourceCommit: "b".repeat(40), }, }); } type BuildPacketOverrides = { workflow: unknown; }; function agentOutput(overrides: Partial = {}): PersonalReportAgentOutput { return { executiveSummary: { headline: "综合盘面以事业发展为主线", summary: "命盘显示事业层面具备稳定的结构,财富与婚恋需结合分盘审慎解读。", priorities: ["先聚焦职业方向", "再核对感情与财富主题"], }, thematicNarrative: [ { id: "career", theme: "career", title: "事业", narrative: "事业层面以十宫与 D10 结构为主,方向性判断稳定,具体应期需要结合大运边界观察。", actions: ["在稳定领域深耕"], caveats: ["该部分为方向性描述"], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-1", "ev-audit-3"], }, { id: "marriage", theme: "marriage", title: "婚恋", narrative: "婚恋部分以七宫与 D9 为主,呈现结构特征,不构成确定性结论。", actions: [], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-2"], }, ], ...overrides, }; } // --------------------------------------------------------------------------- // Packet builder: allowlist + fail-closed // --------------------------------------------------------------------------- test("packet builder extracts only allowlisted facts, never internal noise", () => { const workflow = pythonStyleChartPayload(); (workflow as Record).internal_path = "/opt/app/private/engine.py"; (workflow as Record).prompt_text = "系统提示词原文"; (workflow as Record).traceback = "Traceback (most recent call last)"; (workflow as Record).raw_payload = { anything: true }; const packet = buildPacket({ workflow }); assert.equal(packet.skillName, "jyotish-vedic-astrology"); assert.equal(packet.skillVersion, "6.9.14"); assert.equal(packet.skillSnapshotSha256, "a".repeat(64)); assert.equal(packet.skillSourceCommit, "b".repeat(40)); const serialized = JSON.stringify(packet); assert.doesNotMatch(serialized, /internal_path|prompt_text|traceback|raw_payload|\/opt\/app/); assert.match(serialized, /ev-audit-/); assert.match(serialized, /ev-conflict-/); }); test("packet builder computes a canonical 64-hex calculation hash and marks derivation", () => { const packet = buildPacket(); assert.match(packet.chart.calculationHash, /^[0-9a-f]{64}$/); assert.equal(packet.chart.calculationHashDerived, true); }); test("packet builder keeps a real engine hash when present", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record; chart.result_hash = "c".repeat(64); const packet = buildPacket({ workflow }); assert.equal(packet.chart.calculationHash, "c".repeat(64)); assert.equal(packet.chart.calculationHashDerived, false); }); test("packet builder fails closed without real D1 houses", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record; const houses = chart.houses as Record; delete houses["house_9"]; delete houses["house_10"]; delete houses["house_11"]; delete houses["house_12"]; assert.throws(() => buildPacket({ workflow }), ReportEvidenceInsufficientError); }); test("packet builder fails closed without retrograde facts", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record; const planets = chart.planets as Record>; planets.Sun.retrograde = undefined; assert.throws(() => buildPacket({ workflow }), ReportEvidenceInsufficientError); }); test("packet builder fails closed without ascendant or evidence refs", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record; delete chart.ascendant; assert.throws(() => buildPacket({ workflow }), ReportEvidenceInsufficientError); const workflow2 = pythonStyleChartPayload() as Record; const consumer = workflow2.consumer_context as Record; consumer.available_layers = []; const machine = workflow2.machine_evidence_packet as Record; machine.sections = {}; machine.conflicts = []; assert.throws(() => buildPacket({ workflow: workflow2 }), ReportEvidenceInsufficientError); }); test("no mock fallback: an empty workflow never yields a usable packet", () => { assert.throws(() => buildPacket({ workflow: {} }), ReportEvidenceInsufficientError); }); test("packet builder normalizes the real Python object-map shapes", () => { const packet = buildPacket(); // planets object map -> facts, absolute longitude from degree/degree_raw. assert.equal(packet.chart.planets.length, 9); const sun = packet.chart.planets.find((planet) => planet.id === "Sun"); assert.ok(sun); assert.equal(sun.sign, "Leo"); assert.equal(sun.degree, 142.5); assert.equal(sun.house, 1); assert.equal(sun.retrograde, false); assert.equal(packet.chart.planets.find((planet) => planet.id === "Saturn")?.retrograde, true); // houses object map (cusp_sign only) -> whole-sign derived signs, marked. assert.equal(packet.chart.houses.length, 12); assert.equal(packet.chart.houses[0].sign, "Leo"); assert.equal(packet.chart.houses[0].signDerived, true); assert.deepEqual(packet.chart.houses[0].occupants, ["Sun"]); assert.equal(packet.chart.houses[1].sign, "Virgo"); assert.deepEqual([...packet.chart.houses[1].occupants].sort(), ["Ketu", "Moon"]); // sections object map -> deterministic statuses: core calculation sections // verified, internal layers partial, external/missing degraded. const refs = packet.evidenceRefs; const verified = refs.filter((ref) => ref.status === "verified").map((ref) => ref.technique); assert.ok(verified.includes("planet_degrees")); assert.ok(verified.includes("house_degrees")); const blocked = refs.filter((ref) => ref.status === "blocked").map((ref) => ref.technique); assert.ok(blocked.includes("D2")); assert.ok(blocked.includes("vedastro_official_raw_response")); const partial = refs.filter((ref) => ref.status === "partial").map((ref) => ref.technique); assert.ok(partial.includes("dasha_boundaries")); assert.ok(partial.includes("external_oracle_status")); // conflicts produce canonical ev-conflict refs. assert.ok(refs.some((ref) => ref.id.startsWith("ev-conflict-"))); }); test("packet builder resolves the base chart from modules.chart and nested chart", () => { const base = pythonStyleChartPayload() as Record; const chartData = base.chart as Record; // modules.chart wins over nested chart over top level. const modulesChart = { ...chartData, planets: { Sun: { sign: "Aries", degree: 10.5, degree_raw: 10.5, house: 1, retrograde: false } }, }; const modules = chartData.modules as Record; modules.chart = modulesChart; const viaModules = buildPacket({ workflow: base }); assert.equal(viaModules.chart.planets.length, 1); assert.equal(viaModules.chart.planets[0].sign, "Aries"); delete modules.chart; // nested chart_data.chart is the orchestrator's second choice. const nested = { ...chartData, planets: { Moon: { sign: "Pisces", degree: 350.5, degree_raw: 350.5, house: 12, retrograde: false } }, }; chartData.chart = nested; const viaNested = buildPacket({ workflow: base }); assert.equal(viaNested.chart.planets.length, 1); assert.equal(viaNested.chart.planets[0].id, "Moon"); }); test("varga houses are whole-sign derived from the divisional ascendant, never fabricated", () => { const packet = buildPacket(); const d9 = packet.chart.vargaHouses.find((varga) => varga.id === "D9"); assert.ok(d9); assert.equal(d9.houses.length, 12); // D9 ascendant is Leo (index 4): house 1 Leo, house 2 Virgo. assert.equal(d9.houses[0].sign, "Leo"); assert.equal(d9.houses[1].sign, "Virgo"); // Moon sits in Virgo (index 5) -> whole-sign house 2 of D9. assert.ok(d9.houses[1].occupants.includes("Moon")); assert.ok(d9.houses.every((house) => house.signDerived === true)); const d10 = packet.chart.vargaHouses.find((varga) => varga.id === "D10"); assert.ok(d10); // D10 ascendant is Taurus (index 1): house 1 Taurus, house 2 Gemini. assert.equal(d10.houses[0].sign, "Taurus"); assert.equal(d10.houses[1].sign, "Gemini"); assert.ok(d10.houses[1].occupants.includes("Moon")); }); test("array-shaped chart data remains supported", () => { const workflow = { success: true, chart: { ascendant: { sign: "Leo", degree: 12.5 }, planets: [ { id: "Sun", sign: "Leo", degree: 142.5, house: 1, retrograde: false }, { id: "Moon", sign: "Virgo", degree: 172.5, house: 2, retrograde: false }, ], houses: Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"][index], })), }, consumer_context: { route: "general", core_status: "ready", available_layers: ["D1"], missing_route_layers: [], hard_blockers: [], answer_policy: { can_answer_precise_timing: true, deterministic_claims_forbidden_for: [] }, }, machine_evidence_packet: { conflicts: [], sections: [{ name: "planet_degrees", status: "verified", note: "" }], }, }; const packet = buildPacket({ workflow }); assert.equal(packet.chart.planets.length, 2); assert.equal(packet.chart.houses.length, 12); // Array houses carry a real sign: not derived. assert.equal(packet.chart.houses[0].signDerived, false); assert.equal(packet.chart.houses[0].sign, "Aries"); assert.ok(packet.evidenceRefs.some((ref) => ref.status === "verified")); }); test("section status mapping is deterministic (used core sections verified, external degraded)", () => { const packet = buildPacket(); const byTechnique = new Map(packet.evidenceRefs.map((ref) => [ref.technique, ref.status])); assert.equal(byTechnique.get("D1"), "verified"); assert.equal(byTechnique.get("planet_degrees"), "verified"); assert.equal(byTechnique.get("house_degrees"), "verified"); assert.equal(byTechnique.get("dasha_boundaries"), "partial"); assert.equal(byTechnique.get("external_oracle_status"), "partial"); assert.equal(byTechnique.get("D2"), "blocked"); assert.equal(byTechnique.get("vedastro_official_raw_response"), "blocked"); }); // --------------------------------------------------------------------------- // Fingerprint // --------------------------------------------------------------------------- test("request fingerprint is canonical: sorted, deduped themes, no requestId", () => { const base = { reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["wealth", "career", "wealth", "timing"], sessionId: null, chartProfileId: null, }; const fingerprintA = computeRequestFingerprint(base); const fingerprintB = computeRequestFingerprint({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career", "timing", "wealth"], sessionId: null, chartProfileId: null, }); assert.equal(fingerprintA, fingerprintB); assert.equal(fingerprintA, sha256Hex(canonicalSerialize({ reportType: "personal_full", presentationMode: "default", depth: "standard", themes: ["career", "timing", "wealth"], sessionId: null, chartProfileId: null, }))); const differentType = computeRequestFingerprint({ ...base, reportType: "personal_thematic", }); assert.notEqual(fingerprintA, differentType); const withSession = computeRequestFingerprint({ ...base, sessionId: "11111111-1111-4111-8111-111111111111", }); assert.notEqual(fingerprintA, withSession); }); // --------------------------------------------------------------------------- // Assembly: canonical contract shape // --------------------------------------------------------------------------- test("assembled document passes the canonical server parse with a D1 of 12 houses", () => { const packet = buildPacket(); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput(), }); const parsed = safeParseServerReportDocument(document); assert.equal(parsed.ok, true); if (!parsed.ok) return; assert.equal(parsed.document.charts.length, 3); assert.equal(parsed.document.provenance.skillName, "jyotish-vedic-astrology"); assert.equal(parsed.document.provenance.skillVersion, "6.9.14"); assert.equal(parsed.document.provenance.skillSnapshotSha256, "a".repeat(64)); assert.equal(parsed.document.provenance.skillSourceCommit, "b".repeat(40)); const d1 = parsed.document.charts.find((chart) => chart.id === "D1"); assert.ok(d1); assert.equal(d1.houses.length, 12); assert.deepEqual( d1.houses.map((house) => house.houseNumber).sort((a, b) => a - b), Array.from({ length: 12 }, (_, index) => index + 1), ); assert.ok(d1.planets && d1.planets.length === 9); assert.equal(d1.planets[0].retrograde, false); assert.equal(d1.planets[6].retrograde, true); // Appendix ids are canonical ev- ids and globally unique. const ids = [ ...parsed.document.evidenceAppendix.techniqueAudit.map((row) => row.id), ...parsed.document.evidenceAppendix.conflicts.map((row) => row.id), ...parsed.document.evidenceAppendix.calculationEvidence.map((row) => row.id), ]; assert.equal(new Set(ids).size, ids.length); assert.ok(ids.every((id) => /^ev-[a-z0-9_-]{1,63}$/.test(id))); // evidenceHash matches the canonical recomputation from the appendix. assert.equal( parsed.document.provenance.evidenceHash, computeEvidenceHash(parsed.document.evidenceAppendix), ); }); test("technique audit reflects final evidence status and narrative usage", () => { const workflow = pythonStyleChartPayload() as Record; const machinePacket = workflow.machine_evidence_packet as Record; const sections = machinePacket.sections as Record>; sections.external_oracle_status.status = "official_blocked"; const packet = buildPacket({ workflow }); assert.deepEqual(packet.blockedTechniques, []); const output = agentOutput(); const blockedRef = packet.evidenceRefs.find((ref) => ref.technique === "D2"); assert.ok(blockedRef); output.thematicNarrative[0].evidenceRefs.push(blockedRef.id); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: output, }); const auditById = new Map(document.evidenceAppendix.techniqueAudit.map((row) => [row.id, row])); const referencedIds = new Set(document.thematicNarrative.flatMap((section) => section.evidenceRefs)); assert.ok([...referencedIds].every((id) => auditById.get(id)?.used === true)); assert.equal(document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D1")?.status, "verified"); assert.equal(document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D2")?.status, "blocked"); assert.match( document.evidenceAppendix.techniqueAudit.find((row) => row.techniqueName === "D2")?.notes ?? "", /未返回本次计算数据/, ); const external = document.evidenceAppendix.techniqueAudit.find( (row) => row.techniqueName === "external_oracle_status", ); assert.equal(external?.status, "blocked"); assert.match(external?.notes ?? "", /官方外部引擎本次未验证或不可用/); }); test("D9/D10 charts are omitted when no real divisional houses exist", () => { const workflow = pythonStyleChartPayload() as Record; const chart = workflow.chart as Record; const modules = chart.modules as Record; modules.varga_full = {}; const packet = buildPacket({ workflow }); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput(), }); const parsed = safeParseServerReportDocument(document); assert.equal(parsed.ok, true); if (!parsed.ok) return; assert.deepEqual(parsed.document.charts.map((chartRow) => chartRow.id), ["D1"]); }); // --------------------------------------------------------------------------- // Deterministic guard // --------------------------------------------------------------------------- test("guard rejects dangling evidence refs", () => { const packet = buildPacket(); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput({ thematicNarrative: [ { id: "career", theme: "career", title: "事业", narrative: "稳定结构。", actions: [], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-999"], }, ], }), }); const guarded = applyReportGuard(document, packet); assert.equal(guarded.ok, false); if (!guarded.ok) assert.match(guarded.reason, /unresolved_evidence_ref/); }); test("guard redacts precise timing and downgrades the section when timing is blocked", () => { const packet = buildPacket(); const timingBlockedPacket = { ...packet, answerPolicy: { ...packet.answerPolicy, canAnswerPreciseTiming: false } }; const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet: timingBlockedPacket, agentOutput: agentOutput({ thematicNarrative: [ { id: "career", theme: "career", title: "事业", narrative: "方向稳定。2027年3月将迎来事业转折,届时务必把握机会。", actions: ["2027年3月跳槽"], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-1"], }, ], }), }); const guarded = applyReportGuard(document, timingBlockedPacket); assert.equal(guarded.ok, true); if (!guarded.ok) return; const section = (guarded.document as unknown as { thematicNarrative: { narrative: string; claimStatus: string; caveats: string[] }[] }) .thematicNarrative[0]; assert.doesNotMatch(section.narrative, /2027年3月/); assert.equal(section.claimStatus, "blocked"); assert.ok(section.caveats.some((caveat) => caveat.includes("确定性边界"))); }); test("guard rejects medical deterministic claims outright", () => { const packet = buildPacket(); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput({ thematicNarrative: [ { id: "career", theme: "career", title: "事业", narrative: "你一定会患上心脏病。", actions: [], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-1"], }, ], }), }); const guarded = applyReportGuard(document, packet); assert.equal(guarded.ok, false); if (!guarded.ok) assert.match(guarded.reason, /deterministic_medical_claim/); }); test("guard rejects investment deterministic claims hidden in actions", () => { const packet = buildPacket(); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput({ thematicNarrative: [ { id: "wealth", theme: "wealth", title: "财富", narrative: "财富结构稳定。", actions: ["买入股票必然大涨"], caveats: [], claimStatus: "single_system_inference", evidenceRefs: ["ev-audit-1"], }, ], }), }); const guarded = applyReportGuard(document, packet); assert.equal(guarded.ok, false); if (!guarded.ok) assert.match(guarded.reason, /deterministic_investment_claim/); }); test("guard forces blocked when every evidence ref is blocked", () => { const workflow = pythonStyleChartPayload() as Record; const consumer = workflow.consumer_context as Record; consumer.hard_blockers = ["Narayana"]; consumer.available_layers = ["D1", "Vimshottari"]; const packet = buildPacket({ workflow }); const blockedRef = packet.evidenceRefs.find((ref) => ref.status === "blocked"); assert.ok(blockedRef); const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet, agentOutput: agentOutput({ thematicNarrative: [ { id: "timing", theme: "timing", title: "时机", narrative: "该部分仅保留方向性说明。", actions: [], caveats: [], claimStatus: "single_system_inference", evidenceRefs: [blockedRef.id], }, ], }), }); const guarded = applyReportGuard(document, packet); assert.equal(guarded.ok, true); if (!guarded.ok) return; const section = (guarded.document as unknown as { thematicNarrative: { id: string; claimStatus: string }[] }) .thematicNarrative[0]; assert.equal(section.claimStatus, "blocked"); }); test("guard redacts timing from the summary and blocks the report-level status", () => { const packet = buildPacket(); const timingBlockedPacket = { ...packet, answerPolicy: { ...packet.answerPolicy, canAnswerPreciseTiming: false } }; const document = assembleReportDocument({ reportId: "22222222-2222-4222-8222-222222222222", generatedAt: "2026-08-06T00:00:00.000Z", packet: timingBlockedPacket, agentOutput: agentOutput({ executiveSummary: { headline: "综合盘面以事业发展为主线", summary: "明年3月将迎来关键转折,整体结构稳定。", priorities: ["先聚焦职业方向"], }, }), }); const guarded = applyReportGuard(document, timingBlockedPacket); assert.equal(guarded.ok, true); if (!guarded.ok) return; const summary = (guarded.document as unknown as { executiveSummary: { summary: string; overallClaimStatus: string } }) .executiveSummary; assert.doesNotMatch(summary.summary, /明年3月/); assert.equal(summary.overallClaimStatus, "blocked"); }); test("findForbiddenDeterministicClaims and redaction are deterministic", () => { assert.ok(findForbiddenDeterministicClaims("2027年3月会发生转折").some((claim) => claim.domain === "timing")); assert.ok(findForbiddenDeterministicClaims("投资必然赚钱").some((claim) => claim.domain === "investment")); assert.equal(findForbiddenDeterministicClaims("方向性判断稳定").length, 0); const redacted = redactDeterministicSentences("方向稳定。2027年3月转折。", PRECISE_TIMING_PATTERNS); assert.equal(redacted.removedCount, 1); assert.doesNotMatch(redacted.text, /2027年3月/); }); // --------------------------------------------------------------------------- // Generation pipeline (fake agent, real canonical parse) // --------------------------------------------------------------------------- function fakeAgent(output: PersonalReportAgentOutput, calls: { count: number }): ReportAgentPort { return { modelId: "test-model", async generate() { calls.count += 1; return output; }, }; } test("generatePersonalReport returns a ready document that passes the server parse", async () => { const bundle = bundleV2Fixture(); const calls = { count: 0 }; const result = await generatePersonalReport({ reportId: "22222222-2222-4222-8222-222222222222", bundle, depth: "standard", agent: fakeAgent(agentOutput({ thematicNarrative: [{ id: "theme-career", theme: "career", title: "事业", narrative: "事业主题存在可审慎表达的结构性线索。", actions: [], caveats: [], claimStatus: "multi_system_consensus", evidenceRefs: ["ev-tech-d1", "ev-tech-d10"], }], }), calls), now: () => new Date("2026-08-06T00:00:00.000Z"), }); assert.equal(calls.count, 1); assert.equal(result.status, "ready"); if (result.status !== "ready") return; assert.match(result.evidenceHash, /^[0-9a-f]{64}$/); const reparsed = safeParseServerReportDocument(result.document); assert.equal(reparsed.ok, true); }); test("generatePersonalReport fails with report_guard_rejected on guard rejection", async () => { const bundle = bundleV2Fixture(); const result = await generatePersonalReport({ reportId: "22222222-2222-4222-8222-222222222222", bundle, depth: "standard", agent: fakeAgent(agentOutput({ thematicNarrative: [ { id: "theme-career", theme: "career", title: "事业", narrative: "你必定会胜诉。", actions: [], caveats: [], claimStatus: "multi_system_consensus", evidenceRefs: ["ev-tech-d1", "ev-tech-d10"], }, ], }), { count: 0 }), }); assert.deepEqual(result, { status: "failed", failureCode: "report_guard_rejected" }); }); test("generatePersonalReport fails with report_schema_invalid when the final parse rejects", async () => { const bundle = bundleV2Fixture(); const blockedRef = bundle.evidenceRefs.find((ref) => ref.status === "blocked"); assert.ok(blockedRef); const result = await generatePersonalReport({ reportId: "22222222-2222-4222-8222-222222222222", bundle, depth: "standard", agent: fakeAgent(agentOutput({ thematicNarrative: [ { id: "timing", theme: "timing", title: "时机", narrative: "该部分必然会成功,结构稳定。", actions: [], caveats: [], claimStatus: "single_system_inference", evidenceRefs: [blockedRef.id], }, ], }), { count: 0 }), now: () => new Date("2026-08-06T00:00:00.000Z"), }); // The guard downgrades the section to blocked (all refs blocked); the // blocked section still contains the deterministic phrase 必然, so the FINAL // canonical server parse rejects it. Guard mutations are always re-validated. assert.equal(result.status, "failed"); if (result.status === "failed") assert.equal(result.failureCode, "report_schema_invalid"); }); test("skill snapshot is the real packaged report manifest sha256, never the literal unknown", async () => { const snapshot: SkillSnapshot = resolveSkillSnapshot(); assert.equal(snapshot.name, "jyotish-personal-report"); assert.equal(snapshot.version, "1.0.0"); assert.match(snapshot.sha256, /^[0-9a-f]{64}$/); assert.notEqual(snapshot.sha256, "unknown"); const again: SkillSnapshot = resolveSkillSnapshot(); assert.equal(snapshot.sha256, again.sha256); }); test("stable codes include the request-conflict mapping", () => { assert.equal(REPORT_STABLE_CODES.requestConflict, "report_request_conflict"); }); // --------------------------------------------------------------------------- // ReportEvidenceBundle v2 contract: hash, coverage and closed references // --------------------------------------------------------------------------- function bundleV2Fixture(): ReportEvidenceBundleV2 { return finalizeReportEvidenceBundleV2({ schemaVersion: "report_evidence_bundle.v2", subject: { displayName: "测试用户", birthTimeStatus: "accepted", birthPlaceLabel: "北京", }, requestedThemes: ["career", "wealth"], reportType: "personal_full", presentationMode: "default", calculationProfile: { calculationHash: "c".repeat(64), calculationHashDerived: false, birthTimeStatus: "accepted", ayanamsa: "Lahiri", nodeMode: "true", houseSystem: "whole_sign", vimshottari: [{ lord: "Moon", start: "2019-01-01", end: "2029-01-01" }], narayana: [{ lord: "Sun", start: "2023-01-01", end: "2026-01-01" }], }, skill: { name: "jyotish-personal-report", version: "1.0.0", sha256: "a".repeat(64), sourceCommit: "b".repeat(40), }, charts: [ { id: "D1", title: "D1 Rasi", ascendant: { sign: "Aries", degree: 12.5 }, houses: Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"][index], signDerived: false, occupants: index === 0 ? ["Sun", "Mars"] : [], })), planets: [ { id: "Sun", sign: "Aries", degree: 12.5, house: 1, retrograde: false }, { id: "Mars", sign: "Aries", degree: 18.25, house: 1, retrograde: false }, ], }, { id: "D10", title: "D10 Dasamsa", ascendant: { sign: "Capricorn", degree: 4.5 }, houses: Array.from({ length: 12 }, (_, index) => ({ number: index + 1, sign: ["Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius"][index], signDerived: false, occupants: [], })), planets: [], }, ], claimCards: [{ id: "ev-claim-career", theme: "career", section: "事业与方向", conclusion: "事业主题存在可审慎陈述的结构性线索。", supportingFacts: [ { id: "ev-fact-career-d1", label: "D1 十宫结构", value: "十宫结构已由正式计算结果投影。", evidenceRef: "ev-tech-d1", status: "verified", }, { id: "ev-fact-career-d10", label: "D10 事业分盘", value: "D10 已由正式计算结果投影。", evidenceRef: "ev-tech-d10", status: "verified", }, ], counterFacts: [], executedTechniqueRefs: ["ev-tech-d1", "ev-tech-d10"], assertionLevel: "multi_system_consensus", timingBoundary: null, verificationQuestions: [], }], blockedSections: [{ id: "ev-blocked-wealth", theme: "wealth", section: "财富结构", reason: "缺少 D2 与 D11 正式分盘,不能形成财富结论。", missingTechniqueRefs: ["ev-tech-d2", "ev-tech-d11"], }], conflicts: [], executionLedger: [ { id: "ev-tech-d1", technique: "D1", status: "verified", executed: true, note: "基础盘已执行" }, { 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-d11", technique: "D11", status: "blocked", executed: false, note: "未提供" }, ], evidenceRefs: [ { id: "ev-tech-d1", technique: "D1", status: "verified" }, { id: "ev-tech-d10", technique: "D10", status: "verified" }, { id: "ev-tech-d2", technique: "D2", status: "blocked" }, { id: "ev-tech-d11", technique: "D11", status: "blocked" }, ], answerPolicy: { canAnswerPreciseTiming: false, birthTimePolicy: "accepted_directional_only", deterministicClaimsForbiddenFor: ["timing", "medical", "investment"], }, }); } function withoutBundleHash(bundle: ReportEvidenceBundleV2): Omit { const { bundleHash, ...content } = bundle; void bundleHash; return content; } test("bundle v2 covers every requested theme with a claim card or blocked section", () => { const bundle = bundleV2Fixture(); const coveredThemes = new Set([ ...bundle.claimCards.map((card) => card.theme), ...bundle.blockedSections.map((section) => section.theme), ]); assert.deepEqual([...coveredThemes].sort(), [...bundle.requestedThemes].sort()); assert.deepEqual(validateReportEvidenceBundleV2(bundle), bundle); }); test("bundle v2 accepted birth time has an explicit directional policy and no fake candidateRange", () => { const bundle = bundleV2Fixture(); assert.equal(bundle.subject.birthTimeStatus, "accepted"); assert.equal(bundle.calculationProfile.birthTimeStatus, "accepted"); assert.equal(bundle.answerPolicy.birthTimePolicy, "accepted_directional_only"); assert.equal(bundle.answerPolicy.canAnswerPreciseTiming, false); assert.equal("candidateRange" in bundle, false); assert.doesNotMatch(JSON.stringify(bundle), /candidateRange/); }); test("bundle v2 schema rejects opaque techniques, calculation labels and fake dasha coordinates", () => { const bundle = bundleV2Fixture(); const content = withoutBundleHash(bundle); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, executionLedger: content.executionLedger.map((receipt, index) => ( index === 0 ? { ...receipt, technique: "AKIAIOSFODNN7EXAMPLE" } : receipt )), evidenceRefs: content.evidenceRefs.map((ref, index) => ( index === 0 ? { ...ref, technique: "AKIAIOSFODNN7EXAMPLE" } : ref )), }), /report_bundle_technique_not_allowlisted/); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, calculationProfile: { ...content.calculationProfile, nodeMode: "prod-db-01", } as unknown as ReportEvidenceBundleV2["calculationProfile"], }), /Invalid enum value/); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, calculationProfile: { ...content.calculationProfile, vimshottari: [{ lord: "Moon", start: "39.9000", end: "116.4000" }], }, }), /report_bundle_dasha_date_invalid/); }); test("bundle v2 validator rejects direct free-text policy and conflict injection before Agent use", () => { const bundle = bundleV2Fixture(); const content = withoutBundleHash(bundle); const forgedConflictContent = { ...content, conflicts: [{ id: "ev-conflict-1", techniqueRefs: ["ev-tech-d1"], summary: "Authorization: Bearer secret-token at /Users/private/trace.py", resolutionStatus: "unresolved", }], } as unknown as Omit; assert.throws(() => validateReportEvidenceBundleV2({ ...forgedConflictContent, bundleHash: computeReportEvidenceBundleHash(forgedConflictContent), }), /Invalid enum value/); const forgedPolicyContent = { ...content, answerPolicy: { ...content.answerPolicy, deterministicClaimsForbiddenFor: [ "timing", "AKIAIOSFODNN7EXAMPLE", "prod-db-01", "internal-host-22", ], }, } as unknown as Omit; assert.throws(() => validateReportEvidenceBundleV2({ ...forgedPolicyContent, bundleHash: computeReportEvidenceBundleHash(forgedPolicyContent), }), /Invalid enum value/); }); test("bundle v2 claim technique and fact references are closed", () => { const bundle = bundleV2Fixture(); const ledgerIds = new Set(bundle.executionLedger.map((receipt) => receipt.id)); const evidenceIds = new Set(bundle.evidenceRefs.map((ref) => ref.id)); for (const card of bundle.claimCards) { for (const ref of card.executedTechniqueRefs) assert.equal(ledgerIds.has(ref), true); for (const fact of [...card.supportingFacts, ...card.counterFacts]) { assert.equal(evidenceIds.has(fact.evidenceRef), true); } } const content = withoutBundleHash(bundle); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, executionLedger: content.executionLedger.filter((receipt) => receipt.id !== "ev-tech-d10"), }), /report_bundle_invalid_technique_ref:ev-tech-d10/); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, claimCards: content.claimCards.map((card) => ({ ...card, supportingFacts: card.supportingFacts.map((fact) => ({ ...fact, evidenceRef: "ev-unknown-fact-ref", })), })), }), /report_bundle_invalid_fact_ref:ev-unknown-fact-ref/); }); test("bundle v2 blocked or partial techniques cannot support multi-system consensus", () => { const bundle = bundleV2Fixture(); const content = withoutBundleHash(bundle); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, executionLedger: content.executionLedger.map((receipt) => ( receipt.id === "ev-tech-d10" ? { ...receipt, status: "partial" as const } : receipt )), evidenceRefs: content.evidenceRefs.map((ref) => ( ref.id === "ev-tech-d10" ? { ...ref, status: "partial" as const } : ref )), }), /report_bundle_invalid_consensus:ev-claim-career/); assert.throws(() => finalizeReportEvidenceBundleV2({ ...content, executionLedger: content.executionLedger.map((receipt) => ( receipt.id === "ev-tech-d10" ? { ...receipt, status: "blocked" as const, executed: false } : receipt )), evidenceRefs: content.evidenceRefs.map((ref) => ( ref.id === "ev-tech-d10" ? { ...ref, status: "blocked" as const } : ref )), }), /report_bundle_invalid_technique_ref:ev-tech-d10/); }); test("bundle v2 hash is collection-order stable, content-sensitive and excludes bundleHash itself", () => { const bundle = bundleV2Fixture(); const content = withoutBundleHash(bundle); const reordered = { ...content, requestedThemes: [...content.requestedThemes].reverse(), calculationProfile: { ...content.calculationProfile, vimshottari: content.calculationProfile.vimshottari ? [...content.calculationProfile.vimshottari].reverse() : null, narayana: content.calculationProfile.narayana ? [...content.calculationProfile.narayana].reverse() : null, }, charts: [...content.charts].reverse().map((chart) => ({ ...chart, houses: [...chart.houses].reverse().map((house) => ({ ...house, occupants: [...house.occupants].reverse(), })), planets: [...chart.planets].reverse(), })), claimCards: [...content.claimCards].reverse().map((card) => ({ ...card, supportingFacts: [...card.supportingFacts].reverse(), counterFacts: [...card.counterFacts].reverse(), executedTechniqueRefs: [...card.executedTechniqueRefs].reverse(), verificationQuestions: [...card.verificationQuestions].reverse(), })), blockedSections: [...content.blockedSections].reverse().map((section) => ({ ...section, missingTechniqueRefs: [...section.missingTechniqueRefs].reverse(), })), conflicts: [...content.conflicts].reverse().map((conflict) => ({ ...conflict, techniqueRefs: [...conflict.techniqueRefs].reverse(), })), executionLedger: [...content.executionLedger].reverse(), evidenceRefs: [...content.evidenceRefs].reverse(), answerPolicy: { ...content.answerPolicy, deterministicClaimsForbiddenFor: [...content.answerPolicy.deterministicClaimsForbiddenFor].reverse(), }, }; assert.equal(computeReportEvidenceBundleHash(content), bundle.bundleHash); assert.equal(computeReportEvidenceBundleHash(reordered), bundle.bundleHash); const changed = { ...content, claimCards: content.claimCards.map((card) => ({ ...card, conclusion: `${card.conclusion} 内容已改变。`, })), }; assert.notEqual(computeReportEvidenceBundleHash(changed), bundle.bundleHash); const forgedHash = { ...bundle, bundleHash: "f".repeat(64) }; assert.equal(computeReportEvidenceBundleHash(withoutBundleHash(forgedHash)), bundle.bundleHash); assert.throws(() => validateReportEvidenceBundleV2(forgedHash), /report_bundle_hash_mismatch/); }); test("bundle v2 builder blocks incomplete wealth evidence without leaking workflow internals", () => { const generalWorkflow = pythonStyleChartPayload() as Record; const generalChart = generalWorkflow.chart as Record; const generalPlanets = generalChart.planets as Record; generalPlanets["AuthorizationSecretPlanet"] = { sign: "Leo", degree: 142.5, house: 1, retrograde: false, }; const generalDasha = generalChart.dasha as Record; (generalDasha.mahadashas as Record[]).push( { lord: "/opt/private/dasha-secret", start: "2029-01-01", end: "2030-01-01", }, { lord: "Moon", start: "39.9000", end: "116.4000", }, ); generalWorkflow.calculation_profile = { ayanamsa: "/opt/private/calculation-secret", node_mode: "prod-db-01", house_system: "internal-host-22", }; const wealthWorkflow = pythonStyleChartPayload() as Record; wealthWorkflow.api_key = "sk-private-report-secret"; wealthWorkflow.authorization = "Bearer private-token"; wealthWorkflow.database_url = "postgres://private-db"; wealthWorkflow.internal_path = "/Users/private/project/engine.py"; wealthWorkflow.runtime_path = "/opt/jyotisha-production/private.py"; wealthWorkflow.latitude = 39.9; wealthWorkflow.longitude = 116.4; wealthWorkflow.prompt = "raw hidden prompt"; wealthWorkflow.traceback = "Traceback: raw tool failure"; const wealthConsumer = wealthWorkflow.consumer_context as Record; const wealthPolicy = (wealthConsumer.answer_policy ?? {}) as Record; wealthPolicy.deterministic_claims_forbidden_for = [ "timing", "/opt/private/policy-secret", "secret_token", ]; wealthConsumer.answer_policy = wealthPolicy; const wealthMachine = wealthWorkflow.machine_evidence_packet as Record; wealthMachine.conflicts = [{ techniques: ["D1"], summary: "Authorization: Bearer secret-token at /Users/private/trace.py", }]; const wealthSections = wealthMachine.sections as Record; wealthSections["Authorization: Bearer section-secret at /Users/private/tool.py"] = { status: "used", source_path: "modules.secret", }; wealthSections.AKIAIOSFODNN7EXAMPLE = { status: "used", source_path: "modules.internal", }; const bundle = buildReportEvidenceBundleV2({ workflows: [ { theme: "general", workflow: generalWorkflow }, { theme: "wealth", workflow: wealthWorkflow }, ], subject: { displayName: "测试用户", birthTimeStatus: "accepted", birthPlaceLabel: "北京", }, requestedThemes: ["general", "wealth"], reportType: "personal_full", presentationMode: "default", skillSnapshot: { name: "jyotish-vedic-astrology", version: "6.9.14", sha256: "a".repeat(64), sourceCommit: "b".repeat(40), }, }); assert.equal(bundle.schemaVersion, "report_evidence_bundle.v2"); assert.ok(bundle.claimCards.some((card) => card.theme === "general")); assert.ok(bundle.blockedSections.some((section) => section.theme === "wealth")); assert.equal("candidateRange" in bundle, false); assert.equal(bundle.answerPolicy.birthTimePolicy, "accepted_directional_only"); assert.equal(bundle.answerPolicy.canAnswerPreciseTiming, false); assert.equal(bundle.calculationProfile.ayanamsa, null); assert.equal(bundle.calculationProfile.nodeMode, null); assert.equal(bundle.calculationProfile.houseSystem, null); assert.equal(bundle.charts.some((chart) => ( chart.planets.some((planet) => planet.id.includes("AuthorizationSecretPlanet")) || chart.houses.some((house) => house.occupants.includes("AuthorizationSecretPlanet")) )), false); assert.equal(bundle.calculationProfile.vimshottari?.some((period) => ( period.lord.includes("dasha-secret") )), false); assert.deepEqual(bundle.answerPolicy.deterministicClaimsForbiddenFor, ["timing"]); assert.ok(bundle.conflicts.some((conflict) => ( conflict.techniqueRefs.length === 1 && conflict.techniqueRefs[0] === "ev-tech-d1" ))); assert.ok(bundle.conflicts.every((conflict) => ( conflict.summary === "所列技法的结果存在未闭合冲突,报告不得据此作确定性提升。" && conflict.resolutionStatus === "unresolved" ))); assert.equal( bundle.executionLedger.some((receipt) => receipt.technique.includes("section-secret")), false, ); for (const technique of ["D2", "D11"]) { const receipts = bundle.executionLedger.filter((entry) => ( entry.technique.toUpperCase() === technique || entry.technique.toUpperCase().endsWith(`:${technique}`) )); assert.ok(receipts.length > 0, `${technique} must have an explicit receipt`); assert.equal(receipts.some((receipt) => receipt.executed || receipt.status === "verified"), false); } const ledgerIds = new Set(bundle.executionLedger.map((receipt) => receipt.id)); const evidenceIds = new Set(bundle.evidenceRefs.map((ref) => ref.id)); for (const card of bundle.claimCards) { for (const ref of card.executedTechniqueRefs) assert.equal(ledgerIds.has(ref), true); for (const fact of [...card.supportingFacts, ...card.counterFacts]) { assert.equal(evidenceIds.has(fact.evidenceRef), true); } } const serialized = JSON.stringify(bundle); for (const forbidden of [ "sk-private-report-secret", "Bearer private-token", "postgres://private-db", "/Users/private", "/opt/jyotisha-production", "39.9", "116.4", "raw hidden prompt", "Traceback: raw tool failure", "secret-token", "/opt/private", "secret_token", "section-secret", "AuthorizationSecretPlanet", "dasha-secret", "calculation-secret", "AKIAIOSFODNN7EXAMPLE", "prod-db-01", "internal-host-22", ]) { assert.equal(serialized.includes(forbidden), false, `bundle leaked ${forbidden}`); } }); test("bundle v2 never treats an available-only machine section as executed evidence", () => { const workflow = pythonStyleChartPayload() as Record; const consumer = workflow.consumer_context as Record; consumer.available_layers = [ ...(consumer.available_layers as string[]), "Transit", ]; const machine = workflow.machine_evidence_packet as Record; const sections = machine.sections as Record; sections.Transit = { status: "available", source_path: "modules.transit" }; const bundle = buildReportEvidenceBundleV2({ workflows: [{ theme: "timing", workflow }], subject: { displayName: "测试用户", birthTimeStatus: "confirmed", birthPlaceLabel: "北京", }, requestedThemes: ["timing"], reportType: "personal_thematic", presentationMode: "default", skillSnapshot: { name: "jyotish-vedic-astrology", version: "6.9.14", sha256: "a".repeat(64), sourceCommit: "b".repeat(40), }, }); assert.equal(bundle.claimCards.some((card) => card.theme === "timing"), false); assert.equal(bundle.blockedSections.some((section) => section.theme === "timing"), true); const transitReceipts = bundle.executionLedger.filter((receipt) => ( receipt.technique === "Transit" || receipt.technique.endsWith(":Transit") )); assert.ok(transitReceipts.length > 0); assert.equal(transitReceipts.some((receipt) => receipt.executed), false); });