Files
Jyotisha/frontend/tests/personal-report-generation.test.ts
T
2026-08-15 05:10:37 +08:00

1340 lines
53 KiB
TypeScript

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<string, Record<string, unknown>> = {
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<string, Record<string, unknown>> = {};
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<string, Record<string, unknown>> = {
Sun: { sign: "Leo", sign_idx: 4 },
Moon: { sign: "Virgo", sign_idx: 5 },
Mars: { sign: "Cancer", sign_idx: 3 },
};
const d10Planets: Record<string, Record<string, unknown>> = {
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<BuildPacketOverrides> = {}): 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> = {}): 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<string, unknown>).internal_path = "/opt/app/private/engine.py";
(workflow as Record<string, unknown>).prompt_text = "系统提示词原文";
(workflow as Record<string, unknown>).traceback = "Traceback (most recent call last)";
(workflow as Record<string, unknown>).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<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
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<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
const houses = chart.houses as Record<string, unknown>;
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<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
const planets = chart.planets as Record<string, Record<string, unknown>>;
planets.Sun.retrograde = undefined;
assert.throws(() => buildPacket({ workflow }), ReportEvidenceInsufficientError);
});
test("packet builder fails closed without ascendant or evidence refs", () => {
const workflow = pythonStyleChartPayload() as Record<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
delete chart.ascendant;
assert.throws(() => buildPacket({ workflow }), ReportEvidenceInsufficientError);
const workflow2 = pythonStyleChartPayload() as Record<string, unknown>;
const consumer = workflow2.consumer_context as Record<string, unknown>;
consumer.available_layers = [];
const machine = workflow2.machine_evidence_packet as Record<string, unknown>;
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<string, unknown>;
const chartData = base.chart as Record<string, unknown>;
// 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<string, unknown>;
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<string, unknown>;
const machinePacket = workflow.machine_evidence_packet as Record<string, unknown>;
const sections = machinePacket.sections as Record<string, Record<string, unknown>>;
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<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
const modules = chart.modules as Record<string, unknown>;
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<string, unknown>;
const consumer = workflow.consumer_context as Record<string, unknown>;
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<ReportEvidenceBundleV2, "bundleHash"> {
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<ReportEvidenceBundleV2, "bundleHash">;
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<ReportEvidenceBundleV2, "bundleHash">;
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<string, unknown>;
const generalChart = generalWorkflow.chart as Record<string, unknown>;
const generalPlanets = generalChart.planets as Record<string, unknown>;
generalPlanets["AuthorizationSecretPlanet"] = {
sign: "Leo",
degree: 142.5,
house: 1,
retrograde: false,
};
const generalDasha = generalChart.dasha as Record<string, unknown>;
(generalDasha.mahadashas as Record<string, unknown>[]).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<string, unknown>;
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<string, unknown>;
const wealthPolicy = (wealthConsumer.answer_policy ?? {}) as Record<string, unknown>;
wealthPolicy.deterministic_claims_forbidden_for = [
"timing",
"/opt/private/policy-secret",
"secret_token",
];
wealthConsumer.answer_policy = wealthPolicy;
const wealthMachine = wealthWorkflow.machine_evidence_packet as Record<string, unknown>;
wealthMachine.conflicts = [{
techniques: ["D1"],
summary: "Authorization: Bearer secret-token at /Users/private/trace.py",
}];
const wealthSections = wealthMachine.sections as Record<string, unknown>;
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<string, unknown>;
const consumer = workflow.consumer_context as Record<string, unknown>;
consumer.available_layers = [
...(consumer.available_layers as string[]),
"Transit",
];
const machine = workflow.machine_evidence_packet as Record<string, unknown>;
const sections = machine.sections as Record<string, unknown>;
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);
});