fix(report): extract D2/D11 varga aliases so wealth chapters pass final parse
Independent Staging Quality Gate / validate (push) Failing after 7m34s
Independent Staging Quality Gate / publish (push) Has been cancelled

Engine keys like D2_Hora never became structured charts, so wealth write
themes failed the document contract. Canonicalize document vargas, demote
missing charts to blocked, and classify the parse failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 14:53:52 +08:00
parent 661a0dab14
commit 45f190a1d0
7 changed files with 318 additions and 75 deletions
+16
View File
@@ -7472,3 +7472,19 @@
- 相关记录:BUG-441、BUG-449、BUG-461、BUG-469、BUG-471
- 复发自:BUG-469opening 正文仍提问)与 BUG-471(槽位必须可见题干)同时生效
- 修复版本:待发布
## BUG-486 | 个人报告财富章有 D2/D11 回执却无结构化分盘,终态 parse 整份失败
- 状态:resolved
- 首次发现:2026-09-02
- 最近更新:2026-09-02
- 影响面:`buildReportEvidenceBundleV2``generatePersonalReport``readAllVargaCharts` / `readVargaHouses``POST /api/reports` 后台生成、`report_schema_invalid`
- 用户现象:计费配置可用后,完整个人报告创建成功但生成停在失败;列表 `status=failed``failureCode=report_schema_invalid`。日志内层是 `final_parse_rejected`,进度可到约 85%,财富章可能已写成 ready,事业/婚恋/时机没有章节行。
- 触发条件:请求含财富主题;引擎 `modules.varga_full` 使用 `D2_Hora` / `D11_Rudramsa` 这类带流派后缀的键;工作流把 D2/D11 标成已执行。财富章因此被标成 write,终态文档却没有 `charts[].id` 为 D2/D11。
- 根因:两层叠加。提取只把 `^D\d{1,3}$` 当成分盘 id,丢掉 `D2_Hora` / `D11_Rudramsa``readVargaHouses` 也只认 D9/D10。主题计划仍可用 D2/D11 回执闭合财富章。组装后 `findThemeCoverageViolations` 要求财富主题必须带结构化 D2 与 D11,最终 parse 失败,公开码压成 `report_schema_invalid`
- 修复:把文档允许的分盘键归一成 D2/D9/D10/D11/D24(含 `D2_Hora` 等别名),占据星体走 celestial allowlist。write 主题若缺少对应结构化分盘,降成 blocked disclosure,不再组装成会解析失败的文档。终态 parse 失败时把缺分盘与 hash 失配从笼统的 `final_parse_rejected` 分出来;日志仍只写 allowlist token,不含用户或模型原文。不放宽 `.strict()` 文档 schema,也不改 assertionLevel 规则。
- 验证:`frontend/tests/personal-report-generation.test.ts``D2_Hora` / `D11_Rudramsa` 抽出 D2/D11 且财富章成为 claim`frontend/tests/personal-report-generation-v2.test.ts` 财富 write 只有 D1 时 ready 且财富进 blocked`frontend/tests/personal-report-contract.test.ts` 财富主题缺 D2/D11 仍拒绝。三组 focused 测试 85/85`./node_modules/.bin/tsc --noEmit` 通过。未用登录会话在 staging 重生。
- 防复发:文档分盘 id 必须从引擎 `varga_full` 别名归一,不得只认 `D2` 这种短键。财富/事业/婚恋/教育 write 主题缺对应结构化分盘时必须降级 blocked,不得把 `final_parse_rejected` 当正常完成。新增分盘别名必须同时覆盖提取与文档 `CHART_IDS`
- 相关记录:BUG-352、BUG-451、BUG-467
- 复发自:无
- 修复版本:待发布
+1 -1
View File
@@ -479,7 +479,7 @@ export function findChartSetViolations(document: ReportDocument): readonly strin
return violations;
}
const REQUIRED_THEME_CHARTS: Readonly<Record<string, readonly (typeof CHART_IDS)[number][]>> = {
export const REQUIRED_THEME_CHARTS: Readonly<Record<string, readonly (typeof CHART_IDS)[number][]>> = {
career: ["D10"],
wealth: ["D2", "D11"],
marriage: ["D9"],
+184 -73
View File
@@ -3,12 +3,13 @@ import {
computeEvidenceHash,
safeParseServerReportDocument,
} from "./personal-report-contract.server-core.ts";
import type {
ClaimStatus,
EvidenceAppendix,
ReportDepth,
ReportDocumentV1,
ReportDocumentV2,
import {
REQUIRED_THEME_CHARTS,
type ClaimStatus,
type EvidenceAppendix,
type ReportDepth,
type ReportDocumentV1,
type ReportDocumentV2,
} from "./personal-report-contract.ts";
import {
PersonalReportAgentOutputError,
@@ -307,51 +308,84 @@ function readHouses(
return houses;
}
const DOCUMENT_VARGA_CHART_IDS = ["D2", "D9", "D10", "D11", "D24"] as const;
type DocumentVargaChartId = (typeof DOCUMENT_VARGA_CHART_IDS)[number];
const VARGA_KEY_ALIASES: Readonly<Record<DocumentVargaChartId, readonly string[]>> = {
D2: ["D2_Hora", "D2"],
D9: ["D9_Navamsa", "D9"],
D10: ["D10_Dasamsa", "D10"],
D11: ["D11_Rudramsa", "D11"],
D24: ["D24_Siddhamsa", "D24"],
};
function canonicalDocumentVargaChartId(rawId: string): DocumentVargaChartId | null {
const match = /^(D(?:24|11|10|9|2))(?:[_\s-].*)?$/.exec(rawId.trim().toUpperCase());
return match ? match[1] as DocumentVargaChartId : null;
}
/**
* Divisional charts in the real engine live under modules.varga_full with keys
* D9_Navamsa / D10_Dasamsa (or D9 / D10). Each varga is {Ascendant: {sign_idx|
* sign}, <planet>: {sign_idx|sign}, _meta, _dignity, ...} — there are no house
* arrays. House signs are derived whole-sign from the divisional ascendant and
* occupants from each planet's sign index (the same derivation the engine uses
* for D11). Nothing is fabricated; missing varga data simply omits the chart.
* such as D2_Hora / D9_Navamsa / D10_Dasamsa / D11_Rudramsa (or D2 / D9 / D10 /
* D11). Each varga is {Ascendant: {sign_idx|sign}, <planet>: {sign_idx|sign},
* _meta, _dignity, ...} — there are no house arrays. House signs are derived
* whole-sign from the divisional ascendant and occupants from each planet's
* sign index. Missing or non-document vargas are omitted; occupant names go
* through the celestial allowlist.
*/
function deriveVargaHousesFromEngine(
varga: JsonRecord,
): Readonly<{
ascIndex: number;
degree: number;
houses: ReportEvidencePacket["chart"]["houses"];
}> | null {
const ascendant = record(varga.Ascendant);
const ascIndex = ascendant ? signIndex(ascendant.sign_idx ?? ascendant.sign) : null;
if (ascIndex === null) return null;
const occupants: string[][] = Array.from({ length: 12 }, () => []);
for (const [name, item] of Object.entries(varga)) {
if (name.startsWith("_") || name === "Ascendant" || name === "planets") continue;
const row = record(item);
const planetIndex = row ? signIndex(row.sign_idx ?? row.sign) : null;
if (planetIndex === null) continue;
const house = (((planetIndex - ascIndex) % 12) + 12) % 12 + 1;
const safeName = safeCelestialName(name);
if (safeName) occupants[house - 1].push(safeName);
}
return {
ascIndex,
degree: finiteNumber(ascendant?.degree_in_sign ?? ascendant?.degree) ?? 0,
houses: Array.from({ length: 12 }, (_, index) => ({
number: index + 1,
sign: SIGNS[((ascIndex + index) % 12 + 12) % 12],
signDerived: true,
occupants: occupants[index].slice(0, 12),
})),
};
}
function readVargaHouses(
vargaFull: JsonRecord | null,
): ReportEvidencePacket["chart"]["vargaHouses"] {
if (!vargaFull) return [];
const result: { id: "D9" | "D10"; houses: ReportEvidencePacket["chart"]["houses"] }[] = [];
const variants: Readonly<Record<"D9" | "D10", readonly string[]>> = {
D9: ["D9_Navamsa", "D9"],
D10: ["D10_Dasamsa", "D10"],
const result: ReportEvidencePacket["chart"]["vargaHouses"][number][] = [];
const seen = new Set<DocumentVargaChartId>();
const take = (id: DocumentVargaChartId, raw: unknown): void => {
if (seen.has(id)) return;
const varga = record(raw);
if (!varga) return;
const derived = deriveVargaHousesFromEngine(varga);
if (!derived) return;
seen.add(id);
result.push({ id, houses: derived.houses });
};
for (const [id, keys] of Object.entries(variants) as ReadonlyArray<readonly ["D9" | "D10", readonly string[]]>) {
const varga = keys
.map((key) => record(vargaFull[key]))
.find((entry): entry is JsonRecord => entry !== null);
if (!varga) continue;
const ascendant = record(varga.Ascendant);
const ascIndex = ascendant ? signIndex(ascendant.sign_idx ?? ascendant.sign) : null;
if (ascIndex === null) continue;
const occupants: string[][] = Array.from({ length: 12 }, () => []);
for (const [name, item] of Object.entries(varga)) {
if (name.startsWith("_") || name === "Ascendant" || name === "planets") continue;
const row = record(item);
const planetIndex = row ? signIndex(row.sign_idx ?? row.sign) : null;
if (planetIndex === null) continue;
const house = (((planetIndex - ascIndex) % 12) + 12) % 12 + 1;
const safeName = safeCelestialName(name);
if (safeName) occupants[house - 1].push(safeName);
}
const houses: ReportEvidencePacket["chart"]["houses"] = Array.from(
{ length: 12 },
(_, index) => ({
number: index + 1,
sign: SIGNS[((ascIndex + index) % 12 + 12) % 12],
signDerived: true,
occupants: occupants[index].slice(0, 12),
}),
);
result.push({ id, houses });
for (const id of DOCUMENT_VARGA_CHART_IDS) {
for (const key of VARGA_KEY_ALIASES[id]) take(id, vargaFull[key]);
}
for (const [rawId, rawValue] of Object.entries(vargaFull)) {
const id = canonicalDocumentVargaChartId(rawId);
if (id) take(id, rawValue);
}
return result;
}
@@ -904,38 +938,31 @@ function readAllVargaCharts(workflow: JsonRecord): ReportChartFact[] {
const vargaFull = record(modules.varga_full);
if (!vargaFull) return [];
const charts: ReportChartFact[] = [];
for (const [rawId, rawValue] of Object.entries(vargaFull)) {
const id = rawId.toUpperCase();
if (!/^D\d{1,3}$/.test(id)) continue;
const varga = record(rawValue);
if (!varga) continue;
const ascendant = record(varga.Ascendant);
const ascIndex = ascendant ? signIndex(ascendant.sign_idx ?? ascendant.sign) : null;
if (ascIndex === null) continue;
const occupants: string[][] = Array.from({ length: 12 }, () => []);
for (const [name, item] of Object.entries(varga)) {
if (name.startsWith("_") || name === "Ascendant" || name === "planets") continue;
const row = record(item);
const planetIndex = row ? signIndex(row.sign_idx ?? row.sign) : null;
if (planetIndex === null) continue;
const house = (((planetIndex - ascIndex) % 12) + 12) % 12 + 1;
occupants[house - 1].push(name);
}
const seen = new Set<DocumentVargaChartId>();
const take = (id: DocumentVargaChartId, raw: unknown): void => {
if (seen.has(id)) return;
const varga = record(raw);
if (!varga) return;
const derived = deriveVargaHousesFromEngine(varga);
if (!derived) return;
seen.add(id);
charts.push({
id,
title: `${id} 分盘`,
ascendant: {
sign: SIGNS[ascIndex],
degree: finiteNumber(ascendant?.degree_in_sign ?? ascendant?.degree) ?? 0,
sign: SIGNS[derived.ascIndex],
degree: derived.degree,
},
houses: Array.from({ length: 12 }, (_, index) => ({
number: index + 1,
sign: SIGNS[(ascIndex + index) % 12],
signDerived: true,
occupants: occupants[index].slice(0, 20),
})),
houses: derived.houses,
planets: [],
});
};
for (const id of DOCUMENT_VARGA_CHART_IDS) {
for (const key of VARGA_KEY_ALIASES[id]) take(id, vargaFull[key]);
}
for (const [rawId, rawValue] of Object.entries(vargaFull)) {
const id = canonicalDocumentVargaChartId(rawId);
if (id) take(id, rawValue);
}
return charts;
}
@@ -950,8 +977,10 @@ function buildLegacyPacketFromBundle(bundle: ReportEvidenceBundleV2): ReportEvid
const d1 = bundle.charts.find((chart) => chart.id === "D1");
if (!d1?.ascendant) throw new ReportEvidenceInsufficientError("ascendant_missing");
const vargaHouses = bundle.charts
.filter((chart): chart is ReportChartFact & { id: "D9" | "D10" } => chart.id === "D9" || chart.id === "D10")
.map((chart) => ({ id: chart.id, houses: chart.houses }));
.flatMap((chart) => {
const id = canonicalDocumentVargaChartId(chart.id);
return id ? [{ id, houses: chart.houses }] : [];
});
const receiptById = new Map(bundle.executionLedger.map((receipt) => [receipt.id, receipt]));
return {
schemaVersion: "report_evidence_packet.v1",
@@ -1875,7 +1904,7 @@ export function buildReportEvidenceBundleV2(
? "candidate_directional_only"
: "reported_directional_only";
return finalizeReportEvidenceBundleV2({
return demoteThemesMissingRequiredCharts(finalizeReportEvidenceBundleV2({
schemaVersion: "report_evidence_bundle.v2",
subject: input.subject,
requestedThemes: requestedPlans.map((plan) => plan.theme),
@@ -1909,6 +1938,74 @@ export function buildReportEvidenceBundleV2(
birthTimePolicy,
deterministicClaimsForbiddenFor,
},
}));
}
function requiredStructuredChartsForTheme(theme: string): readonly string[] {
return REQUIRED_THEME_CHARTS[theme] ?? [];
}
/**
* A write-theme claim without its required structured charts would assemble
* into a document that the final parse rejects. Demote that theme to a blocked
* disclosure instead of failing the whole report.
*/
function demoteThemesMissingRequiredCharts(bundle: ReportEvidenceBundleV2): ReportEvidenceBundleV2 {
const presentCharts = new Set(bundle.charts.map((chart) => chart.id));
const demoted = bundle.claimCards.filter((card) => (
requiredStructuredChartsForTheme(card.theme).some((chartId) => !presentCharts.has(chartId))
));
if (demoted.length === 0) return bundle;
const extraReceipts: TechniqueExecutionReceipt[] = [];
const extraBlocked: ReportEvidenceBundleV2["blockedSections"][number][] = [];
const existingIds = new Set([
...bundle.executionLedger.map((receipt) => receipt.id),
...bundle.blockedSections.map((section) => section.id),
]);
for (const card of demoted) {
const missing = requiredStructuredChartsForTheme(card.theme)
.filter((chartId) => !presentCharts.has(chartId));
const refs: string[] = [];
for (const chartId of missing) {
const technique = `${card.theme}:${chartId}`;
let id = `ev-tech-${evidenceSlug(technique, "missing")}`;
if (existingIds.has(id)) id = `ev-tech-${evidenceSlug(`${technique}_chart`, "missing")}`;
extraReceipts.push({
id,
technique,
status: "blocked",
executed: false,
note: safeReceiptNote("blocked", false),
});
existingIds.add(id);
refs.push(id);
}
const blockedId = existingIds.has(`ev-blocked-${evidenceSlug(card.theme, "theme")}`)
? `ev-blocked-${evidenceSlug(`${card.theme}_charts`, "theme")}`
: `ev-blocked-${evidenceSlug(card.theme, "theme")}`;
extraBlocked.push({
id: blockedId,
theme: card.theme,
section: card.section,
reason: `缺少结构化分盘:${missing.join("、")}`,
missingTechniqueRefs: refs,
});
existingIds.add(blockedId);
}
const demotedThemes = new Set(demoted.map((card) => card.theme));
const { bundleHash: _bundleHash, ...content } = bundle;
void _bundleHash;
return finalizeReportEvidenceBundleV2({
...content,
claimCards: bundle.claimCards.filter((card) => !demotedThemes.has(card.theme)),
blockedSections: [...bundle.blockedSections, ...extraBlocked],
executionLedger: [...bundle.executionLedger, ...extraReceipts],
evidenceRefs: [
...bundle.evidenceRefs,
...extraReceipts.map(({ id, technique, status }) => ({ id, technique, status })),
],
themeNarrativeSeeds: bundle.themeNarrativeSeeds.filter((seed) => !demotedThemes.has(seed.theme)),
});
}
@@ -2053,6 +2150,7 @@ export function assembleReportDocument(
claimStatus: packet.blockedTechniques.length > 0 ? "blocked" : "single_system_inference",
}];
for (const varga of packet.chart.vargaHouses) {
if (varga.id !== "D9" && varga.id !== "D10") continue;
if (varga.houses.length === 0) continue;
charts.push({
id: varga.id,
@@ -2845,6 +2943,18 @@ export function classifyReportSchemaInnerReason(error: unknown): ReportSchemaInn
return "schema_invalid_unclassified";
}
export function classifyFinalParseInnerReason(
errors: readonly Readonly<{ path: string; message: string }>[],
): ReportSchemaInnerReason {
if (errors.some((error) => error.message.includes("requires structured"))) {
return "final_parse_missing_divisional_chart";
}
if (errors.some((error) => error.path === "provenance.evidenceHash")) {
return "final_parse_evidence_hash";
}
return "final_parse_rejected";
}
export function isPersonalReportGenerationAbort(error: unknown, signal?: AbortSignal): boolean {
if (signal?.aborted) return true;
return error instanceof Error && error.name === "AbortError";
@@ -3041,7 +3151,7 @@ async function generateSectionedPersonalReport(
const guarded = applyReportGuard(candidate, buildLegacyPacketFromBundle(bundle));
if (!guarded.ok) return { status: "failed", failureCode: "report_guard_rejected" };
const parsed = safeParseServerReportDocument(guarded.document);
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") return failSchema("final_parse_rejected");
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") return failSchema(parsed.ok ? "final_parse_rejected" : classifyFinalParseInnerReason(parsed.errors));
return { status: "ready", document: parsed.document, evidenceHash: computeEvidenceHash(parsed.document.evidenceAppendix), usage: deps.agent.getUsage?.() };
} catch (error) {
rethrowIfAborted(error, deps.signal);
@@ -3063,6 +3173,7 @@ export async function generatePersonalReport(
let bundle: ReportEvidenceBundleV2;
try {
bundle = validateReportEvidenceBundleV2(deps.bundle);
bundle = demoteThemesMissingRequiredCharts(bundle);
} catch (error) {
rethrowIfAborted(error, deps.signal);
return failSchema("bundle_invalid");
@@ -3142,7 +3253,7 @@ export async function generatePersonalReport(
}
const parsed = safeParseServerReportDocument(guarded.document);
if (!parsed.ok || parsed.document.schemaVersion !== "report_document.v2") {
return failSchema("final_parse_rejected");
return failSchema(parsed.ok ? "final_parse_rejected" : classifyFinalParseInnerReason(parsed.errors));
}
return {
status: "ready",
+1 -1
View File
@@ -39,7 +39,7 @@ export const claimStatusSchema = z.enum([
]);
export type ReportVargaHouses = Readonly<{
id: "D9" | "D10";
id: "D2" | "D9" | "D10" | "D11" | "D24";
houses: readonly ReportChartHouse[];
}>;
@@ -283,6 +283,20 @@ test("v2 thematic sections require their real structured divisional charts", ()
});
assert.ok(findThemeCoverageViolations(unsupportedCareer).some((error) => error.includes("requires structured D10")));
assert.equal(safeParseReportDocument(unsupportedCareer).ok, false);
const unsupportedWealth = cloneV2();
unsupportedWealth.blockedConflictDisclosure = unsupportedWealth.blockedConflictDisclosure.filter(
(section) => section.theme !== "wealth",
);
unsupportedWealth.thematicNarrative.push({
...structuredClone(unsupportedWealth.thematicNarrative[0]),
id: "theme-wealth",
theme: "wealth",
});
const wealthViolations = findThemeCoverageViolations(unsupportedWealth);
assert.ok(wealthViolations.some((error) => error.includes("requires structured D2")));
assert.ok(wealthViolations.some((error) => error.includes("requires structured D11")));
assert.equal(safeParseReportDocument(unsupportedWealth).ok, false);
assert.equal(safeParseReportDocument(v2Fixture).ok, true);
});
@@ -14,6 +14,7 @@ import {
type TechniqueExecutionReceipt,
} from "../src/lib/report-evidence-bundle-v2.ts";
import {
classifyFinalParseInnerReason,
classifyReportSchemaInnerReason,
generatePersonalReport,
type GeneratePersonalReportResult,
@@ -606,6 +607,29 @@ test("plan binding failure consumes the single writer repair retry", async () =>
assert.equal(observed.calls, 2);
});
test("wealth write without structured D2/D11 charts demotes to blocked instead of failing schema parse", async () => {
const bundle = makeBundle({
themes: [{
theme: "wealth",
section: "财富结构",
refs: ["ev-tech-d2", "ev-tech-d11"],
}],
charts: [chart("D1")],
});
const sectionCalls: string[] = [];
const result = await runSectioned(
bundle,
sectionedAgent({ sectionCalls }),
inMemorySectionService(),
);
const document = readyV2(result);
assert.deepEqual(sectionCalls, []);
assert.deepEqual(document.thematicNarrative.map((section) => section.theme), []);
assert.equal(document.blockedConflictDisclosure.length, 1);
assert.equal(document.blockedConflictDisclosure[0].theme, "wealth");
assert.match(document.blockedConflictDisclosure[0].reason, /D2|D11|结构化分盘/);
});
test("schema inner reasons stay on the allowlist and never include model text", () => {
assert.equal(
classifyReportSchemaInnerReason(new Error("report_writer_theme_count_mismatch")),
@@ -619,6 +643,25 @@ test("schema inner reasons stay on the allowlist and never include model text",
classifyReportSchemaInnerReason(new Error("Unexpected token in JSON at position 12")),
"schema_invalid_unclassified",
);
assert.equal(
classifyFinalParseInnerReason([
{ path: "(guard)", message: "thematic section wealth requires structured D2 chart data or a blocked disclosure" },
{ path: "(guard)", message: "thematic section wealth requires structured D11 chart data or a blocked disclosure" },
]),
"final_parse_missing_divisional_chart",
);
assert.equal(
classifyFinalParseInnerReason([
{ path: "thematicNarrative.0.title", message: "String must contain at most 160 character(s)" },
]),
"final_parse_rejected",
);
assert.equal(
classifyFinalParseInnerReason([
{ path: "provenance.evidenceHash", message: "does not match recomputed evidence hash" },
]),
"final_parse_evidence_hash",
);
});
test("accepted birth time never creates a fake candidate range", async () => {
@@ -1306,6 +1306,65 @@ test("bundle v2 builder blocks incomplete wealth evidence without leaking workfl
}
});
function wealthClosedWorkflow(): Record<string, unknown> {
const workflow = pythonStyleChartPayload() as Record<string, unknown>;
const chart = workflow.chart as Record<string, unknown>;
const modules = chart.modules as Record<string, unknown>;
const vargaFull = modules.varga_full as Record<string, unknown>;
vargaFull.D2_Hora = {
_meta: { div: 2 },
Ascendant: { sign: "Cancer", sign_idx: 3 },
Sun: { sign: "Leo", sign_idx: 4 },
Moon: { sign: "Virgo", sign_idx: 5 },
AuthorizationSecretPlanet: { sign: "Libra", sign_idx: 6 },
};
vargaFull.D11_Rudramsa = {
_meta: { div: 11 },
Ascendant: { sign: "Scorpio", sign_idx: 7 },
Sun: { sign: "Sagittarius", sign_idx: 8 },
Moon: { sign: "Capricorn", sign_idx: 9 },
};
const machine = workflow.machine_evidence_packet as Record<string, unknown>;
const sections = machine.sections as Record<string, unknown>;
sections.D2 = { status: "used", source_path: "modules.varga_full.D2" };
sections.D11 = { status: "used", source_path: "modules.varga_full.D11" };
sections.Yoga = { status: "used", source_path: "modules.yogas" };
sections.Ashtakavarga = { status: "used", source_path: "modules.ashtakavarga" };
return workflow;
}
test("bundle v2 canonicalizes D2_Hora and D11_Rudramsa into structured D2/D11 charts for wealth", () => {
const bundle = buildReportEvidenceBundleV2({
workflows: [{ theme: "wealth", workflow: wealthClosedWorkflow() }],
subject: {
displayName: "测试用户",
birthTimeStatus: "accepted",
birthPlaceLabel: "北京",
},
requestedThemes: ["wealth"],
reportType: "personal_thematic",
presentationMode: "default",
skillSnapshot: {
name: "jyotish-vedic-astrology",
version: "6.9.14",
sha256: "a".repeat(64),
sourceCommit: "b".repeat(40),
},
});
assert.ok(bundle.charts.some((chart) => chart.id === "D2"));
assert.ok(bundle.charts.some((chart) => chart.id === "D11"));
assert.equal(bundle.charts.some((chart) => chart.id.includes("Hora") || chart.id.includes("Rudramsa")), false);
const d2 = bundle.charts.find((chart) => chart.id === "D2");
assert.ok(d2);
assert.equal(d2.houses.length, 12);
assert.equal(d2.houses[0].sign, "Cancer");
assert.ok(d2.houses[1].occupants.includes("Sun"));
assert.equal(d2.houses.some((house) => house.occupants.includes("AuthorizationSecretPlanet")), false);
assert.ok(bundle.claimCards.some((card) => card.theme === "wealth"));
assert.equal(bundle.blockedSections.some((section) => section.theme === "wealth"), false);
});
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>;