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:55:06 +08:00
co-authored by Cursor
parent 661a0dab14
commit 45f190a1d0
7 changed files with 318 additions and 75 deletions
@@ -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>;