fix(report): make density facts readable and printable
Unify reader cleanup rules, lock writer table guards, and register the exact fictional timestamp collision. Preserve existing ordinary-report safety contracts and source-data gaps. Validation: report Node 165/165, final safety 29/29, Python 101/101, Chrome 28/28; both PDFs retain all 130 rows. Full Node 3704 tests with the same 91 baseline failures. Privacy test: 62 passed, 1 failed due to 17 protected-file READ_ERRORs; not a green gate. Build, DB, manual checklist and controlled-login gaps remain documented. User explicitly authorized staging push with these gaps disclosed. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,32 @@
|
||||
/** Cache compatibility for the Python reader-only vocabulary projection. */
|
||||
const STATUS_LABELS: Readonly<Record<string, string>> = {
|
||||
parameter_sensitive: "参数敏感",
|
||||
pyjhora_behavior_only: "仅单一外部参照,未做多引擎核对",
|
||||
not_multiengine_parity: "仅单一外部参照,未做多引擎核对",
|
||||
unresolved_external_tuple_boundary: "外部边界未对齐",
|
||||
partial_verified: "部分核验",
|
||||
raw_appendix_only: "仅原始附录可见",
|
||||
missing_in_local: "本地暂无",
|
||||
internal_reference_omitted: "内部参照已省略",
|
||||
};
|
||||
const CELL_LABELS: Readonly<Record<string, string>> = {
|
||||
blocked: "暂不可用", executed: "已执行", available: "可用", computed: "已计算",
|
||||
};
|
||||
import vocabulary from "../../../scripts/reader_appendix_language.rules.json";
|
||||
|
||||
type VocabularyRule = Readonly<{
|
||||
pattern: string;
|
||||
ignoreCase?: boolean;
|
||||
replacement?: string;
|
||||
labels?: Readonly<Record<string, string>>;
|
||||
labelGroup?: number;
|
||||
prefix?: string;
|
||||
beforeGroups?: readonly number[];
|
||||
afterGroups?: readonly number[];
|
||||
}>;
|
||||
|
||||
// One rule source for Python and TS; the JSON ships in both existing images.
|
||||
const chartFence = new RegExp(vocabulary.chartFencePattern, "m");
|
||||
const replacements = (vocabulary.rules as readonly VocabularyRule[]).map((rule) => ({
|
||||
rule,
|
||||
pattern: new RegExp(rule.pattern, rule.ignoreCase ? "gi" : "g"),
|
||||
}));
|
||||
|
||||
export function cleanReaderAppendixMarkdown(markdown: string): string {
|
||||
return markdown.split(/(^[ \t]*```jyotish-chart[^\n]*\n[\s\S]*?^[ \t]*```[^\n]*(?:\n|$))/m)
|
||||
.map((part, index) => index % 2 ? part : part
|
||||
.replace(new RegExp(`\\b(${Object.keys(STATUS_LABELS).join("|")})\\b`, "g"), (value) => STATUS_LABELS[value])
|
||||
.replace(/\b(?:jyotish_engine\.)?(?:cmd_[a-z0-9_]+|render_pl9_markdown|build_professional_report_reference_packet)\b/g, "本地计算")
|
||||
.replace(/\b(?:[a-z0-9_]+pl9[a-z0-9_]*|pl9_[a-z0-9_]+)(?:\.v\d+)?\b/gi, "外部参照资料")
|
||||
.replace(/\bPL9\b(?:\.pdf)?(?:[ \t]*(?:第[ \t]*\d+(?:[ \t]*[–—/-][ \t]*\d+)*[ \t]*页|(?:pages?|p)[ \t-]*\d+(?:[ \t]*[–—/-][ \t]*\d+)*))?/gi, "外部参照资料")
|
||||
.replace(/\b(?:PyJHora|JHora)\b/gi, "外部参照引擎")
|
||||
.replace(/\|([ \t]*)(`?)(blocked|executed|available|computed)\2([ \t]*)(?=\|)/g,
|
||||
(_match, before: string, quote: string, value: string, after: string) => `|${before}${quote}${CELL_LABELS[value]}${quote}${after}`))
|
||||
.join("");
|
||||
return markdown.split(chartFence).map((part, index) => {
|
||||
if (index % 2) return part;
|
||||
return replacements.reduce((value, { pattern, rule }) => value.replace(pattern, (...match) => {
|
||||
if (rule.replacement !== undefined) return rule.replacement;
|
||||
return (rule.prefix ?? "")
|
||||
+ (rule.beforeGroups ?? []).map((group) => match[group]).join("")
|
||||
+ rule.labels![match[rule.labelGroup!]]
|
||||
+ (rule.afterGroups ?? []).map((group) => match[group]).join("");
|
||||
}), part);
|
||||
}).join("");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Expand only this report's disclosures; restore the reader's state after print/cancel. */
|
||||
export function bindFactTablePrint(root: HTMLElement, target: Pick<Window, "addEventListener" | "removeEventListener">): () => void {
|
||||
const states = new Map<HTMLDetailsElement, boolean>();
|
||||
const beforePrint = () => {
|
||||
root.querySelectorAll<HTMLDetailsElement>("details").forEach((details) => {
|
||||
if (!states.has(details)) states.set(details, details.open);
|
||||
details.open = true;
|
||||
});
|
||||
};
|
||||
const afterPrint = () => {
|
||||
states.forEach((open, details) => { details.open = open; });
|
||||
states.clear();
|
||||
};
|
||||
target.addEventListener("beforeprint", beforePrint);
|
||||
target.addEventListener("afterprint", afterPrint);
|
||||
return () => {
|
||||
target.removeEventListener("beforeprint", beforePrint);
|
||||
target.removeEventListener("afterprint", afterPrint);
|
||||
afterPrint();
|
||||
};
|
||||
}
|
||||
@@ -2,12 +2,7 @@ import { z } from "zod";
|
||||
|
||||
export const FACT_TABLE_GROUPS = ["vimshottari", "ashtakavarga", "shadbala", "functional_roles", "avasthas", "special_points", "sade_sati", "annual"] as const;
|
||||
const sourcePath = z.string().min(1).max(400).regex(/^[A-Za-z0-9_.\[\]-]+$/);
|
||||
|
||||
/** Immutable server facts; never part of a writer output schema. */
|
||||
export const reportFactTableSchema = z.object({
|
||||
id: z.enum(FACT_TABLE_GROUPS),
|
||||
title: z.string().min(1).max(120),
|
||||
claimStatus: z.enum(["multi_system_consensus", "single_system_inference", "parameter_sensitive", "unclosed_divisional_chart", "user_history_verification_required", "blocked"]),
|
||||
const gridShape = {
|
||||
sourcePath,
|
||||
note: z.string().max(500),
|
||||
columns: z.array(z.string().min(1).max(80)).min(1).max(16),
|
||||
@@ -15,13 +10,37 @@ export const reportFactTableSchema = z.object({
|
||||
sourcePath,
|
||||
cells: z.array(z.union([z.string().max(300), z.number().finite(), z.boolean(), z.null()])).min(1).max(16),
|
||||
}).strict()).max(2000),
|
||||
}).strict().superRefine((table, context) => {
|
||||
};
|
||||
const within = (child: string, parent: string) => child === parent || child.startsWith(`${parent}.`) || child.startsWith(`${parent}[`);
|
||||
function validateRows(table: { sourcePath: string; columns: string[]; rows: { sourcePath: string; cells: unknown[] }[] }, context: z.RefinementCtx) {
|
||||
table.rows.forEach((row, index) => {
|
||||
if (row.cells.length !== table.columns.length) context.addIssue({ code: "custom", path: ["rows", index], message: "fact_table_cell_count" });
|
||||
if (row.sourcePath !== table.sourcePath && !row.sourcePath.startsWith(`${table.sourcePath}.`) && !row.sourcePath.startsWith(`${table.sourcePath}[`)) context.addIssue({ code: "custom", path: ["rows", index, "sourcePath"], message: "fact_table_source_mismatch" });
|
||||
if (!within(row.sourcePath, table.sourcePath)) context.addIssue({ code: "custom", path: ["rows", index, "sourcePath"], message: "fact_table_source_mismatch" });
|
||||
});
|
||||
}
|
||||
export const reportFactSubtableSchema = z.object({
|
||||
id: z.string().min(1).max(80).regex(/^[a-z][a-z0-9_-]*$/),
|
||||
title: z.string().min(1).max(120),
|
||||
...gridShape,
|
||||
}).strict().superRefine(validateRows);
|
||||
|
||||
/** Optional subtables are additive: persisted v2 snapshots keep their original rows. */
|
||||
export const reportFactTableSchema = z.object({
|
||||
id: z.enum(FACT_TABLE_GROUPS),
|
||||
title: z.string().min(1).max(120),
|
||||
claimStatus: z.enum(["multi_system_consensus", "single_system_inference", "parameter_sensitive", "unclosed_divisional_chart", "user_history_verification_required", "blocked"]),
|
||||
...gridShape,
|
||||
subtables: z.array(reportFactSubtableSchema).max(8).optional(),
|
||||
}).strict().superRefine((table, context) => {
|
||||
validateRows(table, context);
|
||||
const subtables = table.subtables ?? [];
|
||||
if (new Set(subtables.map(subtable => subtable.id)).size !== subtables.length) context.addIssue({ code: "custom", path: ["subtables"], message: "duplicate_fact_subtable" });
|
||||
subtables.forEach((subtable, index) => {
|
||||
if (!within(subtable.sourcePath, table.sourcePath)) context.addIssue({ code: "custom", path: ["subtables", index, "sourcePath"], message: "fact_table_source_mismatch" });
|
||||
});
|
||||
});
|
||||
export const reportFactTablesSchema = z.array(reportFactTableSchema).max(8).superRefine((tables, context) => {
|
||||
if (new Set(tables.map((table) => table.id)).size !== tables.length) context.addIssue({ code: "custom", message: "duplicate_fact_table" });
|
||||
});
|
||||
export type ReportFactTable = z.infer<typeof reportFactTableSchema>;
|
||||
export type ReportFactSubtable = z.infer<typeof reportFactSubtableSchema>;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { findForbiddenContent } from "./personal-report-contract";
|
||||
import { PLANET_ZH, SIGN_ORDER, SIGN_ZH, type PlanetName, type SignName } from "./chart-view-labels";
|
||||
|
||||
export const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
export const at = (value: unknown, path: string): unknown => path.split(".").reduce((next, key) => record(next)[key], value);
|
||||
export const entries = (value: unknown) => Object.entries(record(value)).filter(([key]) => /^[A-Za-z0-9_-]+$/.test(key));
|
||||
export const list = (value: unknown): unknown[] => Array.isArray(value) ? value : [];
|
||||
export const decimal = (value: unknown): string | null => typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : null;
|
||||
export const yesNo = (value: unknown): string | null => typeof value === "boolean" ? value ? "是" : "否" : null;
|
||||
/** Display-only allowlist: unknown/internal labels never become prose. */
|
||||
export const readable = (value: unknown): string | null => typeof value === "string" && value.length > 0 && value.length <= 300
|
||||
&& !/[_<>\[\]{}\/\\]|https?:|\b\w+\.\w+\b/.test(value) && !findForbiddenContent(value).length ? value : null;
|
||||
export const date = (value: unknown): string | null => typeof value === "string" && /^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value) ? value.slice(0, 10) : null;
|
||||
export function planet(value: unknown, chinese?: unknown): string | null {
|
||||
const label = readable(chinese);
|
||||
if (label && /[㐀-鿿]/.test(label)) return label;
|
||||
return typeof value === "string" ? PLANET_ZH[value as PlanetName] ?? (value === "Lagna" ? "上升" : null) : null;
|
||||
}
|
||||
export function sign(value: unknown, chinese?: unknown, index?: unknown): string | null {
|
||||
const label = readable(chinese);
|
||||
if (label && /[㐀-鿿]/.test(label)) return label;
|
||||
if (typeof value === "string") {
|
||||
const mapped = SIGN_ZH[value as SignName];
|
||||
if (mapped) return mapped;
|
||||
if (Object.values(SIGN_ZH).some(name => value === name || value === `${name}座`)) return value;
|
||||
}
|
||||
return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < 12 ? SIGN_ZH[SIGN_ORDER[index]] : null;
|
||||
}
|
||||
export const position = (value: unknown) => {
|
||||
const raw = record(value);
|
||||
return [sign(raw.sign, raw.sign_cn, raw.sign_idx), decimal(raw.degree_in_sign ?? raw.sign_degree)] as const;
|
||||
};
|
||||
export const isBlocked = (value: unknown) => {
|
||||
const raw = record(value);
|
||||
return raw.status === "blocked" || raw.status === "missing_in_local" || raw.claimStatus === "blocked" || raw.claim_status === "blocked";
|
||||
};
|
||||
@@ -1,128 +1,138 @@
|
||||
import { findForbiddenContent } from "./personal-report-contract";
|
||||
import { reportFactTablesSchema, type ReportFactTable } from "./report-fact-table-schema";
|
||||
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";
|
||||
|
||||
type RecordValue = Record<string, unknown>;
|
||||
const record = (value: unknown): RecordValue => value && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : {};
|
||||
const at = (value: unknown, path: string): unknown => path.split(".").reduce((next, key) => record(next)[key], value);
|
||||
const safeCell = (value: unknown): string | number | boolean | null => {
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string" && value.length <= 300 && !findForbiddenContent(value).length) return value;
|
||||
return null;
|
||||
};
|
||||
const SOURCES = [
|
||||
["vimshottari", "Vimshottari 主运与分运", "timing_and_predictive_systems.dasha", "dasha"],
|
||||
["ashtakavarga", "SAV / BAV 八分力", "strengths_and_scores.ashtakavarga", "ashtakavarga"],
|
||||
["shadbala", "Shadbala 六分量", "strengths_and_scores.shadbala", "shadbala"],
|
||||
["functional_roles", "功能吉凶与宫主", "strengths_and_scores.functional_benefic_malefic", "functional_benefic_malefic"],
|
||||
["avasthas", "Avastha 行星状态", "advanced_systems.avasthas", "avasthas"],
|
||||
["special_points", "附属星点与特殊上升点", "divisional_and_special_charts", ""],
|
||||
["sade_sati", "Sade Sati 阶段框架", "advanced_systems.yogas_doshas.sade_sati", "yogas_doshas.sade_sati"],
|
||||
["annual", "年度回归与 Saham", "timing_and_predictive_systems.annual_tajika_pack", "annual_tajika_pack"],
|
||||
["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<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 });
|
||||
const subtable = (id: string, title: string, sourcePath: string, columns: string[]): ReportFactSubtable => ({ id, title, sourcePath, columns, rows: [], note: NOTE });
|
||||
|
||||
/** Explicit leaves only: no interpretations, remedies, profile, audit or producer text. */
|
||||
const SCALAR_KEYS = new Set([
|
||||
"longitude", "degree", "degree_in_sign", "sign_degree", "sign", "sign_idx", "sign_index", "house",
|
||||
"state", "degree_range", "strength", "lord", "start", "end", "is_current", "years",
|
||||
"dig_bala", "chesta_bala", "naisargika_bala", "drik_bala", "total", "total_virupas", "total_rupas",
|
||||
"is_active", "phase", "moon_sign", "rising", "peak", "setting", "jd_ut", "dt_ut", "dt_local", "sun_lon",
|
||||
]);
|
||||
|
||||
/** 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] of SOURCES) {
|
||||
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 (!source || !Object.keys(record(source)).length) continue;
|
||||
const sourcePath = worksheetValue !== undefined ? `worksheets.${key}` : rootValue !== undefined ? key
|
||||
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;
|
||||
// No new certainty: retain declared limitations; unspecified calculations stay sensitive.
|
||||
const claimStatus = raw.status === "blocked" || raw.status === "missing_in_local" || declared === "blocked" ? "blocked"
|
||||
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 rows: ReportFactTable["rows"] = [];
|
||||
const add = (path: string, label: string, value: unknown) => {
|
||||
const cell = safeCell(value);
|
||||
if (cell !== null) rows.push({ sourcePath: path, cells: [label, cell] });
|
||||
};
|
||||
const walk = (value: unknown, path: string, label: string, allScalars = false, depth = 0) => {
|
||||
if (depth > 10) return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((entry, index) => walk(entry, `${path}[${index}]`, `${label}[${index}]`, allScalars, depth + 1));
|
||||
} else if (value && typeof value === "object") {
|
||||
Object.entries(record(value)).forEach(([name, entry]) => {
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(name)) return;
|
||||
if (SCALAR_KEYS.has(name) || allScalars || name === "avasthas" || ["Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi", "sthana_bala", "kala_bala"].includes(name)) {
|
||||
walk(entry, `${path}.${name}`, `${label}.${name}`, allScalars, depth + 1);
|
||||
}
|
||||
});
|
||||
} else add(path, label, value);
|
||||
};
|
||||
const pick = (path: string, allScalars = false) => {
|
||||
const value = at(source, path);
|
||||
if (value !== undefined) walk(value, `${sourcePath}.${path}`, path, allScalars);
|
||||
};
|
||||
const table: ReportFactTable = { id, title, claimStatus, sourcePath, note: NOTE, columns: [...columns], rows: [] };
|
||||
const children: ReportFactSubtable[] = [];
|
||||
if (claimStatus !== "blocked") {
|
||||
if (id === "vimshottari") {
|
||||
const timeline = Array.isArray(raw.timeline) ? raw.timeline : [];
|
||||
timeline.forEach((period, index) => {
|
||||
list(raw.timeline).forEach((item, index) => {
|
||||
const period = record(item);
|
||||
const path = `${sourcePath}.timeline[${index}]`;
|
||||
walk(period, path, `MD[${index}]`);
|
||||
if (record(period).is_current === true) {
|
||||
const ad = record(period).antardasha_timeline;
|
||||
if (Array.isArray(ad)) ad.forEach((entry, ai) => {
|
||||
walk(entry, `${path}.antardasha_timeline[${ai}]`, `AD[${ai}]`);
|
||||
const pd = record(entry).pratyantar_dasha_timeline;
|
||||
if (Array.isArray(pd)) pd.forEach((item, pi) => walk(item, `${path}.antardasha_timeline[${ai}].pratyantar_dasha_timeline[${pi}]`, `AD[${ai}].PD[${pi}]`));
|
||||
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") {
|
||||
pick("sav.scores", true);
|
||||
Object.keys(record(raw.bav)).forEach((planet) => pick(`bav.${planet}.bindus`, true));
|
||||
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") {
|
||||
Object.keys(record(raw.planets)).forEach((planet) => {
|
||||
for (const component of ["sthana_bala.total", "dig_bala", "kala_bala.total", "chesta_bala", "naisargika_bala", "drik_bala"]) pick(`planets.${planet}.${component}`);
|
||||
});
|
||||
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 of ["functional_benefics", "functional_malefics", "functional_neutrals", "yogakarakas", "owned_houses"]) pick(bucket, true);
|
||||
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") {
|
||||
Object.keys(raw).forEach((planet) => pick(`${planet}.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"]) {
|
||||
Object.keys(record(at(source, kind))).forEach((name) => pick(`${kind}.${name}`));
|
||||
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 field of ["is_active", "phase", "moon_sign", "phases"]) pick(field);
|
||||
} else if (id === "annual") {
|
||||
pick("solar_return.data");
|
||||
for (const field of ["asc_sign", "asc_sign_idx", "age"]) {
|
||||
const path = `annual_chart.data.${field}`;
|
||||
add(`${sourcePath}.${path}`, path, at(source, path));
|
||||
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}`))]);
|
||||
}
|
||||
Object.keys(record(at(source, "annual_chart.data.planets"))).forEach((planet) => pick(`annual_chart.data.planets.${planet}`));
|
||||
Object.keys(record(at(source, "sahams.data"))).forEach((point) => pick(`sahams.data.${point}`));
|
||||
const sahamPath = worksheetValue !== undefined ? "worksheets.advanced_systems.sahams"
|
||||
: rootValue !== undefined ? "advanced_systems.sahams" : `${root.chart ? "chart." : ""}modules.sahams`;
|
||||
const sahams = record(rootValue !== undefined && worksheetValue === undefined ? undefined : at(root, sahamPath));
|
||||
Object.entries(sahams).forEach(([name, point]) => {
|
||||
if (/^[A-Za-z0-9_]+_saham$/.test(name)) walk(point, `${sahamPath}.${name}`, `sahams.${name}`);
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
const tableSourcePath = id === "annual" ? (worksheetValue !== undefined ? "worksheets"
|
||||
: rootValue !== undefined ? "timing_and_predictive_systems" : `${root.chart ? "chart." : ""}modules`) : sourcePath;
|
||||
tables.push({ id, title, claimStatus, sourcePath: tableSourcePath,
|
||||
note: id === "sade_sati" ? "这里只列本次计算返回的阶段框架;尚无三轮起止日期,不能据此判断时间。"
|
||||
: rows.length ? "原始计算供核对,不增加结论的确定性。" : "本次没有可展示的数据,不补算、不补写。",
|
||||
columns: ["原始字段", "数值 / 状态"], rows });
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user