v3.6.2 同步发版:晚子时不再双重换日、港澳时区归位

- 晚子时(真太阳时 23:00–24:00,约 4% 的盘)修正:换日改由 tyme4py provider 原生实现(cast 新增 sect 参数,1=子初 / 2=子正),排盘层不再自行预滚日。3.6.1 及此前会滚两次,日柱多进一天、时柱随之错干——锚点 1973-08-23 23:32 男:旧 癸巳日壬子时 → 正 壬辰日庚子时。已排过的晚子时盘建议重排核对
- --day-boundary 子正 现真正生效(走 sect=2,晚子时日柱算当天),此前只改审计文案不改实际排盘
- data/cities.json 港澳 28 个条目由 Asia/Shanghai 改为 Asia/Hong_Kong(19)、Asia/Macau(9):大陆 1986–1991 行夏令时而港澳 1979 年后无,沿用上海区会把大陆夏令时错加到港澳命主身上
- 修副本环境 `pytest tests/` 收集期中断(test_sync_tool 模块级加载外层 tools/,skipif 挡不住导入),现按设计整模块 skip
- .gitignore 新增 data/.license.json、data/.update_check.json,本机授权与更新缓存不再可能误入仓
- 测试 178 项全绿(副本内 sync 工具 4 项按设计 skip);manifest 1278 文件,ed25519 签名,公钥指纹 3089bc1cc9a453e5…
This commit is contained in:
2026-10-12 19:05:09 +08:00
parent 415cb472b1
commit b20fd5c3c9
14 changed files with 175 additions and 72 deletions
+6
View File
@@ -3,3 +3,9 @@ __pycache__/
*.pyc *.pyc
.pytest_cache/ .pytest_cache/
.venv/ .venv/
# 运行时本机状态:授权文件与自更新缓存一律不入库、不随 zipball 分发
# (与 tools/build_manifest.py 的 EXCLUDED_FILES 保持一致;公众版须不带
# 授权状态文件出货,否则下载者会拿到别人已签发/已耗尽的额度)
data/.license.json
data/.update_check.json
+1 -1
View File
@@ -1 +1 @@
3.6.1 3.6.2
+28 -28
View File
@@ -1774,12 +1774,12 @@
"香港": { "香港": {
"lon": 114.17, "lon": 114.17,
"lat": 22.32, "lat": 22.32,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"澳门": { "澳门": {
"lon": 113.54, "lon": 113.54,
"lat": 22.2, "lat": 22.2,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"北京市东城区": { "北京市东城区": {
"lon": 116.4125, "lon": 116.4125,
@@ -16024,132 +16024,132 @@
"香港特别行政区中西区": { "香港特别行政区中西区": {
"lon": 114.1494, "lon": 114.1494,
"lat": 22.2847, "lat": 22.2847,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区湾仔区": { "香港特别行政区湾仔区": {
"lon": 114.178, "lon": 114.178,
"lat": 22.2791, "lat": 22.2791,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区东区": { "香港特别行政区东区": {
"lon": 114.2212, "lon": 114.2212,
"lat": 22.2825, "lat": 22.2825,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区南区": { "香港特别行政区南区": {
"lon": 114.155, "lon": 114.155,
"lat": 22.2486, "lat": 22.2486,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区油尖旺区": { "香港特别行政区油尖旺区": {
"lon": 114.1684, "lon": 114.1684,
"lat": 22.3145, "lat": 22.3145,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区深水埗区": { "香港特别行政区深水埗区": {
"lon": 114.1582, "lon": 114.1582,
"lat": 22.3366, "lat": 22.3366,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区九龙城区": { "香港特别行政区九龙城区": {
"lon": 114.1879, "lon": 114.1879,
"lat": 22.3153, "lat": 22.3153,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区黄大仙区": { "香港特别行政区黄大仙区": {
"lon": 114.199, "lon": 114.199,
"lat": 22.3391, "lat": 22.3391,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区观塘区": { "香港特别行政区观塘区": {
"lon": 114.2092, "lon": 114.2092,
"lat": 22.3237, "lat": 22.3237,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区荃湾区": { "香港特别行政区荃湾区": {
"lon": 114.116, "lon": 114.116,
"lat": 22.371, "lat": 22.371,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区屯门区": { "香港特别行政区屯门区": {
"lon": 113.9717, "lon": 113.9717,
"lat": 22.3968, "lat": 22.3968,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区元朗区": { "香港特别行政区元朗区": {
"lon": 114.0274, "lon": 114.0274,
"lat": 22.4442, "lat": 22.4442,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区北区": { "香港特别行政区北区": {
"lon": 114.1423, "lon": 114.1423,
"lat": 22.4988, "lat": 22.4988,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区大埔区": { "香港特别行政区大埔区": {
"lon": 114.1668, "lon": 114.1668,
"lat": 22.4484, "lat": 22.4484,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区西贡区": { "香港特别行政区西贡区": {
"lon": 114.2598, "lon": 114.2598,
"lat": 22.3171, "lat": 22.3171,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区沙田区": { "香港特别行政区沙田区": {
"lon": 114.1905, "lon": 114.1905,
"lat": 22.3823, "lat": 22.3823,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区葵青区": { "香港特别行政区葵青区": {
"lon": 114.1343, "lon": 114.1343,
"lat": 22.3666, "lat": 22.3666,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"香港特别行政区离岛区": { "香港特别行政区离岛区": {
"lon": 113.9413, "lon": 113.9413,
"lat": 22.2894, "lat": 22.2894,
"tz": "Asia/Shanghai" "tz": "Asia/Hong_Kong"
}, },
"澳门特别行政区花地玛堂区": { "澳门特别行政区花地玛堂区": {
"lon": 113.5478, "lon": 113.5478,
"lat": 22.2108, "lat": 22.2108,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区花王堂区": { "澳门特别行政区花王堂区": {
"lon": 113.5439, "lon": 113.5439,
"lat": 22.2021, "lat": 22.2021,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区望德堂区": { "澳门特别行政区望德堂区": {
"lon": 113.5451, "lon": 113.5451,
"lat": 22.1967, "lat": 22.1967,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区大堂区": { "澳门特别行政区大堂区": {
"lon": 113.5486, "lon": 113.5486,
"lat": 22.1915, "lat": 22.1915,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区风顺堂区": { "澳门特别行政区风顺堂区": {
"lon": 113.5368, "lon": 113.5368,
"lat": 22.1903, "lat": 22.1903,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区嘉模堂区": { "澳门特别行政区嘉模堂区": {
"lon": 113.5536, "lon": 113.5536,
"lat": 22.1567, "lat": 22.1567,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区路凼填海区": { "澳门特别行政区路凼填海区": {
"lon": 113.5645, "lon": 113.5645,
"lat": 22.1396, "lat": 22.1396,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"澳门特别行政区圣方济各堂区": { "澳门特别行政区圣方济各堂区": {
"lon": 113.5549, "lon": 113.5549,
"lat": 22.1264, "lat": 22.1264,
"tz": "Asia/Shanghai" "tz": "Asia/Macau"
}, },
"_aliases": { "_aliases": {
"乌市": "乌鲁木齐", "乌市": "乌鲁木齐",
+13 -13
View File
@@ -1,14 +1,14 @@
{ {
"files": { "files": {
".gitignore": "1e4671dee2d3123a45609ed82a4329531e0d41bf1b6895ab169ecd8813cfb8f5", ".gitignore": "6afd9b30daced03071366d6069e30682c6d4bccd6e0aa08d17936c8d373e8789",
"LICENSE.md": "6b839540e0fd4ab75530ec31fc5c3cbc3c2d5de5fe85068ef1c0901e54be9315", "LICENSE.md": "6b839540e0fd4ab75530ec31fc5c3cbc3c2d5de5fe85068ef1c0901e54be9315",
"README.md": "adb4481f20360c66e67fd89076c1535c577cdf85f34122d2dac1ff727dd81c38", "README.md": "adb4481f20360c66e67fd89076c1535c577cdf85f34122d2dac1ff727dd81c38",
"SKILL.md": "720aa2de6e7e6f65c44f63421f0aece401146921e54a6c99f24db1af14241813", "SKILL.md": "720aa2de6e7e6f65c44f63421f0aece401146921e54a6c99f24db1af14241813",
"VERSION": "de58d841bd709aa85a66ecc500dfbb48e7c207e84c399e30e751e8ccad6e8c9b", "VERSION": "bb8408e7188bc6e00f87159c1898e6bfee03926fb2c65b5af3f372a7abfbc7fb",
"data/.public_edition.json": "e8692c45d512c78903fc89a924d0e6d4dc9b95a78ca6b359bb7a5ef5c946ffee", "data/.public_edition.json": "e8692c45d512c78903fc89a924d0e6d4dc9b95a78ca6b359bb7a5ef5c946ffee",
"data/cities.json": "00f17905c8843218162e25238a78aeb4c4a3bb5424b611cc30ed9e436db31854", "data/cities.json": "9ae91a498d2b12e5c2f53efc5dc4989e891cf138b06c9b593796427cdc272393",
"memory/README.md": "a979f985dd87e87bfca49676ebf10a537d299b1eeebac121c506c320c88a3afd", "memory/README.md": "a979f985dd87e87bfca49676ebf10a537d299b1eeebac121c506c320c88a3afd",
"pyproject.toml": "b202c2ca784d501959d3dc4d7024851de8b66b15a7e3368b81566b2e045601b4", "pyproject.toml": "81b2ddba56b7a5cf158e807ede54273750f3040d7fa0426ffd9187fd73993292",
"requirements.txt": "46a4c971b192cbf2c4972dd0f420e7928b7e44cd3cb9f7dc5a36efb321ab6fd6", "requirements.txt": "46a4c971b192cbf2c4972dd0f420e7928b7e44cd3cb9f7dc5a36efb321ab6fd6",
"rules/00_总纲_取格.md": "829a35454f5c91966ecafe4dd2113b913588933efe08d0e1c127ae82ccec0da8", "rules/00_总纲_取格.md": "829a35454f5c91966ecafe4dd2113b913588933efe08d0e1c127ae82ccec0da8",
"rules/11_正官格.md": "385193d93dfba89a935ca8907098f70548ad1fc12ab6a07a5280fd04224c83b6", "rules/11_正官格.md": "385193d93dfba89a935ca8907098f70548ad1fc12ab6a07a5280fd04224c83b6",
@@ -568,22 +568,22 @@
"scripts/_runtime_guard.py": "c4f9d00acbb5b2ba49680b23ed4f846c60211e67b9d58950079b3e8909dcfe81", "scripts/_runtime_guard.py": "c4f9d00acbb5b2ba49680b23ed4f846c60211e67b9d58950079b3e8909dcfe81",
"scripts/_update_sig.py": "9bb3521b950ab834c770e313a98c9cc8f7e997997540258724505af774a4abe6", "scripts/_update_sig.py": "9bb3521b950ab834c770e313a98c9cc8f7e997997540258724505af774a4abe6",
"scripts/astro/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "scripts/astro/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"scripts/astro/day_boundary.py": "b5a24b3937fbcd9c278331b5b19f8cf708acc987f6f6f97f346524786735af9a", "scripts/astro/day_boundary.py": "b0ddd270be439b51fab46595661d2f9b11d83b52acda9c2f1ec6f79fec7aae78",
"scripts/astro/equation_of_time.py": "ee9331022b4ed35d407664e97b5ce9ce99167a06ce7cb8c9ffba5497a437da8e", "scripts/astro/equation_of_time.py": "ee9331022b4ed35d407664e97b5ce9ce99167a06ce7cb8c9ffba5497a437da8e",
"scripts/astro/solar_time.py": "35b45edc365083a6ce90afe076176c6f3e798509e76d3f03819fd1845491b153", "scripts/astro/solar_time.py": "35b45edc365083a6ce90afe076176c6f3e798509e76d3f03819fd1845491b153",
"scripts/astro/timezone_resolve.py": "3a287a64bd50736ce91ac15876e88edeb98745eb6d0459cf18e7e6ee3c026aba", "scripts/astro/timezone_resolve.py": "3a287a64bd50736ce91ac15876e88edeb98745eb6d0459cf18e7e6ee3c026aba",
"scripts/cast_chart.py": "9b9f33cfae9af797859554c630529d8fc5c27c5688a0414fbc5038ae2e5c90d8", "scripts/cast_chart.py": "f5fd58dffc9a66d85d018740714b4b7e497cbc13b75c9bf4e901d827ae5f026e",
"scripts/chart/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "scripts/chart/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"scripts/chart/ganzhi.py": "94d53d2a8cd7674312223e976ce6e1379a6fbb53873253b34963b3e3d645508e", "scripts/chart/ganzhi.py": "94d53d2a8cd7674312223e976ce6e1379a6fbb53873253b34963b3e3d645508e",
"scripts/chart/luck.py": "d930f3f402cf814eaaa10fdcaa0a8be4a47411e6961dd0324ac38a8f898b4299", "scripts/chart/luck.py": "d930f3f402cf814eaaa10fdcaa0a8be4a47411e6961dd0324ac38a8f898b4299",
"scripts/chart/model.py": "8279145da10901cfb1a7b4823762074169dfd554bbe8ce39ae15862334b001f6", "scripts/chart/model.py": "8279145da10901cfb1a7b4823762074169dfd554bbe8ce39ae15862334b001f6",
"scripts/chart/shensha.py": "9db4a0e4cfaaa667efa9bbdcf7b8fd3d0c461cafbdf125e037cb467d75803939", "scripts/chart/shensha.py": "9db4a0e4cfaaa667efa9bbdcf7b8fd3d0c461cafbdf125e037cb467d75803939",
"scripts/chart/tyme_adapter.py": "06a46f16efc3d745edb5f35bbdcfee1c78552a44235fc85de0af3eb6c163eaca", "scripts/chart/tyme_adapter.py": "5a8831050738d41e7a27a9bee55f86a84b351b0e9a203a35d12f383a9f9ef801",
"scripts/chart_shot.py": "0096a01e64128a2628ee0e2d3bc211d93fbb0e2b17a49a34f4f2f9a37da4960c", "scripts/chart_shot.py": "0096a01e64128a2628ee0e2d3bc211d93fbb0e2b17a49a34f4f2f9a37da4960c",
"scripts/geo/__init__.py": "1211b885ec767b399472df572f970b07d6d028a5889af49fe50f6fc1a2a0cf7d", "scripts/geo/__init__.py": "1211b885ec767b399472df572f970b07d6d028a5889af49fe50f6fc1a2a0cf7d",
"scripts/geo/cities.py": "9d393608c03a2ad5c7352939e70ea717dae5dd4bc833dfdd0e2f663940c61fb5", "scripts/geo/cities.py": "9d393608c03a2ad5c7352939e70ea717dae5dd4bc833dfdd0e2f663940c61fb5",
"scripts/memory_lookup.py": "23f02b04636924a1f6872d338e85ee2145ed1cde742b3e2e5b6ee4592ff92d50", "scripts/memory_lookup.py": "23f02b04636924a1f6872d338e85ee2145ed1cde742b3e2e5b6ee4592ff92d50",
"scripts/memory_save.py": "cfdf6dcd652475fb9bf04521655581dad10f36d7d7f69d079bf07833b99ba4e6", "scripts/memory_save.py": "eea51e7483966630c89705094b8a56c14f999573e7b06f5bb911bdd6d0accbca",
"scripts/query_shensha.py": "e4c852c8e56fb8b32bde0fb376d985647bcfd93a74fe30fd34c5884d2ab77dc3", "scripts/query_shensha.py": "e4c852c8e56fb8b32bde0fb376d985647bcfd93a74fe30fd34c5884d2ab77dc3",
"scripts/render/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "scripts/render/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"scripts/render/assets/lingyao-logo.png": "3d63f71ff9af61a443708f2665d1331737bb567a4e731d049676dfeb4710e323", "scripts/render/assets/lingyao-logo.png": "3d63f71ff9af61a443708f2665d1331737bb567a4e731d049676dfeb4710e323",
@@ -601,20 +601,20 @@
"tests/golden/longitudes.yaml": "6fda1e6b632d29c135362c9273b931c4416c9f24c31b07e2a7baadc6e2077bc2", "tests/golden/longitudes.yaml": "6fda1e6b632d29c135362c9273b931c4416c9f24c31b07e2a7baadc6e2077bc2",
"tests/golden/time_boundary.yaml": "637090a1c081b9b824fc0073d29dad073d29138c3b48082bdf41ce77f5af3b37", "tests/golden/time_boundary.yaml": "637090a1c081b9b824fc0073d29dad073d29138c3b48082bdf41ce77f5af3b37",
"tests/test_chart_shot.py": "7923d2868ded898331a65152b237cf014fa49b180b08706584ff7319f88b7a33", "tests/test_chart_shot.py": "7923d2868ded898331a65152b237cf014fa49b180b08706584ff7319f88b7a33",
"tests/test_day_boundary.py": "0e6c9ee32177a178f21a7812f83032abe4d1f585de48d07d21e2b432bdbcbd04", "tests/test_day_boundary.py": "6e2038152738a02642a3b04edfaf542328179998e62c1960a59389a527868850",
"tests/test_e2e.py": "e324930fad582dc38618fa3599425bb48ec718beaf426efcb76531911c9464fa", "tests/test_e2e.py": "e324930fad582dc38618fa3599425bb48ec718beaf426efcb76531911c9464fa",
"tests/test_equation_of_time.py": "8a3a1baf7e61c42015405cc80fc0bdf4fd9bc732d440bc156f9c92c94e30f790", "tests/test_equation_of_time.py": "8a3a1baf7e61c42015405cc80fc0bdf4fd9bc732d440bc156f9c92c94e30f790",
"tests/test_html.py": "d40f4558ddfda156706a0c3d4c52c794fbc1e59427acdc93bcdad7df34217638", "tests/test_html.py": "d40f4558ddfda156706a0c3d4c52c794fbc1e59427acdc93bcdad7df34217638",
"tests/test_license_gate.py": "ee44226b75620b3965ba52cc2a0cc5404fbff37822fd96a6faf2aed89d07d47e", "tests/test_license_gate.py": "ee44226b75620b3965ba52cc2a0cc5404fbff37822fd96a6faf2aed89d07d47e",
"tests/test_luck.py": "821f98bc19ab1e920353de58c6ffacac5d757c887c690a2d596f51c1485b21ea", "tests/test_luck.py": "821f98bc19ab1e920353de58c6ffacac5d757c887c690a2d596f51c1485b21ea",
"tests/test_memory.py": "068241927df08301806451ff6e66206ceb9d439d95197b3506fe519d892c28ed", "tests/test_memory.py": "bec4427e3e874b655583a6d9a69c85a590aab15c09ad645e6f7ce2a7139403db",
"tests/test_runtime_guard.py": "7fd7e2f334433432452ea68fde30df1698def2126a1920f8da6d6aab236c1caf", "tests/test_runtime_guard.py": "7fd7e2f334433432452ea68fde30df1698def2126a1920f8da6d6aab236c1caf",
"tests/test_self_update.py": "87a0fdaf28132e70fe407a552ff77f5911e13e67ca1d568581a273eaba875712", "tests/test_self_update.py": "87a0fdaf28132e70fe407a552ff77f5911e13e67ca1d568581a273eaba875712",
"tests/test_share.py": "7938344415cbba95f683b2a09ecec9af28d00658f475ff0a56563dd13beff630", "tests/test_share.py": "7938344415cbba95f683b2a09ecec9af28d00658f475ff0a56563dd13beff630",
"tests/test_shensha.py": "2aaa181279739356828f05a2175027b0ae711b322b4bd0e60d5c5e62538bcac8", "tests/test_shensha.py": "2aaa181279739356828f05a2175027b0ae711b322b4bd0e60d5c5e62538bcac8",
"tests/test_solar_time.py": "f072e5ea82fd0cb331c8a4fdc6dce08148cd9ac81ef9fe4506196fa68a42d1e4", "tests/test_solar_time.py": "f072e5ea82fd0cb331c8a4fdc6dce08148cd9ac81ef9fe4506196fa68a42d1e4",
"tests/test_sync_tool.py": "67395f121cfa0092af7edb579dac54a07690b989eb82bb16c6ea815325e47ce3", "tests/test_sync_tool.py": "b96cf8ac26480cc7431c95335095b77f718d240caf04243e844d2b5c10d7dd76",
"tests/test_timezone.py": "3698878999d232bd431d6908c42fe8f4845c696da6085731abaf2f74017d722d", "tests/test_timezone.py": "8b292cc8e5649b8af3e9e1a0214fcd28c06a2aef28f3529874f43c3c6d77ba42",
"tests/test_update_sig.py": "3bfd6ce28a699afe233fe51d9d9c7aacc308f54a4dcb9f9e7ecd272bb0a9b5a2", "tests/test_update_sig.py": "3bfd6ce28a699afe233fe51d9d9c7aacc308f54a4dcb9f9e7ecd272bb0a9b5a2",
"tests/test_version_sentinel.py": "9b2bc603a2e86aa33d904f1381ecd8ab41eb0e759803ec47122d662c10a7df83", "tests/test_version_sentinel.py": "9b2bc603a2e86aa33d904f1381ecd8ab41eb0e759803ec47122d662c10a7df83",
"vendor/VENDOR.md": "dbf017616ac2fda4fef56ce95e3610e77457108b7fa426136fd25764d78fa3c8", "vendor/VENDOR.md": "dbf017616ac2fda4fef56ce95e3610e77457108b7fa426136fd25764d78fa3c8",
@@ -1279,5 +1279,5 @@
"vendor/tzdata/zoneinfo/zonenow.tab": "7f9d1c11445af7a17862c20a9081226d31a09a1730e270ff59a34c8d2b4c7f7f", "vendor/tzdata/zoneinfo/zonenow.tab": "7f9d1c11445af7a17862c20a9081226d31a09a1730e270ff59a34c8d2b4c7f7f",
"vendor/tzdata/zones": "28d8f3eb25839095fa8a209997bb20e822ebfbebe21e60edc483a3d5e20dc881" "vendor/tzdata/zones": "28d8f3eb25839095fa8a209997bb20e822ebfbebe21e60edc483a3d5e20dc881"
}, },
"version": "3.6.1" "version": "3.6.2"
} }
+1 -1
View File
@@ -1 +1 @@
/hVDbf9RW2/oWP5z1nhGyc6/Ip76Inw4mLKaRtsojbuDS/tAaY9km8phEBGgC4T8qunh/FD8qc4dcVHk+iJfCA== bQwEXaTayoFrFHTwoJ3+vFTSB6kmWkPNNoEXOnVgdUqe2+H1f7ZYn2dp+CzE9XQFozkBqoCX1zsmdoORNEJsAw==
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "ZiPingEightChar" name = "ZiPingEightChar"
version = "3.6.1" version = "3.6.2"
requires-python = ">=3.10" # zoneinfo 需 3.9+,dataclass 语法需 3.10 requires-python = ">=3.10" # zoneinfo 需 3.9+,dataclass 语法需 3.10
dependencies = [ dependencies = [
"tyme4py==1.5.0", # 精确锁定:历表数据跨版本会变,锁版本 = 锁可复现性 "tyme4py==1.5.0", # 精确锁定:历表数据跨版本会变,锁版本 = 锁可复现性
+5 -1
View File
@@ -5,9 +5,13 @@ ZI_HOUR_START = 23 # 真太阳时 23:00 换日
def resolve_calendar_day(tst: datetime, rule: str = "子初") -> tuple[datetime, dict]: def resolve_calendar_day(tst: datetime, rule: str = "子初") -> tuple[datetime, dict]:
""" """
日界归算:rule="子初"(默认)真太阳时 23:00–24:00 归入次日; 日界归算与审计:rule="子初"(默认)真太阳时 23:00–24:00 归次日;
rule="子正" 以 0:00 为日界,晚子时不换日。 rule="子正" 以 0:00 为日界,晚子时不换日。
注意:换日的**实际排盘效果**由 chart.tyme_adapter.cast 的 sect 参数
(tyme4py provider)原生实现;本函数返回的日历日仅供审计与外部参考,
调用方不得把它再拼回时刻传给 cast(会双重换日)。
返回 返回
(日历日 00:00 的 datetime, flag) (日历日 00:00 的 datetime, flag)
""" """
+6 -4
View File
@@ -88,11 +88,13 @@ def main() -> int:
# L0-d UTC → 真太阳时(经度修正 + 均时差) # L0-d UTC → 真太阳时(经度修正 + 均时差)
tst, st_audit = to_true_solar_time(utc, loc.lon) tst, st_audit = to_true_solar_time(utc, loc.lon)
# L0-e 子初换日:真太阳时 23:00–24:00 归入次日 # L0-e 日界审计:is_late_zi 警示与口径文案;换日本身由排盘层
day, bd_audit = resolve_calendar_day(tst) # provider 口径实现(见 tyme_adapter.cast 的 sect 参数),此处不再滚日
_day, bd_audit = resolve_calendar_day(tst)
# 排盘时刻 = 换日后的日历日 + 真太阳时的时分秒 # 排盘时刻 = 真太阳时原样(晚子时的日界归算交给 tyme4py provider,
cast_dt = day.replace(hour=tst.hour, minute=tst.minute, second=tst.second) # 预先滚日会造成双重换日)
cast_dt = tst
# L1-a 排盘(tyme4py 唯一接触面) # L1-a 排盘(tyme4py 唯一接触面)
raw = cast(cast_dt, a.gender) raw = cast(cast_dt, a.gender)
+18 -2
View File
@@ -55,11 +55,27 @@ def _shen_sha(ganzhi: dict, gender: str) -> dict:
} }
def cast(cast_dt: datetime, gender: str) -> dict: def cast(cast_dt: datetime, gender: str, sect: int = 1) -> dict:
"""cast_dt:已完成子初换日调整的真太阳时(naive,北京时间语义)。""" """cast_dt:真太阳时(naive,北京时间语义)。
sect:日界流派。1=子初换日(tyme4py 默认 provider,晚子时 23:xx 日柱归次日);
2=子正换日(LunarSect2,晚子时日柱算当天)。晚子时的换日由 provider
原生实现,调用方不得再对 cast_dt 预先滚日(否则双重换日)。
"""
from tyme4py.eightchar.provider.impl import (
DefaultEightCharProvider, LunarSect2EightCharProvider,
)
from tyme4py.lunar import LunarHour
prev_provider = LunarHour.provider
LunarHour.provider = (LunarSect2EightCharProvider() if sect == 2
else DefaultEightCharProvider())
try:
solar = SolarTime.from_ymd_hms(cast_dt.year, cast_dt.month, cast_dt.day, solar = SolarTime.from_ymd_hms(cast_dt.year, cast_dt.month, cast_dt.day,
cast_dt.hour, cast_dt.minute, cast_dt.second) cast_dt.hour, cast_dt.minute, cast_dt.second)
ec = solar.get_lunar_hour().get_eight_char() ec = solar.get_lunar_hour().get_eight_char()
finally:
LunarHour.provider = prev_provider
pillars = {k: getattr(ec, f"get_{k}")() for k in _PILLARS} pillars = {k: getattr(ec, f"get_{k}")() for k in _PILLARS}
me = ec.get_day().get_heaven_stem() me = ec.get_day().get_heaven_stem()
+6 -5
View File
@@ -85,13 +85,14 @@ def _cast_chart(name: str, gender: str, birth: str, place: str,
# L0-d UTC → 真太阳时(经度修正 + 均时差) # L0-d UTC → 真太阳时(经度修正 + 均时差)
tst, st_audit = to_true_solar_time(utc, loc.lon) tst, st_audit = to_true_solar_time(utc, loc.lon)
# L0-e 日界归算:默认子初换日(真太阳时 23:00–24:00 归入次日) # L0-e 日界审计:is_late_zi 警示与口径文案;换日由排盘层 provider
day, bd_audit = resolve_calendar_day(tst, rule=day_boundary) # 口径实现(sect 参数),此处不再对时刻预先滚日(避免双重换日)
_day, bd_audit = resolve_calendar_day(tst, rule=day_boundary)
cast_dt = day.replace(hour=tst.hour, minute=tst.minute, second=tst.second) cast_dt = tst
# L1-a 排盘(tyme4py 唯一接触面) # L1-a 排盘(tyme4py 唯一接触面);子正口径走 sect=2(晚子时日柱算当天)
raw = cast(cast_dt, gender) raw = cast(cast_dt, gender, sect=2 if day_boundary == "子正" else 1)
# L1-b 起运、大运、流年、流月、小运 # L1-b 起运、大运、流年、流月、小运
forward = is_forward(gender, raw["year"]["gan"]) forward = is_forward(gender, raw["year"]["gan"])
+37
View File
@@ -17,6 +17,9 @@ SCRIPTS_DIR = SKILL_ROOT / "scripts"
GOLDEN_DIR = Path(__file__).resolve().parent / "golden" GOLDEN_DIR = Path(__file__).resolve().parent / "golden"
if str(SCRIPTS_DIR) not in sys.path: if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR)) sys.path.insert(0, str(SCRIPTS_DIR))
_VENDOR = SKILL_ROOT / "vendor"
if str(_VENDOR) not in sys.path:
sys.path.insert(0, str(_VENDOR))
from astro.day_boundary import resolve_calendar_day # noqa: E402 from astro.day_boundary import resolve_calendar_day # noqa: E402
from astro.solar_time import to_true_solar_time # noqa: E402 from astro.solar_time import to_true_solar_time # noqa: E402
@@ -65,5 +68,39 @@ class LateZiGoldenTest(unittest.TestCase):
self.assertIs(flag["is_late_zi"], s["expect"]["is_late_zi"]) self.assertIs(flag["is_late_zi"], s["expect"]["is_late_zi"])
class LateZiSectPillarsTest(unittest.TestCase):
"""晚子时双滚回归:换日由 tyme4py provider 口径实现(sect 参数),
调用方不再预滚日。锚点:1973-08-23 23:32 真太阳时。
子初 → 壬辰日庚子时(滚一日);子正 → 辛卯日庚子时(不滚,晚子时时柱仍次日起)。
"""
def test_sect1_rolls_once(self):
from chart.tyme_adapter import cast
r = cast(datetime(1973, 8, 23, 23, 32), "男", sect=1)
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "壬辰")
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "庚子")
def test_sect2_no_roll(self):
from chart.tyme_adapter import cast
r = cast(datetime(1973, 8, 23, 23, 32), "男", sect=2)
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "辛卯")
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "庚子")
def test_provider_restored(self):
from chart.tyme_adapter import cast
from tyme4py.eightchar.provider.impl import DefaultEightCharProvider
from tyme4py.lunar import LunarHour
cast(datetime(1973, 8, 23, 23, 32), "男", sect=2)
self.assertIsInstance(LunarHour.provider, DefaultEightCharProvider)
def test_non_late_zi_unaffected(self):
from chart.tyme_adapter import cast
for sect in (1, 2):
with self.subTest(sect=sect):
r = cast(datetime(1973, 8, 23, 12, 0), "男", sect=sect)
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "辛卯")
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "甲午")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+7 -7
View File
@@ -131,7 +131,7 @@ class TestUpdateChart:
r1 = run(SAVE, tmp_path, *WANZI) r1 = run(SAVE, tmp_path, *WANZI)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}" assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = Path(r1.stdout.strip()) path = Path(r1.stdout.strip())
assert "丙子庚子庚辰丙子" in path.name # 默认子初换日盘 assert "丙子庚子己卯甲子" in path.name # 默认子初换日盘(晚子时归次日)
# 模拟核对后的回写 + 固定 created_at # 模拟核对后的回写 + 固定 created_at
text = path.read_text(encoding="utf-8") text = path.read_text(encoding="utf-8")
@@ -140,28 +140,28 @@ class TestUpdateChart:
text, count=1, flags=re.M) text, count=1, flags=re.M)
path.write_text(text, encoding="utf-8") path.write_text(text, encoding="utf-8")
# 用户确认子正换日:重排替换盘面段,日柱/时柱变 → 改新主键名 # 用户确认子正换日:重排替换盘面段,日柱变(时柱同为晚子时起法,不变)→ 改新主键名
r2 = run(SAVE, tmp_path, "--update-chart", str(path), r2 = run(SAVE, tmp_path, "--update-chart", str(path),
"--day-boundary", "子正") "--day-boundary", "子正")
assert r2.returncode == 0, f"stderr:\n{r2.stderr}" assert r2.returncode == 0, f"stderr:\n{r2.stderr}"
assert "日柱 庚辰→己卯" in r2.stderr and "时柱 丙子→甲子" in r2.stderr assert "日柱 己卯→戊寅" in r2.stderr
assert "待复核" in r2.stderr # 变动柱影响条目提示复核 assert "待复核" in r2.stderr # 变动柱影响条目提示复核
new_path = Path(r2.stdout.strip()) new_path = Path(r2.stdout.strip())
assert new_path != path and not path.exists() assert new_path != path and not path.exists()
assert "丙子庚子己卯甲子" in new_path.name assert "丙子庚子戊寅甲子" in new_path.name
assert list((tmp_path / "archives").glob("*.md")) == [new_path] assert list((tmp_path / "archives").glob("*.md")) == [new_path]
text = new_path.read_text(encoding="utf-8") text = new_path.read_text(encoding="utf-8")
meta = read_meta(text) meta = read_meta(text)
assert meta["pillars"] == '["丙子", "庚子", "己卯", "甲子"]' assert meta["pillars"] == '["丙子", "庚子", "戊寅", "甲子"]'
assert meta["day_master"] == "己" assert meta["day_master"] == "戊"
assert meta["created_at"] == "2000-01-01 00:00" # 保留 assert meta["created_at"] == "2000-01-01 00:00" # 保留
# 盘面段只剩新盘:日界审计与晚子时警示均为子正口径,无旧盘残留 # 盘面段只剩新盘:日界审计与晚子时警示均为子正口径,无旧盘残留
assert "- 日界规则:子正换日(真太阳时 00:00)" in text assert "- 日界规则:子正换日(真太阳时 00:00)" in text
assert "已按「子正换日」定盘" in text assert "已按「子正换日」定盘" in text
assert text.count("# 命盘") == 1 assert text.count("# 命盘") == 1
chart_section = text.split("## 盘面")[1].split("## 主要关注方向")[0] chart_section = text.split("## 盘面")[1].split("## 主要关注方向")[0]
assert "日柱:己卯" in chart_section and "日柱:庚辰" not in chart_section assert "日柱:戊寅" in chart_section and "日柱:己卯" not in chart_section
# 其余小节保留 # 其余小节保留
assert "- 【已核实】2016 年离乡赴厦门读大学。" in text assert "- 【已核实】2016 年离乡赴厦门读大学。" in text
+12 -6
View File
@@ -1,7 +1,9 @@
"""外层 tools/sync_license_copy.py 的集成测试。 """外层 tools/sync_license_copy.py 的集成测试。
sync/build/sign 工具留在授权方工作区外层 tools/,不随本仓库分发; sync/build/sign 工具留在授权方工作区外层 tools/,不随本仓库分发;
副本或 clone 环境下 tools/ 不存在,整个模块 skip。 副本或 clone 环境下 tools/ 不存在,整个模块 skip。注意 skipif 只跳过测试
**执行**、挡不住模块导入,故下面的模块级 _load_module 也带同一存在性守卫,
否则副本里 `pytest tests/` 会在收集阶段就 FileNotFoundError 并中断全套测试。
测试用微型假母版(monkeypatch 替换模块 SRC)同步进临时授权副本, 测试用微型假母版(monkeypatch 替换模块 SRC)同步进临时授权副本,
不碰真实母版与真实副本;签名用测试私钥,UPDATE_PUBLIC_KEY_HEX 同步造假。 不碰真实母版与真实副本;签名用测试私钥,UPDATE_PUBLIC_KEY_HEX 同步造假。
@@ -18,10 +20,12 @@ ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT.parent / "tools" TOOLS = ROOT.parent / "tools"
SYNC_TOOL = TOOLS / "sync_license_copy.py" SYNC_TOOL = TOOLS / "sync_license_copy.py"
pytestmark = pytest.mark.skipif( _HAS_TOOLS = (SYNC_TOOL.is_file()
not (SYNC_TOOL.is_file()
and (TOOLS / "build_manifest.py").is_file() and (TOOLS / "build_manifest.py").is_file()
and (TOOLS / "sign_manifest.py").is_file()), and (TOOLS / "sign_manifest.py").is_file())
pytestmark = pytest.mark.skipif(
not _HAS_TOOLS,
reason="外层 tools/ 不随仓库分发,本环境无同步工具", reason="外层 tools/ 不随仓库分发,本环境无同步工具",
) )
@@ -41,8 +45,10 @@ def _load_module(name: str, path: Path):
return mod return mod
sync_tool = _load_module("sync_license_copy", SYNC_TOOL) # 无 tools/ 时置 None:本模块所有测试已被 skipif 跳过,不会触达这两个名字
build_manifest = _load_module("build_manifest", TOOLS / "build_manifest.py") sync_tool = _load_module("sync_license_copy", SYNC_TOOL) if _HAS_TOOLS else None
build_manifest = (_load_module("build_manifest", TOOLS / "build_manifest.py")
if _HAS_TOOLS else None)
def _make_master(src: Path, pk_hex: str) -> None: def _make_master(src: Path, pk_hex: str) -> None:
+31
View File
@@ -50,5 +50,36 @@ class HistoryTzGoldenTest(unittest.TestCase):
) )
class HkMoDstTest(unittest.TestCase):
"""香港/澳门时区回归:1986–1991 中国大陆实行夏令时,港澳 1979 年后无;
cities.json 港澳条目必须为 Asia/Hong_Kong / Asia/Macau,不得随大陆拨快。"""
def test_hongkong_1987_no_dst(self):
loc = resolve_location("香港")
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
self.assertEqual(loc.tz, "Asia/Hong_Kong")
self.assertIs(audit["is_dst"], False)
def test_macau_1987_no_dst(self):
loc = resolve_location("澳门")
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
self.assertEqual(loc.tz, "Asia/Macau")
self.assertIs(audit["is_dst"], False)
def test_mainland_1987_dst_kept(self):
# 回归保护:大陆 1987 年夏令时仍应拨快
loc = resolve_location("北京")
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
self.assertIs(audit["is_dst"], True)
def test_hongkong_1970_historical_dst(self):
# 香港 1949 后的历史夏令时(如 1970 年)应被 IANA 库正确处理
# (1949 前走民国五时区历史表分支,不在此断言范围)
loc = resolve_location("香港")
_utc, audit = to_utc(datetime(1970, 7, 15, 12, 0), loc)
self.assertEqual(audit["tz_used"], "Asia/Hong_Kong")
self.assertIs(audit["is_dst"], True)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()