Files
Jyotisha/scripts/report_render/upstream_coverage.py
T
ClaudeandClaude Opus 5.5 2028d7fdf7
Independent Staging Quality Gate / validate-frontend (push) Successful in 5m8s
Independent Staging Quality Gate / validate-python (push) Successful in 5m55s
Independent Staging Quality Gate / publish (push) Successful in 3m9s
feat(report): 「大运传统参考」 from the astrologer's bilingual nine-lord text (yinduzhanxing 058c4f5f)
- Sync references/mahadasha_editorial_bilingual.json (9 lords x 3 paragraphs,
  zh + en, per-paragraph source locators) and mahadasha_editorial_policy.md,
  plus the one-line pointers in SKILL.md and vimshottari_dasha_guide.md.
- New chapter after 大运解释 in both editions: the three paragraphs side by
  side (general / traditionally supportive / traditionally strained), never
  picked by lord or chart, Mahadasha layer only; source labelled as secondary
  study notes with the original edition pending verification; English only
  in the English edition.
- Coverage contract: the topic moves from NOT_CARRIED to the checked list.

No change to the frozen exporter, dates or any scoring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
2026-11-06 21:48:09 +08:00

306 lines
20 KiB
Python

"""Coverage contract against the upstream customer export (2026-11-06).
Every topic the upstream export prints (`customer-export/registry`, branch
`codex/customer-export-frozen-20261016`) is mapped to the section in our full
data report that carries it, in both editions. `audit_report` checks that each
section is present and carries real content, and that degraded markers
(「公开报告资料包未带此模块」, 「简化模型」) are gone.
Topics deliberately not carried are listed in `NOT_CARRIED` with the reason,
so a gap is a decision on record, never a silent omission.
Command line, on any exported report:
python -m scripts.report_render.upstream_coverage report.md [--language en]
"""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class Topic:
upstream: str # upstream export heading
zh: str # our Chinese heading (prefix of the heading text)
en: str # our English heading (prefix)
kind: str = "table" # "table": needs a real table cell; "text": needs prose; "group": heading only
# Optional column that must carry real values: (zh header, en header, "any" | "all").
column: tuple[str, str, str] | None = None
# Upstream topic -> our section. Order follows the upstream export.
TOPICS: tuple[Topic, ...] = (
Topic("AI Reading Guide", "阅读说明", "AI Reading Guide", "text"),
Topic("Key points", "重点", "Focus", "text"),
Topic("Key points · timeline", "时间线", "Timeline", "text"),
Topic("Key points · career / finances / relationships", "三个领域的盘面依据", "Domain evidence"),
Topic("Birth Particulars / Hindu Calendar", "出生资料 / 印度历", "Birth Particulars / Hindu Calendar"),
Topic("Birth Chart Planetary Raw Positions", "本命盘行星原始位置", "Birth Chart Planetary Raw Positions"),
Topic("Chandra Chart / Bhava Spashta", "月亮盘 / Sripati 宫位精确表", "Chandra Chart / Bhava Spashta"),
Topic("Upagraha / Sub-Planets Points", "Upagraha / 副行星 点位", "Upagraha / Sub-Planets Points"),
Topic("Special Lagnas and Points", "特殊上升与派生点", "Special Lagnas and Points"),
Topic("Natal and Divisional North Indian Charts", "本命与分盘北印度图盘", "Natal and Divisional North Indian Charts", "group"),
Topic("Planets, Nakshatras and Houses", "行星、星宿与宫位解释", "Planets, Nakshatras and Houses", "group"),
Topic("Shadbala / Bhava Bala Summary", "六维力量 / Bhava Bala 摘要", "Shadbala / Bhava Bala Summary"),
Topic("Declination / Kranti / Speed", "赤纬 / 速度", "Declination / Speed"),
Topic("Planetary Avasthas", "Avastha 行星状态", "Planetary Avasthas"),
Topic("Bhava Bala House Strength", "Bhava Bala 十二宫力量", "Bhava Bala of the twelve houses"),
Topic("Planetary Friendship Matrix", "行星友敌关系矩阵源数据", "Planetary Friendship matrix source"),
Topic("Aspect Matrix", "相位矩阵", "/ p45-p47 aspect matrix"),
Topic("Jaimini Karakas, Arudha Padas and Special Lagnas", "Jaimini Karaka、Arudha Pada", "Jaimini Karaka, Arudha Pada"),
Topic("Ashtakavarga SAV / BAV / Pinda", "Ashtakavarga SAV / BAV / Pinda", "Ashtakavarga SAV / BAV / Pinda"),
Topic("Ashtakavarga Complete House Scores", "Ashtakavarga 完整宫位分数", "Ashtakavarga Full House Scores"),
Topic("Vimsopaka / Pushkara / Vargottama", "二十分力量 / Pushkara / Vargottama", "Vimsopaka / Pushkara / Vargottama"),
Topic("Ashtakavarga Shodhya / Yoga Pinda", "Ashtakavarga Shodhya", "Ashtakavarga Shodhya"),
Topic("Vimshottari Dasha", "Vimshottari 大运", "Vimshottari Dasha"),
Topic("Yogini Dasha", "Yogini 大运", "Yogini Dasha"),
Topic("Kala Chakra Dasha", "Kala Chakra 大运", "Kala Chakra Dasha", column=("主星/星座", "Lord/sign", "all")),
Topic("Jaimini / Narayana Dasha", "Jaimini / Narayana 大运", "Jaimini / Narayana Dasha"),
Topic("Sthira Dasha", "Sthira", "Sthira Dasha"),
Topic("Niryaana Shoola Dasha", "Niryaana Shoola", "Niryaana Shoola Dasha"),
Topic("Drig Dasha", "Drig", "Drig Dasha"),
Topic("Navamsha Dasha", "Navamsha", "Navamsha Dasha"),
Topic("Lagna Kendradi Rashi Dasha", "上升 Kendradi", "Lagna Kendradi Rashi Dasha"),
Topic("Shoola Dasha", "Shoola", "Shoola Dasha"),
Topic("Tribhagi Dasha", "Tribhagi 大运", "Tribhagi Dasha"),
Topic("Tribhagi 40-year Dasha", "Tribhagi 40-year 大运", "Tribhagi 40-year Dasha"),
Topic("Shattrimshatsama Dasha", "Shattrimshatsama 大运", "Shattrimshatsama Dasha"),
Topic("Satabdika Dasha", "Satabdika 大运", "Satabdika Dasha"),
Topic("Sade Sati / Dhayya / Kantaka Saturn", "Sade Sati / Dhayya / Kantaka", "Sade Sati / Dhayya / Kantaka"),
Topic("KP Chart / Cusps", "KP 星盘 / 宫头", "KP Chart / Cusps"),
Topic("KP Significators / Planet Lords", "KP 征象星 / 行星主星", "KP Significators / Planet Lords"),
Topic("Natal / KP Ruling Planets", "本命 / KP 主宰星", "Natal / KP Ruling Planets"),
Topic("Varshaphala Basic Data and Annual Planets", "Varshaphala 基础资料与年度行星表", "Varshaphala base data"),
Topic("Mudda Annual Dasha Table", "Mudda 年度大运", "Mudda annual dasha"),
Topic("Patyayini Main-Period Ending-Date Table", "Patyayini 结束日", "Patyayini ending dates"),
# A year without a combination says so in one sentence.
Topic("Tajika Yoga Presence Table", "Tajika 瑜伽呈现对照表", "Tajika Yoga Presence Table", "text"),
Topic("Saham Points", "Sahams 点位表", "Sahams"),
Topic("Annual Anchors", "年度锚点", "Annual anchors"),
Topic("True Solar Return", "真实太阳返照", "True Solar Return"),
Topic("Panchadhikari", "Panchadhikari", "Panchadhikari"),
Topic("Tripataki Placement", "Tripataki Chakra 年盘落点", "Tripataki Chakra placements"),
Topic("Annual Monthly Return Chart Data", "年度月返照图盘数据", "Monthly returns within the year"),
Topic("Lifetime Annual Return Chart Data", "生命周期年度返照图盘数据", "Lifetime annual return charts"),
Topic("Eight-Year Birthplace Overview", "出生地八年年度总览", "Eight-year overview at the birthplace"),
Topic("Bhavesh House-Lord Placements", "宫主(Bhavesh)落宫解释", "House lords (Bhavesh)"),
Topic("Mangala / Sade Sati / Dosha Results", "火星婚姻煞 / Sade Sati / Dosha 结果", "Mangala / Sade Sati / Dosha Results"),
Topic("Traditional Supportive Practices / Remedies", "传统辅助建议", "Traditional supportive notes"),
Topic("Benefic / Mixed / Malefic Yoga Candidates", "可用瑜伽总表", "Applicable Benefic / Mixed / Malefic Yogas"),
Topic("Dasha Interpretations", "大运解释", "Dasha Interpretation", "group"),
# Astrologer's bilingual text (yinduzhanxing 058c4f5f), replacing the unpublished upstream prose.
Topic("Traditional Dasha Reference Alternatives", "大运传统参考", "Traditional Dasha Reference", "text"),
Topic("Sudarshana Chakra", "Sudarshana Chakra(三参考点盘)", "Sudarshana Chakra", "group"),
Topic("Divisional Charts Numeric Tables", "分盘数值位置表", "Divisional Charts Numeric Tables", "group"),
Topic("Shadbala Components", "六维力量细项", "Shadbala Components"),
Topic("Functional Benefic / Malefic Layer", "功能性吉凶星层", "Functional Benefic / Malefic Layer"),
Topic("KP House Cusps and Significators", "KP 宫头与征象星", "KP House Cusps and Significators"),
Topic("Calculation Notes", "计算口径", "Calculation Conventions", "group"),
Topic("Research High-Division Raw Data", "研究型分盘", "Research divisional charts"),
Topic("Kala Chakra AD / Pratyantar Boundaries", "Kalachakra", "Kalachakra"),
Topic("Yoga Participants and Timing Links", "瑜伽参与行星", "Yoga Participants", column=("参与行星", "Participants", "any")),
Topic("Shadbala Subcomponent Data", "六维力量分项", "Shadbala Subcomponent", column=("Sthana(位置)", "Sthana", "all")),
Topic("Ashtakavarga Contribution and Reduction Data", "八分法加减", "AV Contribution and Reduction"),
Topic("House Components and Physical Coordinates", "宫位分量与坐标", "House Components and Physical Coordinates"),
Topic("Birth Calendar Elements", "出生历法", "Birth Calendar"),
Topic("Goel Jaimini Strength and Special-Point Evidence", "Goel Jaimini 强度与特殊点", "Goel Jaimini Strength and Special Points"),
Topic("Dasha Boundary Scan", "大运与小运交界检查", "Dasha Boundary Scan", "text"),
Topic("Natal Vivah Saham", "本命 Vivah Saham", "Natal Vivah Saham"),
Topic("Lagna Vargottamamsa", "上升 Vargottamamsa", "Lagna Vargottamamsa"),
Topic("Natal Tithi Lord Associations", "本命 Tithi 主星", "Natal Tithi Lord"),
Topic("Dated Pancha Pakshi Schedule", "五鸟活动", "Pancha Pakshi"),
Topic("Neutral and Compound Planetary Relationships", "行星中性与复合友敌关系", "Neutral and Compound Relationships"),
Topic("Pushkara Decision Basis", "Pushkara 判断依据", "Pushkara Basis"),
Topic("Rashi Tulya Navamsa Mapping", "Rashi Tulya Navamsa", "Rashi Tulya Navamsa"),
Topic("Dispositor Terminals and Cycles", "定位星终点与循环", "Dispositor Terminals"),
Topic("Natal Saham Formula Basis", "本命特殊点公式依据", "Natal Saham Formula Basis"),
Topic("Bhrigu Relationship Geometry", "Bhrigu 关系几何", "Bhrigu Relationship Geometry"),
Topic("Aspect Geometry and Motion Data", "相位几何", "Aspect Geometry"),
Topic("Avastha Inputs and Method Data", "行星状态输入", "Avastha Inputs"),
Topic("Vimsopaka Contribution Details", "Vimsopaka 分盘贡献", "Vimsopaka Contribution"),
Topic("Argala Support and Obstruction Structure", "Argala 支持与阻挡", "Argala"),
Topic("Nakshatra Reference Data", "星宿参照", "Nakshatra Reference"),
Topic("Dispositor Chain Data", "定位星链", "Dispositor Chain"),
Topic("Bhava Chalit Planet Placements", "Bhava Chalit 行星落宫", "Bhava Chalit Planet Placements"),
Topic("Dated Transit Positions", "注明日期的行运", "Dated Transit", column=("星座", "Sign", "all")),
Topic("Double Transit PAC Data", "木土双重行运 PAC", "Double Transit PAC"),
Topic("Lagna and Seventh Lord Transit Checks", "上升主与七宫主行运", "Lagna / 7th Lord Transit"),
Topic("Planetary Congregation Data", "行星聚集", "Congregation"),
Topic("D30 Sign-Based Planetary States", "D30 状态", "D30 States"),
Topic("Bhava Bala Three-Component Alternative", "Bhava Bala 三分量", "Bhava Bala Three-Component"),
Topic("KP Calculation Conventions", "KP 计算口径", "KP Calculation Conventions"),
Topic("D9 Dignity and Navamsa Segments", "D9 尊贵", "D9 Dignity"),
Topic("Divisional Mapping Conventions", "分盘映射", "Divisional Mapping"),
Topic("Bhava Chalit House Centres and Boundaries", "Bhava Chalit 宫心", "Bhava Chalit Centres"),
Topic("Conditional Dasha Applicability", "条件大运是否适用", "Conditional Dasha Applicability"),
Topic("Chara Dasha Period Durations", "Chara 大运时长", "Chara Dasha Period Durations"),
Topic("Bhrigu Approximate Progression Sequence", "Bhrigu 近似推进序列", "Bhrigu Approximate Progression"),
Topic("Additional Native Timing Calculations", "补充推运计算数据", "Additional timing calculations", "group"),
Topic("Yoga Vimshottari natal", "本命 Yoga Vimshottari", "Natal Yoga Vimshottari"),
Topic("Saptavarga lord relationships", "七分盘星座主", "Saptavarga lords"),
Topic("Buddhi Gati", "Buddhi Gati", "Buddhi Gati"),
Topic("Kaala", "Kaala", "Kaala"),
Topic("Sudasa", "Sudasa 出生周期", "Sudasa birth cycle"),
Topic("Independent Annual Method Reference", "独立年度方法参考", "Independent Annual Method Reference", "group"),
Topic("Three-Year Predictive Ephemeris", "三年逐月星历", "Three-Year Predictive Ephemeris"),
Topic("Narayana Method Comparison", "Narayana 口径对照", "Narayana Method Comparison"),
Topic("Yogini Dasha Cycle Context", "Yogini 周期", "Yogini Periods"),
Topic("Tribhagi Dasha Profile", "Tribhagi 适用情况", "Tribhagi Profile"),
Topic("Jaimini Calculation Details", "Jaimini 计算细节", "Jaimini Calculation Details"),
Topic("Jaimini Graha Pada Derivation", "Graha Pada", "Graha Pada"),
Topic("Jaimini Yogi and Ava Yogi", "Jaimini Yogi 与 Ava Yogi", "Jaimini Yogi and Ava Yogi"),
Topic("Karakamsha Coordinates", "Karakamsha", "Karakamsha"),
Topic("Darakaraka Chart Coordinates", "Darakaraka", "Darakaraka"),
Topic("Navamsha Dasha AD Duration Contract", "Navamsha 小运时长", "Navamsha AD Duration"),
Topic("Bhrigu Cycle Context", "Bhrigu 周期", "Bhrigu Cycle"),
Topic("Arudha / Upapada / A10 Derivation", "Arudha、Upapada 与 A10", "Arudha / UL / A10"),
Topic("D30 Source Input Degrees", "D30 输入", "D30 Source Input"),
Topic("Tara and Chandra Combined Matrix", "Tara 与 Chandra 矩阵", "Tara-Chandra Matrix"),
Topic("Nakshatra Dasha Timing Context", "星宿大运上下文", "Nakshatra Dasha Context"),
Topic("Natal Saham Formula Trace", "本命特殊点公式轨迹", "Natal Saham Formula Trace"),
Topic("Vimshottari Birth-Balance Basis", "Vimshottari 出生余额", "Vimshottari Birth-Balance"),
Topic("Multi-Reference Transit Positions", "多参考点行运", "Multi-Reference Transit"),
Topic("Annual Return House Framework", "年运整宫框架", "Annual Return House Framework"),
Topic("Annual Return Calculation Contract", "年运返照计算约定", "Annual Return Calculation Contract"),
Topic("Tajika Aspect and Yoga Candidates", "Tajika 相位与瑜伽候选", "Tajika Aspect and Yoga Candidates", "text"),
Topic("Tripataki Chakra Detail", "Tripataki 轮盘明细", "Tripataki Chakra Detail"),
Topic("Annual Tajika Strength Detail", "年度 Tajika 力量", "Annual Tajika Strength"),
Topic("Annual Saham Formula Trace", "年度特殊点公式轨迹", "Annual Saham Formula Trace"),
Topic("Annual Chart Numeric Data", "年度图盘数值数据", "Annual Chart Numeric Data"),
Topic("Current Dasha Convergence by Domain", "当前多系统收敛", "Current Dasha Convergence"),
Topic("Canonical Yoga Candidate Rows", "瑜伽条目", "Canonical Yoga Rows", column=("行星", "Planet", "any")),
Topic("Ashtakavarga Yoga Pinda Reduction Data", "八分法 Yoga Pinda", "AV Yoga Pinda"),
Topic("Shadbala Result Summary", "六维力量结果", "Shadbala Result"),
Topic("Ashtakavarga Sign Assessments", "八分法星座评估", "AV Sign Assessments"),
Topic("Prastara Ashtakavarga Contribution Totals", "Prastara 合计", "Prastara Totals"),
)
# Upstream topics this report does not carry, each with the decision on record.
NOT_CARRIED: dict[str, str] = {
"Tithi Ashtottari — independent MD/AD/PD": "A raw PyJHora external-replay dump (AGPL fields); not shipped to customers (2026-11-06).",
"Eight-Year Residence Overview": "Needs the user's current residence, which the product does not collect.",
"Narayana Method Comparison · PyJHora comparison": "Report lines naming PyJHora are removed by product rule; Rath and the source-aware method are both shown.",
}
DEGRADED_MARKERS: dict[str, tuple[str, ...]] = {
"zh": ("公开报告资料包未带此模块", "(简化模型)"),
"en": ("This module is not in the public report packet", "(Simplified model)"),
}
_UNDETERMINED = {"zh": {"未判定", "—", "-", "", "无"}, "en": {"not determined", "Not determined", "—", "-", "", "None"}}
_SEPARATOR = re.compile(r"^\|?\s*:?-{2,}")
def _sections(markdown: str) -> list[tuple[str, str]]:
"""(heading text, body up to the next heading of the same or higher level)."""
lines = markdown.splitlines()
out: list[tuple[str, str]] = []
heads = [(i, len(m.group(1)), m.group(2).strip()) for i, line in enumerate(lines)
if (m := re.match(r"^(#{2,4})\s+(.+?)\s*$", line))]
for index, (line_no, level, text) in enumerate(heads):
end = len(lines)
for later_no, later_level, _ in heads[index + 1:]:
if later_level <= level:
end = later_no
break
out.append((text, "\n".join(lines[line_no + 1:end])))
return out
def _real_cell(body: str, language: str) -> bool:
"""A data row (below a table's separator) with a real value outside the label column."""
empty = _UNDETERMINED[language]
in_rows = False
for line in body.splitlines():
if not line.startswith("|"):
in_rows = False
continue
if _SEPARATOR.match(line.strip()):
in_rows = True
continue
if not in_rows:
continue # the header row names columns; it is not data
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if any(cell not in empty for cell in cells[1:]):
return True
return False
def _column_values(body: str, header: str) -> list[str]:
"""Values of one named column across the tables in a section body."""
values: list[str] = []
index: int | None = None
in_rows = False
for line in body.splitlines():
if not line.startswith("|"):
index, in_rows = None, False
continue
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if _SEPARATOR.match(line.strip()):
in_rows = index is not None
continue
if not in_rows:
lowered = [cell.lower() for cell in cells]
index = lowered.index(header.lower()) if header.lower() in lowered else None
continue
if index is not None and index < len(cells):
values.append(cells[index])
return values
def _prose(body: str) -> bool:
text = "\n".join(line for line in body.splitlines() if line.strip() and not line.startswith("|"))
return len(text.strip()) >= 12
def audit_report(markdown: str, language: str = "zh") -> list[dict[str, str]]:
"""Findings for one exported report; an empty list means full coverage."""
findings: list[dict[str, str]] = []
sections = _sections(markdown)
for topic in TOPICS:
wanted = topic.zh if language == "zh" else topic.en
matches = [body for text, body in sections if text.startswith(wanted)]
if not matches:
findings.append({"check": "missing_section", "topic": topic.upstream, "heading": wanted})
continue
if topic.kind == "group":
continue
ok = any(_real_cell(body, language) or (topic.kind == "text" and _prose(body)) for body in matches)
if not ok:
findings.append({"check": "empty_section", "topic": topic.upstream, "heading": wanted})
continue
if topic.column:
header = topic.column[0] if language == "zh" else topic.column[1]
values = [value for body in matches for value in _column_values(body, header)]
real = [value for value in values if value not in _UNDETERMINED[language]]
if not values or (topic.column[2] == "all" and len(real) < len(values)) or not real:
findings.append({"check": "empty_column", "topic": topic.upstream, "heading": f"{wanted} · {header}"})
for marker in DEGRADED_MARKERS[language]:
if marker in markdown:
findings.append({"check": "degraded_marker", "topic": marker, "heading": ""})
return findings
def main(argv: list[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
language = "zh"
if "--language" in args:
index = args.index("--language")
language = args[index + 1]
del args[index:index + 2]
if not args:
print(__doc__)
return 2
with open(args[0], encoding="utf-8") as handle:
findings = audit_report(handle.read(), language)
for item in findings:
print(f"{item['check']}\t{item['topic']}\t{item['heading']}")
print(f"{len(TOPICS)} topics checked, {len(findings)} findings, {len(NOT_CARRIED)} not carried on record")
return 1 if findings else 0
if __name__ == "__main__":
raise SystemExit(main())