Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
204 lines
14 KiB
TypeScript
204 lines
14 KiB
TypeScript
import { reportFactTablesSchema, type ReportFactTable, type ReportFactSubtable } from "./report-fact-table-schema";
|
|
import { SIGN_ORDER, SIGN_ZH } from "./chart-view-labels";
|
|
import { containsHan, type ReportLanguage } from "./report-language";
|
|
import { at, date, decimal, entries, isBlocked, list, planet, planetEn, readable, record, sign, signEn, yesNo } from "./report-fact-table-values";
|
|
|
|
const SOURCES = [
|
|
["vimshottari", "Vimshottari 主运", "timing_and_predictive_systems.dasha", "dasha", ["主运", "起", "止", "年数", "当前"]],
|
|
["ashtakavarga", "SAV 八分力", "strengths_and_scores.ashtakavarga", "ashtakavarga", ["星座", "SAV 点数"]],
|
|
["shadbala", "Shadbala 六分量", "strengths_and_scores.shadbala", "shadbala", ["行星", "位置力", "方向力", "时间力", "动态力", "自然力", "相位力", "合计"]],
|
|
["functional_roles", "功能吉凶", "strengths_and_scores.functional_benefic_malefic", "functional_benefic_malefic", ["类别", "行星"]],
|
|
["avasthas", "Avastha 行星状态", "advanced_systems.avasthas", "avasthas", ["行星", "Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"]],
|
|
["special_points", "附属星点与特殊上升点", "divisional_and_special_charts", "", ["名称", "星座", "度数"]],
|
|
["sade_sati", "Sade Sati 阶段框架", "advanced_systems.yogas_doshas.sade_sati", "yogas_doshas.sade_sati", ["阶段", "星座"]],
|
|
["annual", "年度盘", "timing_and_predictive_systems.annual_tajika_pack", "annual_tajika_pack", ["行星", "星座", "度数"]],
|
|
] as const;
|
|
const NOTE = "原始计算供核对,不增加结论的确定性。";
|
|
const EMPTY = "本次没有可展示的数据,不补算、不补写。";
|
|
|
|
/**
|
|
* English edition of every label above and below, from the same packet
|
|
* (TASK-report-english-edition-20260929). Ids and source paths are shared, so
|
|
* both editions list exactly the same rows.
|
|
*/
|
|
const EN_SOURCES: Readonly<Record<(typeof SOURCES)[number][0], readonly [string, readonly string[]]>> = {
|
|
vimshottari: ["Vimshottari mahadashas", ["Mahadasha", "Start", "End", "Years", "Current"]],
|
|
ashtakavarga: ["SAV Ashtakavarga", ["Sign", "SAV points"]],
|
|
shadbala: ["Shadbala", ["Planet", "Sthana", "Dig", "Kala", "Chesta", "Naisargika", "Drik", "Total"]],
|
|
functional_roles: ["Functional benefics and malefics", ["Group", "Planet"]],
|
|
avasthas: ["Avasthas (planetary states)", ["Planet", "Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"]],
|
|
special_points: ["Upagrahas and special lagnas", ["Name", "Sign", "Degree"]],
|
|
sade_sati: ["Sade Sati phases", ["Phase", "Sign"]],
|
|
annual: ["Annual chart", ["Planet", "Sign", "Degree"]],
|
|
};
|
|
const EN_NOTE = "Raw calculations for checking; they do not make any conclusion more certain.";
|
|
const EN_EMPTY = "Nothing to show this time; nothing is recalculated or filled in.";
|
|
|
|
type Labels = {
|
|
note: string;
|
|
empty: string;
|
|
planet: (value: unknown, chinese?: unknown) => string | null;
|
|
sign: (value: unknown, chinese?: unknown, index?: unknown) => string | null;
|
|
yesNo: (value: unknown) => string | null;
|
|
/** Free text from the engine; the English edition drops anything still in Chinese. */
|
|
text: (value: unknown) => string | null;
|
|
};
|
|
|
|
const ZH_LABELS: Labels = { note: NOTE, empty: EMPTY, planet, sign, yesNo, text: readable };
|
|
const EN_LABELS: Labels = {
|
|
note: EN_NOTE,
|
|
empty: EN_EMPTY,
|
|
planet: (value) => planetEn(value),
|
|
sign: (value, _chinese, index) => signEn(value, index),
|
|
yesNo: (value) => (typeof value === "boolean" ? (value ? "Yes" : "No") : null),
|
|
text: (value) => {
|
|
const label = readable(value);
|
|
return label && !containsHan(label) ? label : null;
|
|
},
|
|
};
|
|
const POINT_NAMES: Record<string, string> = {
|
|
Kaala: "Kaala", Mrityu: "Mrityu", Artha_Praharaka: "Artha Praharaka", Yama_Ghantaka: "Yama Ghantaka",
|
|
Gulika: "Gulika", Maandi: "Maandi", Dhuma: "Dhuma", Vyatipata: "Vyatipata", Parivesha: "Parivesha", Indrachapa: "Indrachapa", Upaketu: "Upaketu",
|
|
Bhava_Lagna: "Bhava Lagna", Hora_Lagna: "Hora Lagna", Ghati_Lagna: "Ghati Lagna", Arudha_Lagna: "Arudha Lagna", Upapada_Lagna: "Upapada Lagna", A10_Karma_Pada: "A10 Karma Pada", bhrigu_bindu: "Bhrigu Bindu",
|
|
};
|
|
type Grid = Pick<ReportFactTable, "sourcePath" | "columns" | "rows" | "note">;
|
|
const add = (grid: Grid, sourcePath: string, cells: (string | null)[]) => grid.rows.push({ sourcePath, cells });
|
|
|
|
/** Server-owned, explicit presentation columns; provenance is never a visible cell. */
|
|
export function assembleReportFactTables(packet: unknown, language: ReportLanguage = "zh"): ReportFactTable[] {
|
|
const en = language === "en";
|
|
const L = en ? EN_LABELS : ZH_LABELS;
|
|
const subtable = (id: string, title: string, sourcePath: string, columns: string[]): ReportFactSubtable => ({ id, title, sourcePath, columns, rows: [], note: L.note });
|
|
const position = (value: unknown) => {
|
|
const raw = record(value);
|
|
return [L.sign(raw.sign, raw.sign_cn, raw.sign_idx), decimal(raw.degree_in_sign ?? raw.sign_degree)] as const;
|
|
};
|
|
const root = record(packet);
|
|
const worksheets = record(root.worksheets);
|
|
const modules = record(record(root.chart).modules ?? root.modules);
|
|
const tables: ReportFactTable[] = [];
|
|
for (const [id, zhTitle, key, moduleKey, zhColumns] of SOURCES) {
|
|
const [title, columns] = en ? EN_SOURCES[id] : [zhTitle, zhColumns];
|
|
const worksheetValue = at(worksheets, key);
|
|
const rootValue = at(root, key);
|
|
const source = worksheetValue ?? rootValue ?? (moduleKey ? at(modules, moduleKey) : modules);
|
|
if (!Object.keys(record(source)).length) continue;
|
|
const sourcePath = worksheetValue != null ? `worksheets.${key}` : rootValue != null ? key
|
|
: `${root.chart ? "chart." : ""}modules${moduleKey ? `.${moduleKey}` : ""}`;
|
|
const raw = record(source);
|
|
const declared = raw.claimStatus ?? raw.claim_status;
|
|
const claimStatus = isBlocked(raw) ? "blocked"
|
|
: declared === "unclosed_divisional_chart" || declared === "user_history_verification_required"
|
|
|| declared === "single_system_inference" || declared === "multi_system_consensus" ? declared : "parameter_sensitive";
|
|
const table: ReportFactTable = { id, title, claimStatus, sourcePath, note: L.note, columns: [...columns], rows: [] };
|
|
const children: ReportFactSubtable[] = [];
|
|
if (claimStatus !== "blocked") {
|
|
if (id === "vimshottari") {
|
|
list(raw.timeline).forEach((item, index) => {
|
|
const period = record(item);
|
|
const path = `${sourcePath}.timeline[${index}]`;
|
|
if (!Object.keys(period).length || isBlocked(period)) return;
|
|
// `years` can mean the birth balance. Never infer the full duration from dates.
|
|
add(table, path, [L.planet(period.lord, period.lord_cn), date(period.start), date(period.end), decimal(period.full_years), L.yesNo(period.is_current)]);
|
|
if (period.is_balance === true && decimal(period.balance_years) !== null) {
|
|
table.note += en
|
|
? ` At birth ${L.planet(period.lord) ?? "the first mahadasha"} had ${decimal(period.balance_years)} years left.`
|
|
: ` 出生时${planet(period.lord, period.lord_cn) ?? "首段主运"}剩余 ${decimal(period.balance_years)} 年。`;
|
|
}
|
|
if (period.is_current !== true) return;
|
|
const ad = en ? subtable("antardasha", "Antardashas in the current mahadasha", path, ["Antardasha", "Start", "End", "Current"])
|
|
: subtable("antardasha", "当前主运下分运", path, ["分运", "起", "止", "当前"]);
|
|
list(period.antardasha_timeline).forEach((entry, ai) => {
|
|
const part = record(entry);
|
|
const partPath = `${path}.antardasha_timeline[${ai}]`;
|
|
if (!Object.keys(part).length || isBlocked(part)) return;
|
|
add(ad, partPath, [L.planet(part.lord, part.lord_cn), date(part.start), date(part.end), L.yesNo(part.is_current)]);
|
|
if (part.is_current !== true) return;
|
|
const pd = en ? subtable("pratyantar", "Pratyantardashas in the current antardasha", partPath, ["Pratyantardasha", "Start", "End", "Current"])
|
|
: subtable("pratyantar", "当前分运下小运", partPath, ["小运", "起", "止", "当前"]);
|
|
list(part.pratyantar_dasha_timeline).forEach((value, pi) => {
|
|
const small = record(value);
|
|
if (Object.keys(small).length && !isBlocked(small)) add(pd, `${partPath}.pratyantar_dasha_timeline[${pi}]`, [L.planet(small.lord, small.lord_cn), date(small.start), date(small.end), L.yesNo(small.is_current)]);
|
|
});
|
|
children.push(pd);
|
|
});
|
|
children.unshift(ad);
|
|
});
|
|
} else if (id === "ashtakavarga") {
|
|
for (const [name, score] of entries(at(raw, "sav.scores"))) {
|
|
const label = L.sign(name);
|
|
if (label) add(table, `${sourcePath}.sav.scores.${name}`, [label, decimal(score)]);
|
|
}
|
|
const bav = en ? subtable("bav", "BAV Ashtakavarga", `${sourcePath}.bav`, ["Planet", ...SIGN_ORDER])
|
|
: subtable("bav", "BAV 八分力", `${sourcePath}.bav`, ["行星", ...SIGN_ORDER.map(name => SIGN_ZH[name])]);
|
|
for (const [name, value] of entries(raw.bav)) {
|
|
if (!planet(name) || isBlocked(value)) continue;
|
|
const bindus = list(record(value).bindus);
|
|
add(bav, `${sourcePath}.bav.${name}`, [L.planet(name), ...SIGN_ORDER.map((_, index) => decimal(bindus[index]))]);
|
|
}
|
|
if (raw.bav != null) children.push(bav);
|
|
} else if (id === "shadbala") {
|
|
for (const [name, value] of entries(raw.planets)) {
|
|
if (!planet(name) || isBlocked(value)) continue;
|
|
add(table, `${sourcePath}.planets.${name}`, [L.planet(name), ...["sthana_bala.total", "dig_bala", "kala_bala.total", "chesta_bala", "naisargika_bala", "drik_bala", "total_virupas"].map(field => decimal(at(value, field)))]);
|
|
}
|
|
} else if (id === "functional_roles") {
|
|
const buckets = en
|
|
? [["functional_benefics", "Functional benefic"], ["functional_malefics", "Functional malefic"], ["functional_neutrals", "Neutral"], ["yogakarakas", "Yogakaraka"]]
|
|
: [["functional_benefics", "功能吉星"], ["functional_malefics", "功能凶星"], ["functional_neutrals", "中性"], ["yogakarakas", "Yogakaraka"]];
|
|
for (const [bucket, label] of buckets) {
|
|
list(raw[bucket]).forEach((name, index) => {
|
|
if (planet(name)) add(table, `${sourcePath}.${bucket}[${index}]`, [label, L.planet(name)]);
|
|
});
|
|
}
|
|
} else if (id === "avasthas") {
|
|
for (const [name, value] of entries(raw)) {
|
|
if (!planet(name) || isBlocked(value)) continue;
|
|
add(table, `${sourcePath}.${name}`, [L.planet(name, record(value).planet_cn), ...["Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"].map(kind => L.text(at(value, `avasthas.${kind}.state`)))]);
|
|
}
|
|
} else if (id === "special_points") {
|
|
for (const kind of ["upagrahas.raw", "special_lagnas", "sensitive_points"]) {
|
|
if (isBlocked(at(source, kind.split(".")[0]))) continue;
|
|
for (const [name, value] of entries(at(source, kind))) {
|
|
if (POINT_NAMES[name] && !isBlocked(value)) add(table, `${sourcePath}.${kind}.${name}`, [POINT_NAMES[name], ...position(value)]);
|
|
if (name === "sarpa_drekkana") for (const [body, point] of entries(value)) {
|
|
if (planet(body) && !isBlocked(point)) add(table, `${sourcePath}.${kind}.${name}.${body}`, [`Sarpa Drekkana · ${L.planet(body)}`, ...position(point)]);
|
|
}
|
|
}
|
|
}
|
|
} else if (id === "sade_sati") {
|
|
const phases = en ? [["rising", "Rising"], ["peak", "Peak"], ["setting", "Setting"]] : [["rising", "上升"], ["peak", "高峰"], ["setting", "下降"]];
|
|
for (const [phase, label] of phases) {
|
|
if (at(raw, `phases.${phase}`) !== undefined) add(table, `${sourcePath}.phases.${phase}`, [label, L.sign(at(raw, `phases.${phase}`))]);
|
|
}
|
|
table.note = en
|
|
? `In progress now: ${L.yesNo(raw.is_active) ?? "not returned this time"}. Only the phase frame returned by this calculation is listed; there are no start and end dates for the three phases yet, so no timing can be read from it.`
|
|
: `当前是否在进行中:${yesNo(raw.is_active) ?? "本次未返回"}。这里只列本次计算返回的阶段框架;尚无三轮起止日期,不能据此判断时间。`;
|
|
} else if (id === "annual") {
|
|
const annual = record(at(raw, "annual_chart.data"));
|
|
if (!isBlocked(raw.annual_chart)) {
|
|
table.note += en
|
|
? ` Annual ascendant: ${L.sign(annual.asc_sign, annual.asc_sign_cn, annual.asc_sign_idx) ?? "not returned this time"}; age: ${decimal(annual.age) ?? "not returned this time"}.`
|
|
: ` 年度上升:${sign(annual.asc_sign, annual.asc_sign_cn, annual.asc_sign_idx) ?? "本次未返回"};年龄:${decimal(annual.age) ?? "本次未返回"}。`;
|
|
for (const [name, value] of entries(annual.planets)) {
|
|
if (planet(name) && !isBlocked(value)) add(table, `${sourcePath}.annual_chart.data.planets.${name}`, [L.planet(name, record(value).name_cn), ...position(value)]);
|
|
}
|
|
}
|
|
if (!table.rows.length) table.note += en ? " Annual planet positions were not returned this time; nothing is recalculated." : " 本次未返回年度行星位置,不补算。";
|
|
// Annual Sahams only: do not relabel natal advanced_systems.sahams as annual facts.
|
|
const saham = en ? subtable("sahams", "Annual Sahams", `${sourcePath}.sahams.data`, ["Name", "Sign", "Degree"])
|
|
: subtable("sahams", "年度 Saham", `${sourcePath}.sahams.data`, ["名称", "星座", "度数"]);
|
|
if (!isBlocked(raw.sahams)) for (const [name, value] of entries(at(raw, "sahams.data"))) {
|
|
const label = L.text(record(value).name) ?? (en ? `${name.replace(/_saham$/, "").replace(/_/g, " ")} Saham` : null);
|
|
if (name.endsWith("_saham") && label && !isBlocked(value)) add(saham, `${saham.sourcePath}.${name}`, [label, ...position(value)]);
|
|
}
|
|
if (at(raw, "sahams.data") != null) children.push(saham);
|
|
}
|
|
}
|
|
if (children.length) table.subtables = children.map(child => ({ ...child, note: child.rows.length ? child.note : L.empty }));
|
|
if (claimStatus === "blocked" || (!table.rows.length && !children.length && id !== "sade_sati" && id !== "annual")) table.note = L.empty;
|
|
tables.push(table);
|
|
}
|
|
return reportFactTablesSchema.parse(tables);
|
|
}
|