fix(report): extract D2/D11 varga aliases so wealth chapters pass final parse
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:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user