Files
Jyotisha/frontend/tests/report-fact-tables.test.ts
T
Jesse_ChenandClaude Opus 5.5 ea78804b78 feat(ashtakavarga): standard Sodhita chain on reports and cards, raw values beside (T7, BUG-1209)
Astrologer ruling 乙8. New scripts/ashtakavarga_shodhana.py: per BAV row Trikona then
Ekadhipatya (the two existing ashtakavarga functions, previously only called by
tests), Sodhita SAV = sum of reduced rows; occupied signs = seven grahas. Removed
calc_sodhita_av (Sun/Mars/Saturn contribution removal labelled 'BPHS标准').
Wired into /api/ashtakavarga (result.sodhita, house_scores[*].sav_sodhita) and the
full reading. Reader: SAV table 'Sodhita SAV | Raw SAV', house table gains
Sodhita column, Sodhita BAV table before raw BAV; reference edition section
rewritten. Card: sav_sodhita before sav_score plus sav_columns label. Personal
report facts: score = Sodhita, rawScore/savRawTotal beside (optional schema
fields). Fact tables: Sodhita column and Sodhita BAV subtable. Transit scoring
unchanged (raw bindus).

Golden: report-density-fictional-engine.json leaf worksheets.strengths_and_scores
.ashtakavarga recaptured from the real engine (scripts/research/refresh_density_
fixture_ashtakavarga.py); bav/sav identical, every other leaf byte-equal.
Changed assertions (three columns in PROGRESS): BUG-1200 house-table tests,
report-fact-tables ashtakavarga columns/subtables, run_all t1/t57.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 12:41:26 +08:00

