import { reportFactTablesSchema, type ReportFactTable, type ReportFactSubtable } from "./report-fact-table-schema"; import { SIGN_ORDER, SIGN_ZH } from "./chart-view-labels"; import { at, date, decimal, entries, isBlocked, list, planet, position, readable, record, sign, 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 = "本次没有可展示的数据,不补算、不补写。"; const POINT_NAMES: Record = { 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; const add = (grid: Grid, sourcePath: string, cells: (string | null)[]) => grid.rows.push({ sourcePath, cells }); const subtable = (id: string, title: string, sourcePath: string, columns: string[]): ReportFactSubtable => ({ id, title, sourcePath, columns, rows: [], note: NOTE }); /** Server-owned, explicit presentation columns; provenance is never a visible cell. */ export function assembleReportFactTables(packet: unknown): ReportFactTable[] { const root = record(packet); const worksheets = record(root.worksheets); const modules = record(record(root.chart).modules ?? root.modules); const tables: ReportFactTable[] = []; for (const [id, title, key, moduleKey, columns] of SOURCES) { 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: 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, [planet(period.lord, period.lord_cn), date(period.start), date(period.end), decimal(period.full_years), yesNo(period.is_current)]); if (period.is_balance === true && decimal(period.balance_years) !== null) table.note += ` 出生时${planet(period.lord, period.lord_cn) ?? "首段主运"}剩余 ${decimal(period.balance_years)} 年。`; if (period.is_current !== true) return; const ad = 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, [planet(part.lord, part.lord_cn), date(part.start), date(part.end), yesNo(part.is_current)]); if (part.is_current !== true) return; const pd = 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}]`, [planet(small.lord, small.lord_cn), date(small.start), date(small.end), 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 = sign(name); if (label) add(table, `${sourcePath}.sav.scores.${name}`, [label, decimal(score)]); } const bav = 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}`, [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}`, [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") { for (const [bucket, label] of [["functional_benefics", "功能吉星"], ["functional_malefics", "功能凶星"], ["functional_neutrals", "中性"], ["yogakarakas", "Yogakaraka"]]) { list(raw[bucket]).forEach((name, index) => { if (planet(name)) add(table, `${sourcePath}.${bucket}[${index}]`, [label, planet(name)]); }); } } else if (id === "avasthas") { for (const [name, value] of entries(raw)) { if (!planet(name) || isBlocked(value)) continue; add(table, `${sourcePath}.${name}`, [planet(name, record(value).planet_cn), ...["Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"].map(kind => readable(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 · ${planet(body)}`, ...position(point)]); } } } } else if (id === "sade_sati") { for (const [phase, label] of [["rising", "上升"], ["peak", "高峰"], ["setting", "下降"]]) { if (at(raw, `phases.${phase}`) !== undefined) add(table, `${sourcePath}.phases.${phase}`, [label, sign(at(raw, `phases.${phase}`))]); } table.note = `当前是否在进行中:${yesNo(raw.is_active) ?? "本次未返回"}。这里只列本次计算返回的阶段框架;尚无三轮起止日期,不能据此判断时间。`; } else if (id === "annual") { const annual = record(at(raw, "annual_chart.data")); if (!isBlocked(raw.annual_chart)) { table.note += ` 年度上升:${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}`, [planet(name, record(value).name_cn), ...position(value)]); } } if (!table.rows.length) table.note += " 本次未返回年度行星位置,不补算。"; // Annual Sahams only: do not relabel natal advanced_systems.sahams as annual facts. const saham = subtable("sahams", "年度 Saham", `${sourcePath}.sahams.data`, ["名称", "星座", "度数"]); if (!isBlocked(raw.sahams)) for (const [name, value] of entries(at(raw, "sahams.data"))) { const label = readable(record(value).name); 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 : EMPTY })); if (claimStatus === "blocked" || (!table.rows.length && !children.length && id !== "sade_sati" && id !== "annual")) table.note = EMPTY; tables.push(table); } return reportFactTablesSchema.parse(tables); }