准确性保证
约 71 万例金标测试如何保证 x-iztro 与 JS iztro 零差异,以及这个「准」的边界在哪。
适合:所有人。「哈希比对怎么做」一节给开发者
排盘库最重要的属性是结果正确。而「正确」在紫微斗数里没有权威裁判 —— 不同实现之间的差异往往来自流派取舍,很难说谁对谁错。
x-iztro 因此把目标定得很具体:与 JS iztro v2.5.8 逐字段一致。 把它当作金标准,差异就从「见仁见智」变成了可以自动检测的 bug。
iztro 是什么,为什么拿它当金标准
iztro 是一个 TypeScript 写的开源紫微斗数排盘库, 是这个领域里最完整、维护时间最长的开源实现之一, 不少前端项目与小程序在用。
选它做基准的理由不是「它一定对」,而是三条工程上的性质:
- 完整:本命盘、六层运限、四组十二神、年系杂耀、中州派、六种盘面语言, 一个不缺 —— 有得可对,才对得下去。
- 确定:同样的输入永远给同样的输出,没有随机与外部依赖, 所以差异一定是逻辑差异,不是噪声。
- 可锁版本:把版本钉在 v2.5.8,基准就是稳定的; iztro 升级时重新生成基准数据,失败的用例清单就是版本间的行为差异清单。
这个「准」指什么、不指什么
与 iztro 一致 ≠ 命理界唯一正解
x-iztro 保证的是:在同一套流派取舍下,算得与一个成熟实现完全一样。
它不保证这套流派取舍本身是「对的」。 庚干化科取太阴还是天府、年干支按正月初一还是立春换、晚子时归今天还是明天 —— 这些历来就有分歧,iztro 选了一套,x-iztro 原样跟随,并把有分歧的地方 做成配置开关让你自己决定。
如果你的流派与默认不同,改配置或用自定义四化表,别期待默认输出符合你的师承。
金标数据的年份区间
基准数据由 JS 侧生成,用例集中在 JS 实现能稳定生成的年份区间内, 边界年代(1583–1983 与 2044–2100)另有按十年抽样的一层。
x-iztro 本身支持公历 1583–9999 年,区间之外的年份能排出盘, 但没有金标数据逐例对照过 —— 用在极端年份上时请自行验证。
覆盖矩阵
全部基准数据由锁定版本的 JS iztro 生成,共约 71 万例:
| 层级 | 用例数 | 覆盖范围 | 数据格式 |
|---|---|---|---|
| Tier 1 | 1,560 | 60 年 × 13 时辰 × 男女,全字段逐一比对(含展示字段、来因宫与结构化日期) | 完整 JSON |
| Tier 2 | 37,440 | 60 年 × 每月 1/15 号 × 13 时辰 × 男女 | 压缩 JSON |
| Tier 3 | 586,430 | 60 年每一天 × 13 时辰 × 男女 × fix_leap(闰月双份) | SHA-256 CSV |
| 边界年代 | 46,228 | 1583–1983 与 2044–2100 每 10 年抽样,补 Tier 1/2/3 只覆盖 1984–2043 的盲区 | SHA-256 CSV |
| Horoscope | 5,760 | 360 命盘 × 16 目标日期,六层级运限全字段 | 紧凑 JSON |
| Variants | 14,268 | by_lunar 闰月逐日、中州派、六种盘面语言 | CSV / JSON |
| Config | 9,696 | 四个分界开关的非默认取值,含排盘层与运限层的组合 | CSV / JSON |
| 中州派盘型 | 12,488 | 天盘 / 地盘 / 人盘 | SHA-256 CSV |
| 合计 | 713,870 |
以下不计入上表:
- 翻译反查 1,559 例:逐条对照 iztro
kot的实际取值,守的是同形译名的消歧顺序 - 绑定契约 13 例:把 DTO 与 iztro 的
JSON.stringify输出逐键逐值对照 - Python 端到端 172 例(含自定义四化表与亮度表、全时辰覆盖)
- Go 端到端测试:金标对照、星耀落宫、并发正确性、覆盖表、非法输入轰炸
- C FFI 边界安全测试:任何非法输入都必须返回错误 JSON 而非崩溃
- Prompt 快照测试:中英两种语言的本命与运限 prompt 逐字节比对
每一层在防什么
Tier 1 抓字段级差异。它比对包括展示串、来因宫标记在内的每一个字段, 一旦某个字段的翻译或格式与 iztro 不同,立刻暴露。
Tier 2 与 Tier 3 抓边界日期。紫微斗数的错误常常只在特定日期出现 —— 闰月、月末、年初、节气交接。Tier 3 覆盖 60 年里的每一天,一天都不漏。
边界年代抓年份两端。Tier 1/2/3 集中在 1984–2043, 这一层按十年抽样把范围拉到 1583 与 2100, 防的是历法算法在远端年份上悄悄走样。
Horoscope 抓运限。16 个目标日期专门选在会出问题的位置: 12 个流年支各一、童限、高龄、闰月、晚子时。
Variants 抓流派与盘面语言。中州派与六种盘面语言各自完整比对, 确保切换算法派别或语言不引入偏差。
Config 与中州派盘型抓分界点与盘型。立春窗口逐日、晚子时、生日前后 —— 每个开关都在它会产生分歧的窗口里逐日验证。
哈希比对怎么做的
给开发者Tier 3 有 58 万例,存完整 JSON 会有几十 GB。所以这几层比对的是规范化串的 SHA-256:
JS 侧的 tests/golden/canonical.mjs 与 Rust 侧的 tests/common/mod.rs
实现同一套序列化规则,逐字节同构。两边各自把排盘结果压成同一个规范化串,
比对哈希即可 —— 存的是 64 个十六进制字符,而不是几十 KB 的 JSON。
哈希不一致时,用生成器的 --inspect 系列参数重放该例的 JS 输出,
与 Rust 的规范化串做 diff,直接定位到出错字段。
跑测试
# 常规层:单元 + Tier 1/2 + 运限 + 变体 + 配置 + 契约,约 15 秒
cargo test
# Tier 3 全量:586,430 例,约 20 秒
cargo test --release --test golden_tier3 -- --ignored
# 绑定端到端
cd python && pytest tests/ # 需先 maturin develop
cd go/iztro && go test ./...重新生成基准数据
需要 Node.js 环境:
cd tests/golden
npm install
node generate_tier1.mjs # → tier1_data.json
node generate_tier2.mjs # → tier2/year_*.json(60 个文件)
node generate_tier3.mjs # → tier3/year_*.csv(60 个文件,约 30 分钟)
node generate_horoscope.mjs # → horoscope_data.json
node generate_variants.mjs
node generate_config.mjs跟进 iztro 新版本
流程是固定的:
- 升级
tests/golden/package.json里锁定的 iztro 版本 - 重新生成全部基准数据
- 跑
cargo test
失败的用例清单就是两个版本之间的行为差异清单 —— 不需要读 changelog, 测试直接告诉你哪些字段变了。
这条流程覆盖的是数值差异。iztro 新增或删除 API 时测试不会报, 那部分要按从 iztro 迁移:API 对照一页逐条自查。
零容忍的含义
任何一例不一致都当作 bug 处理,不接受「差异很小」「这个字段不重要」这类理由。 凡是 iztro 有的功能与数据,x-iztro 必须给出相同结果; 在此之上再谈扩展功能(语言无关标识、Prompt 生成、Config 开关的语义化)。