191 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { assembleReportFactTables } from "../src/lib/report-fact-tables";
import { reportFactTablesSchema } from "../src/lib/report-fact-table-schema";
import { assembleLongformCharts } from "../src/lib/personal-report-generation";
import { buildLongformCoverDocument } from "../src/lib/personal-report-longform-cover";
import { CHART_IDS, reportDocumentV2Schema } from "../src/lib/personal-report-contract";
import { projectOrdinaryReportMarkdown } from "../src/lib/report-public-projection";
const packet = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8"));
const resolve = (path: string) => path.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((value, key) => value?.[key], packet);
const fixed = (value: number) => value.toFixed(2);
test("density golden: sixteen real charts have complete houses and nine occupants", () => {
const charts = assembleLongformCharts(packet, "ev-cover-longform");
assert.deepEqual(charts.map(chart => chart.id), [...CHART_IDS]);
assert.equal(reportDocumentV2Schema.shape.charts.safeParse(charts).success, true);
const document = buildLongformCoverDocument({
report: { id: "123e4567-e89b-12d3-a456-426614174000", reportType: "personal_full", presentationMode: "default", depth: "standard", requestedThemes: ["career"], skillName: "jyotish-vedic-astrology", skillVersion: "6.9.14", skillSourceCommit: null, skillSnapshotSha256: "ab".repeat(32) },
subject: { displayName: "虚构测试", birthTimeStatus: "reported", birthPlaceLabel: "虚构地点" },
markdown: "# 个人报告\n\n## 摘要\n\n原始计算供核对。", charts, factTables: assembleReportFactTables(packet),
});
assert.equal(document.factTables?.length, 8);
assert.equal(document.charts.length, 16);
for (const chart of charts) {
assert.equal(chart.houses.length, 12);
assert.equal(chart.houses.flatMap(h => h.occupants).length, 9);
assert.equal(chart.claimStatus, "parameter_sensitive");
}
});
test("density applicability remains outside ordinary narrative projection", () => {
const prose = "# 个人报告\n\n这是普通正文。\n";
const appendix = "\n## 辅助时间轴适用性(参数敏感)\n\n| 大运族 | 是否适用 / 原因 | 使用边界 |\n| --- | --- | --- |\n| Yogini | 已返回周期 | 第二时间轴,仅供交叉核对,不单独生成应期 |\n";
assert.equal(projectOrdinaryReportMarkdown(prose + appendix), projectOrdinaryReportMarkdown(prose + "\n## 核对(参数敏感)\n"));
assert.doesNotMatch(projectOrdinaryReportMarkdown(prose + appendix), /Yogini|第二时间轴/);
});
test("density golden: eight server groups contain only unchanged engine leaves", () => {
assert.equal(packet.fixtureProvenance.fictional, true);
const tables = assembleReportFactTables(packet);
assert.equal(tables.length, 8);
for (const table of tables) {
assert.ok(table.rows.length > 0 || table.subtables?.some(child => child.rows.length > 0), table.id);
for (const grid of [table, ...(table.subtables ?? [])]) {
assert.notEqual(resolve(grid.sourcePath), undefined, grid.sourcePath);
for (const row of grid.rows) assert.notEqual(resolve(row.sourcePath), undefined, row.sourcePath);
}
}
const shadbala = tables.find(t => t.id === "shadbala")!;
assert.equal(shadbala.rows.length, 7);
for (const row of shadbala.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells.slice(1), [raw.sthana_bala.total, raw.dig_bala, raw.kala_bala.total, raw.chesta_bala, raw.naisargika_bala, raw.drik_bala, raw.total_virupas].map(fixed));
}
const ashtakavarga = tables.find(t => t.id === "ashtakavarga")!;
assert.equal(ashtakavarga.rows.length, 12);
// BUG-1209 (ruling 乙8): Sodhita SAV first, raw SAV beside it; the reduced BAV
// subtable comes before the raw BAV. Was: cells[1] = raw SAV and subtables[0] = raw BAV.
const sodhita = resolve("worksheets.strengths_and_scores.ashtakavarga.sodhita");
assert.equal(sodhita.status, "executed");
assert.deepEqual(ashtakavarga.columns, ["星座", "净化后 SAV", "原始 SAV"]);
for (const row of ashtakavarga.rows) {
const sign = row.sourcePath.split(".").at(-1)!;
assert.equal(row.cells[1], fixed(sodhita.sodhita_sav.scores[sign]));
assert.equal(row.cells[2], resolve(row.sourcePath).toFixed(2));
}
assert.equal(ashtakavarga.subtables?.[0].id, "sodhita_bav");
assert.equal(ashtakavarga.subtables?.[0].rows.length, 7);
for (const row of ashtakavarga.subtables![0].rows) assert.deepEqual(row.cells.slice(1), resolve(row.sourcePath).map(fixed));
assert.equal(ashtakavarga.subtables?.[1].rows.length, 8);
for (const row of ashtakavarga.subtables![1].rows) assert.deepEqual(row.cells.slice(1), resolve(row.sourcePath).bindus.map(fixed));
const annual = tables.find(t => t.id === "annual")!;
assert.equal(annual.rows.length, 0, "golden annual chart has no planet positions");
assert.ok(annual.subtables?.[0].rows.every(row => row.sourcePath.includes("annual_tajika_pack.sahams.data")));
assert.equal(annual.subtables?.[0].rows.length, 36);
});
test("density golden: Sade Sati reports missing cycle dates without inventing them", () => {
const table = assembleReportFactTables(packet).find(t => t.id === "sade_sati")!;
assert.match(table.note, /尚无三轮起止日期/);
assert.equal(table.rows.length, 3);
assert.deepEqual(table.rows.map(row => row.cells[0]), ["上升", "高峰", "下降"]);
assert.match(table.note, /当前是否在进行中:否/);
assert.equal(table.claimStatus, "parameter_sensitive");
assert.ok(table.rows.every(row => !/start|end|date/.test(row.sourcePath)));
});
test("density facts preserve declared limitations and reject malformed provenance", () => {
const altered = structuredClone(packet);
altered.worksheets.advanced_systems.yogas_doshas.sade_sati.claimStatus = "user_history_verification_required";
assert.equal(assembleReportFactTables(altered).find(t => t.id === "sade_sati")?.claimStatus, "user_history_verification_required");
const tables = assembleReportFactTables(packet);
tables[0].rows[0].sourcePath = tables[0].sourcePath + "_wrong.lord";
assert.equal(reportFactTablesSchema.safeParse(tables).success, false);
assert.deepEqual(assembleReportFactTables(undefined), []);
});
test("density visible columns never expose engine paths or excessive precision", () => {
for (const table of assembleReportFactTables(packet)) {
for (const grid of [table, ...(table.subtables ?? [])]) {
const visible = [grid.title, grid.note, ...grid.columns, ...grid.rows.flatMap(row => row.cells)].join("\n");
assert.doesNotMatch(visible, /[A-Za-z]+_[A-Za-z]+|\[\d+\]|\d+\.\d{3,}|jd_ut|dt_ut|\.data\./);
assert.ok(grid.rows.every(row => row.cells.every(cell => cell === null || typeof cell === "string")));
}
}
});
test("density golden: full years and birth balance have distinct sourced meanings", () => {
const table = assembleReportFactTables(packet).find(t => t.id === "vimshottari")!;
assert.equal(table.rows.length, 9);
assert.deepEqual(table.columns, ["主运", "起", "止", "年数", "当前"]);
assert.equal(resolve(table.rows[0].sourcePath).full_years, 18);
assert.equal(resolve(table.rows[0].sourcePath).balance_years, 0.72);
assert.equal(table.rows[0].cells[3], "18.00");
assert.match(table.note, /出生时.*剩余 0\.72 年/);
for (const row of table.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells, [raw.lord_cn, raw.start, raw.end, fixed(raw.full_years), raw.is_current ? "是" : "否"]);
}
assert.deepEqual(table.subtables?.map(child => child.id), ["antardasha", "pratyantar"]);
for (const child of table.subtables!) {
assert.equal(resolve(child.sourcePath).is_current, true);
assert.equal(child.rows.length, 9);
for (const row of child.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells, [raw.lord_cn, raw.start, raw.end, raw.is_current ? "是" : "否"]);
}
}
const altered = structuredClone(packet);
delete altered.worksheets.timing_and_predictive_systems.dasha.timeline[0].full_years;
assert.equal(assembleReportFactTables(altered).find(t => t.id === "vimshottari")!.rows[0].cells[3], null, "neither dates nor balance nor standard constants may supply missing duration");
});
test("density golden: state and position cells retain actual source values", () => {
const tables = assembleReportFactTables(packet);
const states = tables.find(t => t.id === "avasthas")!;
for (const row of states.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells.slice(1), ["Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"].map(key => raw.avasthas[key].state));
}
for (const grid of [tables.find(t => t.id === "special_points")!, ...tables.find(t => t.id === "annual")!.subtables!]) {
for (const row of grid.rows) {
const raw = resolve(row.sourcePath);
assert.equal(row.cells[2], fixed(raw.degree_in_sign ?? raw.sign_degree));
}
}
const altered = structuredClone(packet);
const strengths = altered.worksheets.strengths_and_scores;
delete strengths.shadbala.planets.Sun.total_virupas;
delete altered.worksheets.divisional_and_special_charts.upagrahas.raw.Kaala.degree_in_sign;
const output = assembleReportFactTables(altered);
assert.equal(output.find(t => t.id === "shadbala")!.rows[0].cells[7], null, "do not sum components");
assert.equal(output.find(t => t.id === "special_points")!.rows[0].cells[2], null, "do not derive sign degrees from longitude");
});
test("density fact subtable schema accepts legacy snapshots and rejects malformed new grids", () => {
const tables = assembleReportFactTables(packet);
const legacy = tables.map(source => {
const table = { ...source };
delete table.subtables;
return { ...table, columns: ["原始字段", "数值 / 状态"], rows: [{ sourcePath: table.sourcePath, cells: ["legacy", 1] }] };
});
assert.equal(reportFactTablesSchema.safeParse(legacy).success, true);
const invalid = structuredClone(tables);
invalid[0].subtables![0].rows[0].cells.pop();
assert.equal(reportFactTablesSchema.safeParse(invalid).success, false);
const wrongSource = structuredClone(tables);
wrongSource[0].subtables![0].sourcePath = "worksheets.other";
assert.equal(reportFactTablesSchema.safeParse(wrongSource).success, false);
const duplicates = structuredClone(tables);
duplicates[0].subtables!.push(duplicates[0].subtables![0]);
assert.equal(reportFactTablesSchema.safeParse(duplicates).success, false);
});
test("density blocked sources stay blocked and missing annual positions are not invented", () => {
const altered = structuredClone(packet);
altered.worksheets.strengths_and_scores.shadbala.claimStatus = "blocked";
altered.worksheets.timing_and_predictive_systems.annual_tajika_pack.sahams.status = "blocked";
const tables = assembleReportFactTables(altered);
assert.equal(tables.find(t => t.id === "shadbala")!.claimStatus, "blocked");
assert.deepEqual(tables.find(t => t.id === "shadbala")!.rows, []);
assert.deepEqual(tables.find(t => t.id === "annual")!.rows, []);
assert.deepEqual(tables.find(t => t.id === "annual")!.subtables![0].rows, []);
assert.match(tables.find(t => t.id === "annual")!.note, /年度上升:天蝎;年龄:26\.00/);
assert.match(tables.find(t => t.id === "annual")!.note, /未返回年度行星位置,不补算/);
});