Speed up rectification scoring without changing results.
Cache work that depends only on the candidate minute, drop unread response fields, and reject windows above 241 candidates before any chart is built. Eleven canonical scenario hashes match 77057a819. Sealed and reported-offset replays match the 2026-10-08 records (changed_trial_count 8 and 581) and are stored as scoring_perf_2026_10_13.
Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
74384be024
commit
8e0503f9c0
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-14 — 校正每轮重算更快(待合入 staging)
|
||||
|
||||
- 校正每轮重算更快(2 小时窗口约快一倍以上),结果不变。一次要算的分钟数超过 241 会直接拒绝,不再占着计算空位排盘。Skill 版本不 bump(BUG-1323~1325、BUG-1345)。
|
||||
|
||||
## 2026-10-13 — 明说要做生时校正时,第一步就能打开(待合入 staging)
|
||||
|
||||
- 新对话第一句就说「帮我做生时校正 / 生时纠正 / 核对出生时间」,会直接打开校正,只留一句过渡,不先写一段解读。
|
||||
|
||||
@@ -17720,3 +17720,63 @@
|
||||
- 复发自:BUG-1306。
|
||||
- 修复版本:分支 `codex/rectification-step0-entry-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1323 | 校正引导窗口按候选两两比较,窗口一长就平方变慢
|
||||
|
||||
- 状态:fixed-pending-verify(尚未合入 staging)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
|
||||
- 来源:issue #57。任务书 `docs/tasks/TASK-rect-scoring-perf-20261013.md`。
|
||||
- 现象:校正把时间窗拉到大约两小时时,每一轮重算要多花好几秒。人看到的候选和分数并没有因此变多。
|
||||
- 触发条件:同一次打分里的候选分钟变多。候选越密,引导窗口这一段越接近按平方增长。
|
||||
- 根因:`guided_collect_windows` 对代表候选两两比较时,每一对都重新扫全部候选的大运起点。起点只跟「哪一条运、第几个分界」有关,同一分界算一次就够。
|
||||
- 修复:每个候选的起点先算好。某个分界已经比出不同并收过窗口后,后面的候选对跳过这个分界。窗口的排序、去重和截断没有改。
|
||||
- 验证:`tests/test_rect_scoring_perf_20261013.py` 用基线函数副本对照四种窗口,输出相同;`_context_time` 次数不超过候选数的 16 倍。同机 121 候选这一段从约 4.8 秒降到约 0.6 秒。11 个场景的规范化输出 sha256 与基线 `77057a819` 全部相同。
|
||||
- 防复发:上面的线性计数断言。
|
||||
- 相关记录:BUG-749、BUG-1324。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/rect-scoring-perf-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1324 | 只跟候选有关的三样东西仍在每个事件里重算
|
||||
|
||||
- 状态:fixed-pending-verify(尚未合入 staging)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
|
||||
- 来源:issue #57。关联 BUG-721。
|
||||
- 现象:同一个候选分钟上,功能吉凶、那罗延子运、大运换段邻近被每个事件各算一遍。两小时窗口里这三段合计大约一秒多。
|
||||
- 触发条件:一次打分有多个事件、多个候选。
|
||||
- 根因:功能吉凶只看上升星座,那罗延子运表只看该候选的大运,换段起点只看该候选的月亮(那罗延再加上升)。旧的 BUG-721 要求缓存在请求级静态上下文的顶层,新代码没有照做,测试又只用三到五个候选,所以没拦住。
|
||||
- 修复:这两份结果放在候选静态上下文的顶层,不放进特征,因此不改特征指纹。打分时直接用。换段邻近按候选日期和月亮度数各算一次全范围起点,再按事件年份过滤;过滤和原来在事件窗口里现算等价,因为原来的函数只会收下窗口内的年份,空结果也不依赖窗口。探针路径仍不接收子运表,这一项记在进度里,本单不改。
|
||||
- 验证:`tests/test_rect_scoring_perf_20261013.py`。功能吉凶在一次 121 候选打分里调用 121 次。打分循环里不再调用 `get_current_narayana_dasha`。缓存结果与现算在 0 到 120 岁、步长 0.01 年上一致。换段矩阵与基线副本一致,含跨午夜。`feature_hash` 不变。
|
||||
- 防复发:上述计数断言,以及记忆化测试把这两个顶层键列入可清空层。
|
||||
- 相关记录:BUG-721、BUG-1323、BUG-1325。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/rect-scoring-perf-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1325 | 打分响应里带着没有读取点的内部矩阵和重复回执
|
||||
|
||||
- 状态:fixed-pending-verify(尚未合入 staging)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-14
|
||||
- 来源:issue #57。关联 BUG-728。
|
||||
- 现象:两小时窗口的默认响应大约 2 MB。其中事件贡献矩阵和一份与对外回执重复的内部回执没有产品读取点。
|
||||
- 触发条件:调用校正打分。诊断接口也重复带了那份内部回执。
|
||||
- 根因:矩阵和内部回执直接放进了对外响应。新增字段之前没有先找读取点。
|
||||
- 修复:默认响应去掉 `event_contribution_matrix` 和 `candidate_decision_receipt`。研究脚本需要矩阵时显式打开 `include_internal=True`。对外仍用原来的 `decision_receipt`。算法版本和策略版本都不改。
|
||||
- 验证:`tests/test_rectification_v5_services.py` 默认断言这两个键不在,打开开关后都在。同机 121 候选默认规范化响应约 0.89 MB。11 个场景在打开内部字段并去掉计时字段后,sha256 与基线全部相同。
|
||||
- 防复发:默认响应断言这两个键不存在。新响应字段先找读取点。
|
||||
- 相关记录:BUG-728、BUG-1324。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/rect-scoring-perf-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1345 | 打分接口没有候选数上限,超宽窗口会占着重计算槽
|
||||
|
||||
- 状态:fixed-pending-verify(尚未合入 staging)
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #57。产品 2026-10-14 决定上限 241,与前端放宽到的最长区间一致。
|
||||
- 现象:直接调用打分可以一次排一整天、每分钟一个候选。计算还没开始就把重计算槽占住几分钟。
|
||||
- 触发条件:候选分钟数超过 241。产品路径的两小时、步长 1,最多 121 个,不受影响。全天扫描在步长 2、5、10 下仍低于上限。
|
||||
- 根因:打分按请求里的窗口枚举候选,没有上限。
|
||||
- 修复:超过 241 在排盘前拒绝,HTTP 400,正文里有 `too_many_candidates`。接口文件没有改,所以错误码仍是原来的 `ERR_BAD_REQUEST`。前端把这种 400 当成坏请求,不按「服务忙」重试。研究脚本可以用 `enforce_candidate_cap=False` 跳过,本单的冻结重跑没有用到这个开关。
|
||||
- 验证:`tests/test_rect_scoring_perf_20261013.py` 覆盖 240、241 通过,242 和全天步长 1 在 50 毫秒内拒绝,步长 2、5、10 通过。前端 `rectification-v9-engine-contract.test.ts` 断言这种 400 不重试。
|
||||
- 防复发:上述上限测试。
|
||||
- 相关记录:BUG-1325。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/rect-scoring-perf-20261013`,尚未合入 staging。
|
||||
|
||||
|
||||
@@ -451,6 +451,8 @@ Prevention: 引擎/上游同步类任务必须 fetch 后核对 `origin/main`,
|
||||
|
||||
Prevention: 引擎/打分路径的性能任务书开工前先查这两份清单;动到其中任何文件,即使输出不变,也要把「按研究协议重新冻结 sealed holdout / reported offset 记录」写进任务分解,否则不要合入。同机 A/B 是必要证据,但不能代替重新冻结。关联 BUG-1047、BUG-985。
|
||||
|
||||
2026-10-14:校正打分提速(BUG-1323~1325、BUG-1345)改了 `event_probes.py`、`dasha_transition_proximity.py`、`active_rectification_event_engine.py`、`api_service.py`。与 `ephemeris_nutation_2026_10_08` 对照输出不变后,另存标签 `scoring_perf_2026_10_13`。nutation 那四份文件没有改。
|
||||
|
||||
## ERR-111 | Shadbala 特征指纹随 PYTHONHASHSEED 变化,同一输入跨进程 `feature_hash` 不同 | observed 2026-09-26
|
||||
|
||||
同一台机器、同一份代码、同一虚构请求连跑两次 `score_candidates`:`candidate_feature_snapshot.features[*].fingerprints.shadbala` / `static` 与 `feature_hash` 每次都不同;固定 `PYTHONHASHSEED=0` 后两次逐字节一致。说明 Shadbala 结果里有依赖集合 / 哈希顺序的结构进入了指纹。打分分数本身未见变化(未系统核对)。
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"record_version": "reported-offset-native-candidate-date-v3",
|
||||
"frozen_at_utc": "2026-10-13T17:45:35.555216+00:00",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"radii_minutes": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"offsets_minutes": [
|
||||
-30,
|
||||
-20,
|
||||
-15,
|
||||
-10,
|
||||
-8,
|
||||
-5,
|
||||
-3,
|
||||
0,
|
||||
3,
|
||||
5,
|
||||
8,
|
||||
10,
|
||||
15,
|
||||
20,
|
||||
30
|
||||
],
|
||||
"minute_step": 1,
|
||||
"dataset": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"dataset_benchmark_id": "minute_rectification_fact_ranker_v4_holdout_v3",
|
||||
"implementation_sha256": "1c146bf5273edf871d885e9e2e68ee1c8a2f71759b54b4b878f63e771f44197c",
|
||||
"implementation_sha256_prefix": "1c146bf5273edf87",
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "89c146df190e8902731ea8111d1aff490fede00c919f529c7cf4c3e629a75579",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "085ca37ab70e1da18e6941161868618b22c9c9f09c043d0195de7f5650363453",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "9bfc0b8a06af6122fa6c3881706e64f28b9ba58b53d98ff55957d4741d820f3f",
|
||||
"scripts/dasha_analyzer.py": "5e53b1b4d4c7414c08dd0e7716f120a63c70ec3428ec93547f64d55c580da0e1",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "c7aaefd4b7b7ea98a5b94e0f7798e5d0139c7423d10ce4ebfff258a460d4456f",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "bdcaac92ece761dbcbd35d5a8f751e6b4b11f8475ef8152a5ea4d36ceb38356a",
|
||||
"scripts/research/sealed_holdout_rerun.py": "27904b39a64ff5f1b95ae3c8fc65ca0f3a8b4d8e3f4b88fbbf5d85ffeb53db0d",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "c1f06967c3d4d214edadff139c24aa31e59fb628477174235abe14f799a99076"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock",
|
||||
"scorer": "native_event_contribution_matrix_not_shadow_fact_ranker",
|
||||
"delivery": "initial_signature_clusters_peak_gap_lt_8_envelope_no_answers_no_elimination",
|
||||
"rank": "score_desc_then_sha256(benchmark_id:case_id:candidate_time)",
|
||||
"grid": "every_minute_inclusive_not_production_two_minute_sampling",
|
||||
"cross_midnight": "date_aware_candidates_distance_and_native_single_matrix",
|
||||
"evaluator_sha256": "bdcaac92ece761dbcbd35d5a8f751e6b4b11f8475ef8152a5ea4d36ceb38356a",
|
||||
"replay_revision": "native_candidate_date_v3",
|
||||
"supersedes": "candidate_date_grouped_v2_identity_and_path_not_assumed_numerically_wrong",
|
||||
"is_blind_evaluation": false,
|
||||
"truth_hidden_from_ranker": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"official_blind_trial_count": 0,
|
||||
"results_previously_seen": true,
|
||||
"must_not_use_for_tuning": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"record_version": "exposed-v3-fixed-protocol-cross-midnight-rerun-v2",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "89c146df190e8902731ea8111d1aff490fede00c919f529c7cf4c3e629a75579",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "085ca37ab70e1da18e6941161868618b22c9c9f09c043d0195de7f5650363453",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "9bfc0b8a06af6122fa6c3881706e64f28b9ba58b53d98ff55957d4741d820f3f",
|
||||
"scripts/dasha_analyzer.py": "5e53b1b4d4c7414c08dd0e7716f120a63c70ec3428ec93547f64d55c580da0e1",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "c7aaefd4b7b7ea98a5b94e0f7798e5d0139c7423d10ce4ebfff258a460d4456f",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "bdcaac92ece761dbcbd35d5a8f751e6b4b11f8475ef8152a5ea4d36ceb38356a",
|
||||
"scripts/research/sealed_holdout_rerun.py": "27904b39a64ff5f1b95ae3c8fc65ca0f3a8b4d8e3f4b88fbbf5d85ffeb53db0d",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "c1f06967c3d4d214edadff139c24aa31e59fb628477174235abe14f799a99076"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"production_identity_scope": "context_only_shadow_scorer_does_not_call_transition_proximity",
|
||||
"frozen_at_utc": "2026-10-13T17:44:49.007680+00:00",
|
||||
"dataset_path": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"algorithm_version": "birth-time-event-fact-ranker-v4-shadow",
|
||||
"implementation_sha256": "1c146bf5273edf871d885e9e2e68ee1c8a2f71759b54b4b878f63e771f44197c",
|
||||
"files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"historical_frozen_sha256": "f41c298dd6cdcebe7a93e632f7191954be7987f6012ca2a34cecb6e447fbf196",
|
||||
"evaluator_sha256": "27904b39a64ff5f1b95ae3c8fc65ca0f3a8b4d8e3f4b88fbbf5d85ffeb53db0d",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"candidate_radius_minutes": [
|
||||
10
|
||||
],
|
||||
"minute_step": 1,
|
||||
"release_metrics": {
|
||||
"top_1_rate_minimum": 0.6,
|
||||
"top_3_rate_minimum": 0.85,
|
||||
"mean_absolute_minute_error_maximum": 2.0,
|
||||
"false_confirmation_rate_maximum": 0.05,
|
||||
"correct_insufficient_evidence_rejection_rate_minimum": 0.9
|
||||
},
|
||||
"results_previously_seen": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"must_not_use_for_tuning": true,
|
||||
"tie_breaker": "sha256(benchmark_id:case_id:candidate_time); published minute is never passed to the ranker",
|
||||
"metric_rank_definition": "competition_rank_1_plus_strictly_higher_scores_legacy_protocol",
|
||||
"extra_metric_rank_definition": "score_desc_then_existing_opaque_sha256_total_order"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
# 校正打分提速进度(2026-10-14)
|
||||
|
||||
结论:两小时窗口的打分大约快到原来的 38%,结果不变。11 个场景的规范化输出与基线逐字节相同。密封重跑 20 例的 trials、metrics 与历史比较和 2026-10-08 那份记录逐字节相同,`changed_trial_count` 仍是 8。报偏移 900 例的核对写在文末。
|
||||
|
||||
## 基线
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 任务书基线 | `77057a819` |
|
||||
| 计划评论里的 base | `2994588ad` |
|
||||
| 本分支实际起点 | `74384be0243b16e7a261c2afdea2ca12bab92285`(当时的 `origin/staging`) |
|
||||
| 冻结文件相对 `77057a819` | `git diff --stat` 为空,所以计时和逐字节对照仍用这份基线 |
|
||||
|
||||
机器:13th Gen Intel Core i5-13400F,16 逻辑处理器,Windows。同一进程里先跑基线 5 次再跑新代码 5 次,取中位数。`PYTHONHASHSEED=0`。绝对秒数比任务书写的 3.8–4.8 秒高,因为那组数字不是这台机器测的。下面的比例以这次同机对照为准。
|
||||
|
||||
## 同机耗时(5 次中位数)
|
||||
|
||||
| 窗口 | 候选 | 基线 | 现在 | 相对基线 | 目标 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 12:00–12:20 | 21 | 0.5359 s | 0.3185 s | 59% | 不慢于基线 +5% |
|
||||
| 11:45–12:15 | 31 | 1.9700 s | 0.8696 s | 44% | — |
|
||||
| 11:00–13:00 | 121 | 10.6181 s | 4.0695 s | 38% | ≤ 45% |
|
||||
| 全天 `block_scan` 步长 10 | 144 | 6.7187 s | 3.8025 s | 57% | 不慢于基线 |
|
||||
|
||||
121 候选分段(中位数,秒):
|
||||
|
||||
| 段 | 基线 | 现在 | 相对 |
|
||||
| --- | --- | --- | --- |
|
||||
| `guided_collect_windows` | 4.8700 | 0.6116 | 13%(目标 ≤ 25%) |
|
||||
| `merge_transition_proximity` | 1.7124 | 0.4497 | 26%(目标 ≤ 35%) |
|
||||
| `get_current_narayana_dasha` | 0.7562 | 0.0387 | 探针路径仍会调用,打分循环为 0 |
|
||||
| `derive_functional_benefic_malefic` | 0.3295 | 0.0081 | 一次打分 121 次,每个候选 1 次 |
|
||||
| `build_candidate_static_context` | 0.9830 | 1.0625 | 这里多算了两份缓存,总时间仍下降 |
|
||||
|
||||
121 候选默认响应的规范化字节:基线 2,071,995(响应里带着矩阵),现在 930,631(约 0.89 MB)。目标 ≤ 0.96 MB。
|
||||
|
||||
## 11 个场景 sha256
|
||||
|
||||
两边都用 `PYTHONHASHSEED=0`,`sort_keys=True`,去掉 `column_compare_ms`。打分场景打开 `include_internal=True`。诊断场景另外去掉 `candidate_decision_receipt`,因为新诊断不再重复那份内部回执。基线工作树是一次性检出 `77057a819`,比完已删除。
|
||||
|
||||
| 场景 | sha256 | 字节 | 相同 |
|
||||
| --- | --- | --- | --- |
|
||||
| 12:00–12:20 | `2bc6c777ac4957ce99277649879f7f8ded5c8f246361d73cf18454a3ac7466b0` | 429250 | 是 |
|
||||
| 11:45–12:15 | `f886afac4b27a8a1f36c771bfdf1565e8df1f4f579d19e73288b0549a9544bda` | 597372 | 是 |
|
||||
| 11:00–13:00 | `2d5dcb5cd6e92664bba692d59f75eabdc4b55f822817b7a6f5106c89fd8c8708` | 2071995 | 是 |
|
||||
| 23:20–00:40 | `73aeb9b7a4d0a3a2b5643aff5e2ef779f15e2e765eefc60aa3985b347ca4b9c1` | 1255752 | 是 |
|
||||
| declined relationship+career | `9e48c7cf4b21c133bb52b85e73b0ab7891826a0125a5ef428f6a280503585ee8` | 1082766 | 是 |
|
||||
| refresh probes + column times | `7c977c19c48448e92a7845225ba04897359f29bbaf92e4ff2c69a83e7b070b1a` | 932456 | 是 |
|
||||
| 10:00–13:00 步长 2 | `e03adc5cd88e4df144186afc88cead358b67db37784717e8331f256507357cdc` | 1405125 | 是 |
|
||||
| block_scan 全天步长 10 | `38b27ab49744a7a8036017d10e919f2b8b8baa58fd53590e4b0a35d9689eb1e9` | 394426 | 是 |
|
||||
| block_scan 10:00–14:00 步长 5 | `6c13482afd5b5de9545185b3c92145f40629550556b2f0e83b009e690d933133` | 129483 | 是 |
|
||||
| 记忆化测试的公开请求 | `e0eb1c449a2a42cd081b15af5dece3f97fcffae627ab605d9514b413617964a7` | 90384 | 是 |
|
||||
| diagnostics 11:00–13:00 | `5a9d9ad7304ac01419ba85b6a24c23d1bc9b00ec1df417a7bb9385ce8dfea83b` | 456088 | 是 |
|
||||
|
||||
## 计数与等价
|
||||
|
||||
- 引导窗口:一次调用里 `_context_time` ≤ 16 × 候选数。121 与 21 的次数比不超过 (121/21)×1.2。输出与基线函数副本四种形状相同。
|
||||
- 换段邻近:一次调用里 `_vim_start_dates` ≤ 2 × 候选数,`_narayana_start_dates` ≤ 1 × 候选数。先按年份 1–9999 算全表,再按事件年份过滤。`event_probes.py` 里这两个函数只在年份落在窗口内时追加,空结果不依赖窗口,所以过滤后的列表与原来在窗口里现算相同。跨午夜矩阵也相同。
|
||||
- 功能吉凶:一次 121 候选打分调用 121 次。放在静态上下文顶层,不进 `feature`,`feature_hash` 不变。
|
||||
- 那罗延子运:打分循环(`_candidate_row`)里 `get_current_narayana_dasha` 为 0。0 到 120 岁、步长 0.01 年,缓存查找与现算相同。探针路径 `event_probes._score_year` 仍不接收这张表,所以整次打分里这个函数不是 0。这是任务书明确不改的偏差。
|
||||
- 候选上限:240 和 241 通过;242 和全天步长 1 在排盘前 50 毫秒内拒绝。步长 2、5、10 的产品窗口通过。全天 `block_scan` 在没给步长时自己改成 10,不触发上限。研究脚本里的回放半径到 120 正好 241,不超限。冻结重跑不经过 `score_candidates`,上限不影响那 900 例。
|
||||
|
||||
超过上限时 HTTP 400,正文含 `too_many_candidates`。API 入口文件没有改,所以错误码仍是 `ERR_BAD_REQUEST`。前端把这种 400 当成坏请求,不按服务忙重试。
|
||||
|
||||
## 三栏
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tests/test_rectification_engine_memoization.py` 缓存键 | 4 个 | 6 个,加上功能吉凶和那罗延子运表 | BUG-1324 |
|
||||
| `tests/test_rectification_v5_services.py` 矩阵断言 | 默认响应里有矩阵 | 默认没有;`include_internal=True` 才有 | BUG-1325 |
|
||||
| 同文件回执读取 | `candidate_decision_receipt` | `decision_receipt` | BUG-1325 |
|
||||
| 两份研究脚本的 `FREEZE` / `REPORT` | `ephemeris_nutation_2026_10_08` | `scoring_perf_2026_10_13` | BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录 |
|
||||
| 密封合同当前指针 | nutation 记录 | `scoring_perf_2026_10_13`;nutation 放进该对象里的 `previous_*` | 同上 |
|
||||
| `tests/test_sealed_holdout_contract_freshness.py` 排除前缀 | 到 nutation | 再排除两份新记录 | 同上 |
|
||||
| `frontend/tests/rectification-confirmation-gate.test.ts` 冻结路径 | nutation 报告 | `sealed_holdout_rerun_scoring_perf_2026_10_13.json` | 同上 |
|
||||
| `tests/test_repo_privacy_markers.py` 时钟集合行号 | 1182、1190 | 1195、1203 | BUG-1325 的三栏注释把原集合下移,钟点没变 |
|
||||
|
||||
## 冻结核对
|
||||
|
||||
标签 `scoring_perf_2026_10_13`。nutation 四份文件没有改。
|
||||
|
||||
密封重跑:trials 规范化 JSON 与 `sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json` 逐字节相同,metrics 相同,`historical_comparison` 相同,`changed_trial_count` 仍是 8。新报告 sha256 `25e2e59ecab81b528f0f27369a8638a907ea551f57fdd8572db00fe25a139641`。冻结记录 sha256 `1ce7f7ac67221e10bb4d176f5e8af9299fbae7586717d83048b7e3c124e43e7c`。当前树遗留 12 文件哈希 `1c146bf5273edf871d885e9e2e68ee1c8a2f71759b54b4b878f63e771f44197c`(`active_rectification_event_engine.py` 在这 12 个文件里,所以这份哈希变了,输出没变)。
|
||||
|
||||
报偏移 900 例:trials 规范化 JSON、summary、`historical_comparison` 都与 `reported_offset_ephemeris_nutation_2026_10_08.json` 相同。`changed_trial_count` 仍是 581。这份报告没有 `metrics` 键,旧报告也没有。新报告 sha256 `9466cf3945a63621104157fb34291cb23205ddb045effa7e70f3c35647516443`。冻结记录 sha256 `1560f24ffea19116c904272907ff9cce24904960406676f3aada3c8c174cf98a`。重跑从 `2026-10-13T17:45:36Z` 到 `2026-10-13T18:02:16Z`。
|
||||
|
||||
## 本单不做
|
||||
|
||||
- 探针路径的大运用的是请求日期,并且不接收那罗延子运表。
|
||||
- 候选特征快照瘦身要动前端契约。
|
||||
- 引导窗口和换段邻近共用同一张大运起点表。
|
||||
|
||||
## 自测
|
||||
|
||||
- `tests/test_rect_scoring_perf_20261013.py` 与任务书点名的校正测试、`tests/test_rectification_*.py`(当时尚未改指针的完整性门除外)退出码 0。
|
||||
- 指针改完后 `tests/test_sealed_holdout_contract_freshness.py` 退出码 0。
|
||||
- 前端 `tsc --noEmit` 退出码 0,`npm run lint` 0 error。`rectification-v9-engine-contract.test.ts` 18 通过、0 失败、0 取消。`rectification-confirmation-gate.test.ts` 8 通过、0 失败、0 取消。
|
||||
- 没有跑前端全量,也没有跑 Python 快速门。
|
||||
@@ -467,7 +467,7 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-rect-scoring-perf-20261013.md` | — | 校正打分提速:引导窗口平方扫描、候选不变量重复算、响应带无读取点字段、候选数上限 241(BUG-1323~1325、1345) | 待领取 | `codex/rect-scoring-perf-20261013` |
|
||||
| `TASK-rect-scoring-perf-20261013.md` | `PROGRESS-rect-scoring-perf-20261013.md` | 校正打分提速:引导窗口平方扫描、候选不变量重复算、响应带无读取点字段、候选数上限 241(BUG-1323~1325、1345) | 待验收 | `codex/rect-scoring-perf-20261013` |
|
||||
| `TASK-node-runtime-resources-20261013.md` | — | 报告 worker abort 监听泄漏、本地 PG 适配层往返与元数据缓存、无上限内存表、用户端会话读缓存(BUG-1326~1328、1346,PG 与缓存部分 A/B) | 待领取 | `codex/node-runtime-resources-20261013` |
|
||||
| `TASK-home-sidebar-rerender-20261013.md` | — | 流式回答时侧栏每帧整栏重渲染(BUG-1329,A/B) | 待领取 | `codex/home-sidebar-rerender-20261013` |
|
||||
| `TASK-api-disk-cache-retention-20261013.md` | — | API 磁盘缓存只进不出、VedAstro 审计文件留存出生资料、失败快照久缓存、缓存键规范化(BUG-1330~1332,A/B) | 待领取 | `codex/api-disk-cache-retention-20261013` |
|
||||
|
||||
@@ -330,17 +330,27 @@ async function readEngineJsonTimed(
|
||||
const elapsedMs = Date.now() - started;
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const kind: EngineCallFailureKind = response.status === 429 ? "busy" : "http_error";
|
||||
const errorText = data && typeof data === "object"
|
||||
? String((data as Record<string, unknown>).error ?? (data as Record<string, unknown>).message ?? "")
|
||||
: "";
|
||||
const errorCode = data && typeof data === "object"
|
||||
? String((data as Record<string, unknown>).error_code ?? "")
|
||||
: "";
|
||||
const tooManyCandidates = response.status === 400
|
||||
&& (errorCode === "too_many_candidates" || errorText.includes("too_many_candidates"));
|
||||
const kind: EngineCallFailureKind = response.status === 429
|
||||
? "busy"
|
||||
: tooManyCandidates
|
||||
? "bad_payload"
|
||||
: "http_error";
|
||||
logEngineCallFailure({
|
||||
path,
|
||||
kind,
|
||||
elapsedMs,
|
||||
httpStatus: response.status,
|
||||
});
|
||||
const message = data && typeof data === "object"
|
||||
? String((data as Record<string, unknown>).error ?? (data as Record<string, unknown>).message ?? `Jyotish API ${response.status}`)
|
||||
: `Jyotish API ${response.status}`;
|
||||
throw new RectificationEngineError(persistenceCodeForKind(kind), message, {
|
||||
const message = errorText || `Jyotish API ${response.status}`;
|
||||
throw new RectificationEngineError(tooManyCandidates ? "too_many_candidates" : persistenceCodeForKind(kind), message, {
|
||||
kind,
|
||||
httpStatus: response.status,
|
||||
retryAfterSeconds: retryAfterFromHeaders(response.headers) ?? undefined,
|
||||
|
||||
@@ -105,8 +105,11 @@ const diagnosticReport = JSON.parse(readFileSync(
|
||||
// 原值(8):docs/research/sealed_holdout_rerun_upstream_sync6_2026_10_07.json
|
||||
// 新值(8):docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json
|
||||
// 原因(8):章动修正与 scoring-12 改了冻结身份(ERR-110,BUG-1275)。影子重跑不是发布指标,确认门行为与阈值不变。
|
||||
// 原值(9):docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json
|
||||
// 新值(9):docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json
|
||||
// 原因(9):BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录。确认门行为与阈值不变。
|
||||
const currentTreeRerun = JSON.parse(readFileSync(
|
||||
new URL("../../docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json", import.meta.url),
|
||||
new URL("../../docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json", import.meta.url),
|
||||
"utf8",
|
||||
)) as {
|
||||
frozen_record: { implementation_sha256: string };
|
||||
@@ -252,9 +255,10 @@ test("sealed holdout aggregates match the v3 report and still block confirmation
|
||||
// 原值(5):…upstream_sync5_2026_10_02.json / 新值(5):…astrologer_rulings_batch1_2026_10_03.json / 原因(5):TASK-astrologer-rulings-batch1 重新冻结(同上)。
|
||||
// 原值(6):…astrologer_rulings_batch1_2026_10_03.json / 新值(6):…upstream_sync6_2026_10_07.json / 原因(6):sync6 的 D5/D11 与 scoring-11 重新冻结(ERR-110,BUG-1260)。
|
||||
// 原值(7):…upstream_sync6_2026_10_07.json / 新值(7):…ephemeris_nutation_2026_10_08.json / 原因(7):章动修正与 scoring-12 重新冻结(ERR-110,BUG-1275)。
|
||||
// 原值(8):…ephemeris_nutation_2026_10_08.json / 新值(8):…scoring_perf_2026_10_13.json / 原因(8):BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录。
|
||||
assert.equal(
|
||||
productHoldout.current_tree_scorer.source_report,
|
||||
"docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json",
|
||||
"docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json",
|
||||
);
|
||||
assert.equal(currentTreeRerun.official_valid_independent_blind, false);
|
||||
assert.equal(productHoldout.current_tree_scorer.matches_metrics_scorer, false);
|
||||
|
||||
@@ -712,3 +712,30 @@ test("busy retries that still fail keep the busy kind", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("400 too_many_candidates is a bad request and is not retried", async () => {
|
||||
const waits: number[] = [];
|
||||
setEngineBusyWaitForTests(async (ms) => {
|
||||
waits.push(ms);
|
||||
});
|
||||
const stub = stubEngineSequence([{
|
||||
status: 400,
|
||||
body: { error: "too_many_candidates: limit=241 count=1440", error_code: "ERR_BAD_REQUEST" },
|
||||
}]);
|
||||
try {
|
||||
const result = await captureWarnings(() => runV9CandidateScore(SCORE_INPUT));
|
||||
assert.equal(result.result.status, "rejected");
|
||||
if (result.result.status === "rejected") {
|
||||
const error = result.result.reason;
|
||||
assert.ok(error instanceof RectificationEngineError);
|
||||
assert.equal(error.kind, "bad_payload");
|
||||
assert.equal(error.code, "too_many_candidates");
|
||||
assert.equal(error.httpStatus, 400);
|
||||
}
|
||||
assert.equal(waits.length, 0);
|
||||
assert.equal(stub.calls(), 1);
|
||||
} finally {
|
||||
stub.restore();
|
||||
setEngineBusyWaitForTests(null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"dataset_path": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"report_path": "references/real_case_calibration/minute_rectification_holdout_v3_report.json",
|
||||
"source_audit_status": "corrected_known_date_errors",
|
||||
"evaluated_on": "2026-10-08",
|
||||
"evaluated_on": "2026-10-13",
|
||||
"evaluated_on_scope": "latest_fixed_protocol_rerun_not_historical_runtime_metrics",
|
||||
"historical_source_audit_status": "invalidated_after_replay",
|
||||
"report_status": "invalidated_after_source_audit",
|
||||
@@ -24,7 +24,7 @@
|
||||
"evaluated_on": "2026-07-21"
|
||||
},
|
||||
"current_tree_scorer": {
|
||||
"implementation_sha256": "511158c8387658d51f6055bd7afd15730b8b25314510d48f52498ed737c824d7",
|
||||
"implementation_sha256": "1c146bf5273edf871d885e9e2e68ee1c8a2f71759b54b4b878f63e771f44197c",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
@@ -52,7 +52,7 @@
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "b72dbd61feec556c3863cb1b4496ded677c7077324a71b86e31722250a8d1528",
|
||||
"production_scoring_sha256": "89c146df190e8902731ea8111d1aff490fede00c919f529c7cf4c3e629a75579",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
@@ -62,9 +62,9 @@
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "12dc4049d8feeaf9e8f7df54666e7de7e8de6b45f195f52999bdbd52217908d9",
|
||||
"research_implementation_sha256": "085ca37ab70e1da18e6941161868618b22c9c9f09c043d0195de7f5650363453",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "cdc0e68edc770a2a1810fbdb68cbfd46dcd50b267c739c496fcc078c5e26b5b4",
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "9bfc0b8a06af6122fa6c3881706e64f28b9ba58b53d98ff55957d4741d820f3f",
|
||||
"scripts/dasha_analyzer.py": "5e53b1b4d4c7414c08dd0e7716f120a63c70ec3428ec93547f64d55c580da0e1",
|
||||
@@ -78,20 +78,20 @@
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "c7aaefd4b7b7ea98a5b94e0f7798e5d0139c7423d10ce4ebfff258a460d4456f",
|
||||
"scripts/rectification/api_service.py": "e316ced0f3796d0378a0fe1aad950ee25922ae8afd53add578ed59ca8b2f0f79",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "0afba494aa12899621173c3cc941c6c1d0be9cec0bdf4c4a1c9bcd004ab22de8",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "9bbadc246e6b8e94bb047522c3e5645613f508b3473ef40b38a207c77fdad58c",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "9df602c938f87a7b540e97c6c31a263f07b4ed0ccaeb1d2fac39b0d3d27ba0ed",
|
||||
"scripts/research/sealed_holdout_rerun.py": "a7f8a1c2230e33441e335810f13cfd723f3f8ad5b03a3657cb606e29d8559f19",
|
||||
"scripts/research/reported_offset_sweep.py": "bdcaac92ece761dbcbd35d5a8f751e6b4b11f8475ef8152a5ea4d36ceb38356a",
|
||||
"scripts/research/sealed_holdout_rerun.py": "27904b39a64ff5f1b95ae3c8fc65ca0f3a8b4d8e3f4b88fbbf5d85ffeb53db0d",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "c1f06967c3d4d214edadff139c24aa31e59fb628477174235abe14f799a99076"
|
||||
},
|
||||
@@ -100,7 +100,7 @@
|
||||
"matches_metrics_scorer": false,
|
||||
"official_eval_implementation_hash_matches": false,
|
||||
"official_eval_trial_count": 0,
|
||||
"source_report": "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json",
|
||||
"source_report": "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json",
|
||||
"fixed_protocol_rerun_trial_count": 20,
|
||||
"fixed_protocol_rerun_hash_matches": true,
|
||||
"metrics": {
|
||||
@@ -113,10 +113,10 @@
|
||||
}
|
||||
},
|
||||
"current_tree_fixed_protocol_rerun": {
|
||||
"evaluated_on": "2026-10-08",
|
||||
"report_path": "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.freeze.json",
|
||||
"implementation_sha256": "511158c8387658d51f6055bd7afd15730b8b25314510d48f52498ed737c824d7",
|
||||
"evaluated_on": "2026-10-13",
|
||||
"report_path": "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.freeze.json",
|
||||
"implementation_sha256": "1c146bf5273edf871d885e9e2e68ee1c8a2f71759b54b4b878f63e771f44197c",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
@@ -144,7 +144,7 @@
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "b72dbd61feec556c3863cb1b4496ded677c7077324a71b86e31722250a8d1528",
|
||||
"production_scoring_sha256": "89c146df190e8902731ea8111d1aff490fede00c919f529c7cf4c3e629a75579",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
@@ -154,9 +154,9 @@
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "12dc4049d8feeaf9e8f7df54666e7de7e8de6b45f195f52999bdbd52217908d9",
|
||||
"research_implementation_sha256": "085ca37ab70e1da18e6941161868618b22c9c9f09c043d0195de7f5650363453",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "cdc0e68edc770a2a1810fbdb68cbfd46dcd50b267c739c496fcc078c5e26b5b4",
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "9bfc0b8a06af6122fa6c3881706e64f28b9ba58b53d98ff55957d4741d820f3f",
|
||||
"scripts/dasha_analyzer.py": "5e53b1b4d4c7414c08dd0e7716f120a63c70ec3428ec93547f64d55c580da0e1",
|
||||
@@ -170,20 +170,20 @@
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "c7aaefd4b7b7ea98a5b94e0f7798e5d0139c7423d10ce4ebfff258a460d4456f",
|
||||
"scripts/rectification/api_service.py": "e316ced0f3796d0378a0fe1aad950ee25922ae8afd53add578ed59ca8b2f0f79",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "0afba494aa12899621173c3cc941c6c1d0be9cec0bdf4c4a1c9bcd004ab22de8",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "9bbadc246e6b8e94bb047522c3e5645613f508b3473ef40b38a207c77fdad58c",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "9df602c938f87a7b540e97c6c31a263f07b4ed0ccaeb1d2fac39b0d3d27ba0ed",
|
||||
"scripts/research/sealed_holdout_rerun.py": "a7f8a1c2230e33441e335810f13cfd723f3f8ad5b03a3657cb606e29d8559f19",
|
||||
"scripts/research/reported_offset_sweep.py": "bdcaac92ece761dbcbd35d5a8f751e6b4b11f8475ef8152a5ea4d36ceb38356a",
|
||||
"scripts/research/sealed_holdout_rerun.py": "27904b39a64ff5f1b95ae3c8fc65ca0f3a8b4d8e3f4b88fbbf5d85ffeb53db0d",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "c1f06967c3d4d214edadff139c24aa31e59fb628477174235abe14f799a99076"
|
||||
},
|
||||
@@ -201,15 +201,15 @@
|
||||
"verified_minute_claim_allowed": false,
|
||||
"metric_gates_passed": false,
|
||||
"events_per_case": 3,
|
||||
"boundary": "Previously exposed v3 shadow fixed-protocol rerun after the 2026-10-08 nutation fix (ERR-110, BUG-1275). Sidereal longitudes now include nutation and the scoring identity is scoring-12. Versus the 2026-09-20 historical report, changed_trial_count is 8. Not a release metric. Prior sync6 report and freeze preserved byte-for-byte.",
|
||||
"report_sha256": "9dec7743b12b5c25534096553a227809d285c1e4e32f6b10a58d80887a264e85",
|
||||
"freeze_record_sha256": "ed6c87e252a2a4d3212f7fb32d292623c0ddb48653e0550e7e5496441af274ef",
|
||||
"replay_started_at_utc": "2026-10-08T10:23:40.470851+00:00",
|
||||
"replay_finished_at_utc": "2026-10-08T10:23:44.334924+00:00",
|
||||
"boundary": "Previously exposed v3 shadow fixed-protocol rerun after the scoring speedup (ERR-110, BUG-1323 to BUG-1325 and BUG-1345). Trials and metrics match the 2026-10-08 nutation report. Versus the 2026-09-20 historical report, changed_trial_count stays 8. Not a release metric. Prior nutation report and freeze preserved byte-for-byte.",
|
||||
"report_sha256": "25e2e59ecab81b528f0f27369a8638a907ea551f57fdd8572db00fe25a139641",
|
||||
"freeze_record_sha256": "1ce7f7ac67221e10bb4d176f5e8af9299fbae7586717d83048b7e3c124e43e7c",
|
||||
"replay_started_at_utc": "2026-10-13T17:45:00.642234+00:00",
|
||||
"replay_finished_at_utc": "2026-10-13T17:45:04.577716+00:00",
|
||||
"previous_fixed_protocol_rerun": {
|
||||
"report_path": "docs/research/sealed_holdout_rerun_upstream_sync6_2026_10_07.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_upstream_sync6_2026_10_07.freeze.json",
|
||||
"superseded_reason": "Ephemeris nutation (ERR-110, BUG-1275). Prior sync6 report and freeze preserved byte-for-byte."
|
||||
"report_path": "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.freeze.json",
|
||||
"superseded_reason": "BUG-1323 to BUG-1325 changed frozen identity files. Output is unchanged, so ERR-110 stored a new record. Prior nutation report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_fixed_protocol_rerun": {
|
||||
@@ -330,25 +330,25 @@
|
||||
"verified_minute_claim_allowed": false
|
||||
},
|
||||
"current_tree_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_ephemeris_nutation_2026_10_08.json",
|
||||
"report_sha256": "b433a906280a5b12e4e48fd5e86292b73d21a627f490e3083d719aceea3252c9",
|
||||
"freeze_record_path": "docs/research/reported_offset_ephemeris_nutation_2026_10_08.freeze.json",
|
||||
"freeze_record_sha256": "bc43921250cdcc160fb51d88ebcae0262d180e18c0ab2b14791605b09d639d39",
|
||||
"report_path": "docs/research/reported_offset_scoring_perf_2026_10_13.json",
|
||||
"report_sha256": "9466cf3945a63621104157fb34291cb23205ddb045effa7e70f3c35647516443",
|
||||
"freeze_record_path": "docs/research/reported_offset_scoring_perf_2026_10_13.freeze.json",
|
||||
"freeze_record_sha256": "1560f24ffea19116c904272907ff9cce24904960406676f3aada3c8c174cf98a",
|
||||
"scope": "reported_offset_sensitivity_not_product_accuracy",
|
||||
"trial_count": 900,
|
||||
"official_valid_independent_blind": false,
|
||||
"official_blind_trial_count": 0,
|
||||
"must_not_use_for_tuning": true,
|
||||
"must_not_claim_as_release_metrics": true,
|
||||
"replay_started_at_utc": "2026-10-08T10:23:40.483622+00:00",
|
||||
"replay_finished_at_utc": "2026-10-08T10:38:50.744210+00:00",
|
||||
"boundary": "Previously exposed reported-offset sensitivity after the 2026-10-08 nutation fix (ERR-110, BUG-1275). Versus the 2026-09-20 historical report, changed_trial_count is 581 of 900. Not a release metric. No tuning. Prior sync6 report and freeze preserved byte-for-byte.",
|
||||
"replay_started_at_utc": "2026-10-13T17:45:36.012964+00:00",
|
||||
"replay_finished_at_utc": "2026-10-13T18:02:16.333181+00:00",
|
||||
"boundary": "Previously exposed reported-offset sensitivity after the scoring speedup (ERR-110, BUG-1323 to BUG-1325 and BUG-1345). Summary and trials match the 2026-10-08 nutation report. Versus the 2026-09-20 historical report, changed_trial_count stays 581 of 900. Not a release metric. No tuning. Prior nutation report and freeze preserved byte-for-byte.",
|
||||
"previous_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_upstream_sync6_2026_10_07.json",
|
||||
"report_sha256": "1b9c9c18b6820a2beca8d7ddef20b6fa0778c5d43dd987c0c214e2f9ff00660f",
|
||||
"freeze_record_path": "docs/research/reported_offset_upstream_sync6_2026_10_07.freeze.json",
|
||||
"freeze_record_sha256": "65c7ab537b3fecc6d3e175bb2ab702b5630fe41bcff40bd551917cc32977b366",
|
||||
"superseded_reason": "Ephemeris nutation (ERR-110, BUG-1275). Prior sync6 report and freeze preserved byte-for-byte. Versus the 2026-09-20 historical report, changed_trial_count is 581 of 900. Not used for tuning."
|
||||
"report_path": "docs/research/reported_offset_ephemeris_nutation_2026_10_08.json",
|
||||
"report_sha256": "b433a906280a5b12e4e48fd5e86292b73d21a627f490e3083d719aceea3252c9",
|
||||
"freeze_record_path": "docs/research/reported_offset_ephemeris_nutation_2026_10_08.freeze.json",
|
||||
"freeze_record_sha256": "bc43921250cdcc160fb51d88ebcae0262d180e18c0ab2b14791605b09d639d39",
|
||||
"superseded_reason": "BUG-1323 to BUG-1325 changed frozen identity files. Output is unchanged, so ERR-110 stored a new record. Versus the 2026-09-20 historical report, changed_trial_count stays 581 of 900. Prior nutation report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_reported_offset_replay": {
|
||||
|
||||
@@ -127,6 +127,7 @@ def _active_narayana(
|
||||
birth_at: datetime,
|
||||
event_at: datetime,
|
||||
periods: list[dict[str, Any]] | None = None,
|
||||
antardasha_by_md: dict[int, list[dict[str, Any]]] | None = None,
|
||||
) -> tuple[int | None, int | None]:
|
||||
if periods is None:
|
||||
periods = narayana_dasha.calc_narayana_mahadasha(
|
||||
@@ -134,10 +135,30 @@ def _active_narayana(
|
||||
planet_longitudes,
|
||||
)
|
||||
age = max((event_at - birth_at).total_seconds() / (365.2425 * 86_400), 0.0)
|
||||
active = narayana_dasha.get_current_narayana_dasha(periods, age)
|
||||
major = active.get("md") or {}
|
||||
minor = active.get("ad") or {}
|
||||
return major.get("sign_idx"), minor.get("sign_idx")
|
||||
if antardasha_by_md is None:
|
||||
active = narayana_dasha.get_current_narayana_dasha(periods, age)
|
||||
major = active.get("md") or {}
|
||||
minor = active.get("ad") or {}
|
||||
return major.get("sign_idx"), minor.get("sign_idx")
|
||||
# BUG-1324: same MD/AD lookup as narayana_dasha.get_current_narayana_dasha
|
||||
# lines 769-800 (total_cycle, cycle_start, age_in_cycle, MD loop, AD loop).
|
||||
# PD (lines 801-812) is unused by scoring and is not computed here.
|
||||
if not periods:
|
||||
return None, None
|
||||
total_cycle = sum(p["years"] for p in periods)
|
||||
if total_cycle == 0:
|
||||
return None, None
|
||||
cycle_start = min(float(p.get("start_age", 0)) for p in periods)
|
||||
age_in_cycle = ((age - cycle_start) % total_cycle) + cycle_start
|
||||
for period in periods:
|
||||
if period["start_age"] <= age_in_cycle < period["end_age"]:
|
||||
md_sign = period.get("sign_idx")
|
||||
ads = antardasha_by_md.get(period["sign_idx"]) or []
|
||||
for ad in ads:
|
||||
if ad["start_age"] <= age_in_cycle < ad["end_age"]:
|
||||
return md_sign, ad.get("sign_idx")
|
||||
return md_sign, None
|
||||
return None, None
|
||||
|
||||
|
||||
def _varga_chart(charts: dict, prefix: str) -> dict | None:
|
||||
@@ -198,13 +219,15 @@ def _score_event(
|
||||
vimshottari: tuple[str, str, str],
|
||||
narayana: tuple[int | None, int | None],
|
||||
arudha_padas: dict,
|
||||
functional: dict | None = None,
|
||||
) -> CandidateEvidence:
|
||||
_, target_houses = DOMAIN_CONFIG[event["domain"]]
|
||||
ascendant_index = int(natal_chart["ascendant"]["lon"] // 30)
|
||||
target_lords = _house_lords(ascendant_index, target_houses)
|
||||
functional = functional_benefics.derive_functional_benefic_malefic(
|
||||
natal_chart["ascendant"].get("sign")
|
||||
)
|
||||
if functional is None:
|
||||
functional = functional_benefics.derive_functional_benefic_malefic(
|
||||
natal_chart["ascendant"].get("sign")
|
||||
)
|
||||
functional_benefics_set = set(functional.get("functional_benefics") or [])
|
||||
functional_malefics_set = set(functional.get("functional_malefics") or [])
|
||||
major_lord, minor_lord, pratyantar_lord = vimshottari
|
||||
@@ -542,6 +565,18 @@ def build_candidate_static_context(
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
narayana_periods = None
|
||||
functional_profile = functional_benefics.derive_functional_benefic_malefic(
|
||||
chart["ascendant"].get("sign")
|
||||
)
|
||||
narayana_antardasha_by_md = None
|
||||
if narayana_periods:
|
||||
narayana_antardasha_by_md = {
|
||||
period["sign_idx"]: narayana_dasha.calc_narayana_antardasha(
|
||||
narayana_periods,
|
||||
period["sign_idx"],
|
||||
)
|
||||
for period in narayana_periods
|
||||
}
|
||||
|
||||
birth_info = chart.get("birth_info") if isinstance(chart.get("birth_info"), dict) else {}
|
||||
kp_snapshot = observe_kp_cusps(
|
||||
@@ -595,6 +630,8 @@ def build_candidate_static_context(
|
||||
"shadbala_result": shadbala_result,
|
||||
"vimshottari_timeline": vimshottari_timeline,
|
||||
"narayana_periods": narayana_periods,
|
||||
"functional_profile": functional_profile,
|
||||
"narayana_antardasha_by_md": narayana_antardasha_by_md,
|
||||
}
|
||||
|
||||
|
||||
@@ -649,6 +686,7 @@ def _candidate_row(
|
||||
candidate_at,
|
||||
event_at,
|
||||
context.get("narayana_periods"),
|
||||
context.get("narayana_antardasha_by_md"),
|
||||
)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
missing_layers.append("Narayana_MD_AD")
|
||||
@@ -664,6 +702,7 @@ def _candidate_row(
|
||||
vimshottari=vimshottari,
|
||||
narayana=narayana,
|
||||
arudha_padas=arudha_padas,
|
||||
functional=context.get("functional_profile"),
|
||||
))
|
||||
transit_rules = _controlled_transit_rules(
|
||||
request,
|
||||
|
||||
@@ -4,7 +4,9 @@ from typing import Any, Mapping, Sequence
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
from scripts.active_rectification_events import build_candidate_result_summary
|
||||
from scripts.offline_compute_mixins import BadRequest
|
||||
from scripts.rectification.candidate_feature_service import build_candidate_feature_snapshot
|
||||
from scripts.rectification.candidate_window import enumerate_candidate_window
|
||||
from scripts.rectification.contracts import (
|
||||
EVENT_CONTRACT_VERSION,
|
||||
RectificationRequest,
|
||||
@@ -195,6 +197,35 @@ def _rectification_report(
|
||||
}
|
||||
|
||||
|
||||
MAX_CANDIDATES_PER_REQUEST = 241
|
||||
|
||||
|
||||
class TooManyCandidates(BadRequest):
|
||||
"""Rejected before any chart is computed.
|
||||
|
||||
The API server maps every BadRequest to HTTP 400 with error_code
|
||||
ERR_BAD_REQUEST and this task must not edit that server. The token
|
||||
too_many_candidates lives in the message, with the cap and the count.
|
||||
"""
|
||||
|
||||
code = "too_many_candidates"
|
||||
|
||||
def __init__(self, count: int) -> None:
|
||||
self.count = int(count)
|
||||
self.limit = MAX_CANDIDATES_PER_REQUEST
|
||||
super().__init__(f"too_many_candidates: limit={self.limit} count={self.count}")
|
||||
|
||||
|
||||
def _candidate_count(request: RectificationRequest) -> int:
|
||||
return len(enumerate_candidate_window(request))
|
||||
|
||||
|
||||
def _reject_if_too_many_candidates(request: RectificationRequest) -> None:
|
||||
count = _candidate_count(request)
|
||||
if count > MAX_CANDIDATES_PER_REQUEST:
|
||||
raise TooManyCandidates(count)
|
||||
|
||||
|
||||
def engine_scoring_versions() -> dict[str, str]:
|
||||
"""Identity fields also returned by `/api/rectification/v5/score`, without scoring."""
|
||||
return {
|
||||
@@ -219,7 +250,14 @@ def candidate_features(request: RectificationRequest) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
def score_candidates(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
include_internal: bool = False,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(request)
|
||||
scoring_request = scoreable_request(request)
|
||||
built = build_event_contribution_matrix(scoring_request)
|
||||
rows = score_from_matrix(scoring_request, built)
|
||||
@@ -272,7 +310,7 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
)
|
||||
rectification_report["next_step_codes"] = candidate_summary["next_step_codes"]
|
||||
candidate_summary["report"] = rectification_report
|
||||
return {
|
||||
payload = {
|
||||
"result_id": result_id,
|
||||
"algorithm_version": ALGORITHM_VERSION,
|
||||
"event_contract_version": EVENT_CONTRACT_VERSION,
|
||||
@@ -286,11 +324,9 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
"conflicting_event_ids": [item["event_id"] for item in row["evidence"] if item["points"] < 0],
|
||||
} for row in rows],
|
||||
"candidate_decisions": candidate_decisions,
|
||||
"candidate_decision_receipt": decision_receipt,
|
||||
"decision_receipt": decision_receipt,
|
||||
"execution_ledger_version": EXECUTION_LEDGER_VERSION,
|
||||
"execution_ledger": execution_ledger,
|
||||
"event_contribution_matrix": built["matrix"],
|
||||
"candidate_feature_snapshot": build_candidate_feature_snapshot(
|
||||
scoring_request, spec_hash, built.get("static_contexts")
|
||||
),
|
||||
@@ -317,10 +353,20 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
|
||||
"margin_percent": decision_receipt["margin_percent"],
|
||||
"can_confirm_exact_minute": decision_receipt["confirmation_allowed"],
|
||||
}
|
||||
if include_internal:
|
||||
payload["candidate_decision_receipt"] = decision_receipt
|
||||
payload["event_contribution_matrix"] = built["matrix"]
|
||||
return payload
|
||||
|
||||
|
||||
def diagnostics(request: RectificationRequest) -> dict[str, Any]:
|
||||
scored = score_candidates(request)
|
||||
def diagnostics(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(request)
|
||||
scored = score_candidates(request, enforce_candidate_cap=enforce_candidate_cap)
|
||||
return {
|
||||
"result_id": scored["result_id"],
|
||||
"algorithm_version": scored["algorithm_version"],
|
||||
@@ -328,7 +374,6 @@ def diagnostics(request: RectificationRequest) -> dict[str, Any]:
|
||||
"decision_policy_version": scored["decision_policy_version"],
|
||||
"calculation_spec_hash": scored["calculation_spec_hash"],
|
||||
"candidate_decisions": scored["candidate_decisions"],
|
||||
"candidate_decision_receipt": scored["candidate_decision_receipt"],
|
||||
"decision_receipt": scored["decision_receipt"],
|
||||
"execution_ledger_version": scored["execution_ledger_version"],
|
||||
"execution_ledger": scored["execution_ledger"],
|
||||
@@ -460,7 +505,11 @@ def _normalize_relative_support(raw: Sequence[float]) -> list[float]:
|
||||
return shares
|
||||
|
||||
|
||||
def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
def block_scan(
|
||||
request: RectificationRequest,
|
||||
*,
|
||||
enforce_candidate_cap: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Aggregate event scores into declared periods or caller-supplied sub-blocks."""
|
||||
requested_step = request.get("minute_step")
|
||||
if isinstance(requested_step, int) and requested_step > 1:
|
||||
@@ -472,7 +521,9 @@ def block_scan(request: RectificationRequest) -> dict[str, Any]:
|
||||
if step <= 1:
|
||||
step = 10
|
||||
scoring_request = {**request, "minute_step": step}
|
||||
scored = score_candidates(scoring_request)
|
||||
if enforce_candidate_cap:
|
||||
_reject_if_too_many_candidates(scoring_request)
|
||||
scored = score_candidates(scoring_request, enforce_candidate_cap=enforce_candidate_cap)
|
||||
events_by_id = {
|
||||
str(event.get("id") or ""): event
|
||||
for event in request.get("events") or []
|
||||
|
||||
@@ -130,6 +130,8 @@ def merge_transition_proximity(
|
||||
for context in static_contexts
|
||||
if isinstance(context, dict) and (time := _context_time(context))
|
||||
}
|
||||
# BUG-1324: one unfiltered dasha walk per candidate, then filter by event year.
|
||||
# Released when this function returns. Not a module-level cache.
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
pd_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
@@ -156,14 +158,15 @@ def merge_transition_proximity(
|
||||
# Legacy time-only contexts still use the request's date.
|
||||
candidate_at = context.get("candidate_at")
|
||||
candidate_date = candidate_at.date().isoformat() if isinstance(candidate_at, datetime) else birth_date
|
||||
vim_key = (candidate_date, round(float(moon), 6), lo, hi)
|
||||
moon_key = round(float(moon), 6)
|
||||
vim_key = (candidate_date, moon_key)
|
||||
if vim_key not in vim_cache:
|
||||
vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), lo, hi)
|
||||
vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), 1, 9999)
|
||||
pd_cache[vim_key] = _vim_start_dates(
|
||||
candidate_date,
|
||||
float(moon),
|
||||
lo,
|
||||
hi,
|
||||
1,
|
||||
9999,
|
||||
include_pratyantar=True,
|
||||
)
|
||||
planets = context.get("planet_longitudes") or {}
|
||||
@@ -171,25 +174,26 @@ def merge_transition_proximity(
|
||||
narayana_key = (
|
||||
candidate_date,
|
||||
int(asc) if isinstance(asc, int) else None,
|
||||
lo,
|
||||
hi,
|
||||
round(float(moon), 6),
|
||||
moon_key,
|
||||
)
|
||||
if narayana_key not in narayana_cache:
|
||||
narayana_cache[narayana_key] = (
|
||||
_narayana_start_dates(int(asc), planets, candidate_date, lo, hi)
|
||||
_narayana_start_dates(int(asc), planets, candidate_date, 1, 9999)
|
||||
if isinstance(asc, int) and isinstance(planets, dict)
|
||||
else None
|
||||
)
|
||||
ad_starts = vim_cache[vim_key]
|
||||
ad_starts = [item for item in vim_cache[vim_key] if lo <= item.year <= hi]
|
||||
ad_set = set(ad_starts)
|
||||
pd_only = [item for item in pd_cache[vim_key] if item not in ad_set]
|
||||
pd_only = [item for item in pd_cache[vim_key] if lo <= item.year <= hi and item not in ad_set]
|
||||
narayana_starts = narayana_cache[narayana_key]
|
||||
if narayana_starts is not None:
|
||||
narayana_starts = [item for item in narayana_starts if lo <= item.year <= hi]
|
||||
scored = score_transition_proximity(
|
||||
event_date=event_date,
|
||||
precision=precision,
|
||||
vim_starts=ad_starts,
|
||||
vim_pd_starts=pd_only,
|
||||
narayana_starts=narayana_cache[narayana_key] or [],
|
||||
narayana_starts=narayana_starts or [],
|
||||
)
|
||||
if scored["points"] <= 0 and not scored["rule_ids"]:
|
||||
continue
|
||||
|
||||
@@ -814,6 +814,7 @@ def _score_year(
|
||||
vimshottari=vimshottari,
|
||||
narayana=narayana,
|
||||
arudha_padas=context.get("arudha_padas") or {},
|
||||
functional=context.get("functional_profile"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1913,8 +1914,12 @@ def guided_collect_windows(
|
||||
vim_cache=vim_cache,
|
||||
narayana_cache=narayana_cache,
|
||||
)
|
||||
# BUG-1323: gathered depends only on (track, boundary index). Prefetch once
|
||||
# per candidate and skip a boundary after the one==two check has passed.
|
||||
work_tracks = [starts_by_time.get(_context_time(context) or "") or {} for context in work]
|
||||
rows: list[dict[str, Any]] = []
|
||||
seen: set[tuple[int, int, int, str]] = set()
|
||||
done: set[tuple[str, int]] = set()
|
||||
for left, right in _representative_pairs(work):
|
||||
left_time = str(_context_time(left) or "")
|
||||
right_time = str(_context_time(right) or "")
|
||||
@@ -1928,10 +1933,12 @@ def guided_collect_windows(
|
||||
for index, (one, two) in enumerate(zip(left_dates, right_dates)):
|
||||
if one == two:
|
||||
continue
|
||||
if (track, index) in done:
|
||||
continue
|
||||
done.add((track, index))
|
||||
gathered: list[date] = []
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
dates = (starts_by_time.get(time or "") or {}).get(track) or []
|
||||
for tracks in work_tracks:
|
||||
dates = tracks.get(track) or []
|
||||
if index < len(dates):
|
||||
gathered.append(dates[index])
|
||||
if len(gathered) < 2 or min(gathered) == max(gathered):
|
||||
|
||||
@@ -50,7 +50,7 @@ def worker(root: Path, dated: bool):
|
||||
request = make_request(case)
|
||||
if dated:
|
||||
request["candidate_intervals"] = [{"start_at": f'{request["birth_date"]}T{request["start_time"]}', "end_at": f'{request["birth_date"]}T{request["end_time"]}'}]
|
||||
result.append(projection(score(request), width))
|
||||
result.append(projection(score(request, include_internal=True), width))
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bench and canonical dump for the 2026-10-13 rectification scoring speedup.
|
||||
|
||||
Read-only aside from the dump files the caller asks for. Cross-process dumps
|
||||
must be launched with PYTHONHASHSEED=0. The baseline tree has no
|
||||
include_internal parameter; that call falls back to the old signature.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.rectification.api_service import block_scan, diagnostics, score_candidates
|
||||
from scripts.rectification.contracts import normalize_rectification_request
|
||||
from scripts.rectification import event_probes
|
||||
from scripts.rectification import scoring_service
|
||||
from scripts import active_rectification_event_engine as event_engine
|
||||
|
||||
FROZEN_TODAY = date(2026, 9, 16)
|
||||
EVENT_SPECS = (
|
||||
("education", "education_start", "2008-01-01", "2008-12-31", "year"),
|
||||
("career", "career_entry", "2012-06-01", "2012-06-30", "month"),
|
||||
("relocation", "relocation", "2014-03-01", "2014-03-31", "month"),
|
||||
("relationship", "relationship_commitment", "2016-10-01", "2016-10-31", "month"),
|
||||
("career", "promotion", "2018-01-01", "2018-12-31", "year"),
|
||||
("finance", "asset_change", "2019-05-01", "2019-05-31", "month"),
|
||||
("family", "family_event", "2020-08-01", "2020-08-31", "month"),
|
||||
("health", "self_health_event", "2022-01-01", "2022-12-31", "year"),
|
||||
)
|
||||
|
||||
|
||||
class _FrozenDate(date):
|
||||
@classmethod
|
||||
def today(cls) -> date:
|
||||
return FROZEN_TODAY
|
||||
|
||||
|
||||
def _events() -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for index, (domain, kind, start, end, precision) in enumerate(EVENT_SPECS, start=1):
|
||||
rows.append({
|
||||
"id": f"00000000-0000-4000-8000-{index:012d}",
|
||||
"domain": domain,
|
||||
"event_kind": kind,
|
||||
"date_start": start,
|
||||
"date_end": end,
|
||||
"precision": precision,
|
||||
"summary": "虚构",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def make_request(start: str, end: str, **extra: Any) -> dict[str, Any]:
|
||||
body = {
|
||||
"birth_date": "1990-01-01",
|
||||
"start_time": start,
|
||||
"end_time": end,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"events": _events(),
|
||||
**extra,
|
||||
}
|
||||
return normalize_rectification_request(body, today=FROZEN_TODAY)
|
||||
|
||||
|
||||
def _strip(value: Any, keys: set[str]) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {key: _strip(item, keys) for key, item in value.items() if key not in keys}
|
||||
if isinstance(value, list):
|
||||
return [_strip(item, keys) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _canonical(value: Any) -> bytes:
|
||||
return json.dumps(value, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode("ascii")
|
||||
|
||||
|
||||
def _score(request: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
return score_candidates(request, include_internal=True)
|
||||
except TypeError:
|
||||
return score_candidates(request)
|
||||
|
||||
|
||||
def _call(kind: str, request: dict[str, Any], *, internal: bool = False) -> dict[str, Any]:
|
||||
if kind == "score":
|
||||
# Bench times the product call. Dump asks for the internal matrix so
|
||||
# the baseline tree, which always returned it, stays comparable.
|
||||
if internal:
|
||||
return _score(request)
|
||||
return score_candidates(request)
|
||||
if kind == "block":
|
||||
return block_scan(request)
|
||||
if kind == "diagnostics":
|
||||
return diagnostics(request)
|
||||
raise ValueError(kind)
|
||||
|
||||
|
||||
def scenarios() -> list[dict[str, Any]]:
|
||||
from tests.test_rectification_engine_memoization import public_score_request
|
||||
|
||||
rows = [
|
||||
{"name": "12:00-12:20", "kind": "score", "request": make_request("12:00", "12:20")},
|
||||
{"name": "11:45-12:15", "kind": "score", "request": make_request("11:45", "12:15")},
|
||||
{"name": "11:00-13:00", "kind": "score", "request": make_request("11:00", "13:00")},
|
||||
{"name": "23:20-00:40", "kind": "score", "request": make_request("23:20", "00:40")},
|
||||
{"name": "declined-relationship-career", "kind": "score", "request": make_request(
|
||||
"11:30", "12:30", declined_domains=["relationship", "career"],
|
||||
)},
|
||||
{"name": "refresh-probes-columns", "kind": "score", "request": make_request(
|
||||
"11:30", "12:30", refresh_probes=True, column_times=["11:40", "12:10", "12:20"],
|
||||
)},
|
||||
{"name": "minute-step-2", "kind": "score", "request": make_request("10:00", "13:00", minute_step=2)},
|
||||
{"name": "block-step-10", "kind": "block", "request": make_request("00:00", "23:59", minute_step=10)},
|
||||
{"name": "block-step-5", "kind": "block", "request": make_request("10:00", "14:00", minute_step=5)},
|
||||
{"name": "public-score-request", "kind": "score", "request": normalize_rectification_request(
|
||||
public_score_request(), today=FROZEN_TODAY,
|
||||
)},
|
||||
{"name": "diagnostics-11:00-13:00", "kind": "diagnostics", "request": make_request("11:00", "13:00")},
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
def _strip_keys(kind: str) -> set[str]:
|
||||
# diagnostics no longer repeats the receipt. Drop it on both sides so the
|
||||
# remaining payload is what must stay byte-identical.
|
||||
if kind == "diagnostics":
|
||||
return {"column_compare_ms", "candidate_decision_receipt"}
|
||||
return {"column_compare_ms"}
|
||||
|
||||
|
||||
class _SegmentTimer:
|
||||
def __init__(self) -> None:
|
||||
self.totals = {
|
||||
"guided_collect_windows": 0.0,
|
||||
"merge_transition_proximity": 0.0,
|
||||
"get_current_narayana_dasha": 0.0,
|
||||
"derive_functional_benefic_malefic": 0.0,
|
||||
"build_candidate_static_context": 0.0,
|
||||
}
|
||||
|
||||
def wrap(self, owner: Any, name: str, key: str) -> Callable[[], None]:
|
||||
original = getattr(owner, name)
|
||||
|
||||
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
return original(*args, **kwargs)
|
||||
finally:
|
||||
self.totals[key] += time.perf_counter() - started
|
||||
|
||||
setattr(owner, name, wrapped)
|
||||
|
||||
def restore() -> None:
|
||||
setattr(owner, name, original)
|
||||
|
||||
return restore
|
||||
|
||||
|
||||
def _median(samples: list[float]) -> float:
|
||||
return round(statistics.median(samples), 4)
|
||||
|
||||
|
||||
def bench() -> dict[str, Any]:
|
||||
jobs = [
|
||||
("12:00-12:20", "score", make_request("12:00", "12:20")),
|
||||
("11:45-12:15", "score", make_request("11:45", "12:15")),
|
||||
("11:00-13:00", "score", make_request("11:00", "13:00")),
|
||||
("block-step-10", "block", make_request("00:00", "23:59", minute_step=10)),
|
||||
]
|
||||
rows = []
|
||||
event_probes.date = _FrozenDate
|
||||
try:
|
||||
for name, kind, request in jobs:
|
||||
samples = []
|
||||
segments = []
|
||||
sizes = []
|
||||
counts = []
|
||||
for _ in range(5):
|
||||
timer = _SegmentTimer()
|
||||
restores = [
|
||||
timer.wrap(event_probes, "guided_collect_windows", "guided_collect_windows"),
|
||||
timer.wrap(scoring_service, "merge_transition_proximity", "merge_transition_proximity"),
|
||||
timer.wrap(event_engine.narayana_dasha, "get_current_narayana_dasha", "get_current_narayana_dasha"),
|
||||
timer.wrap(event_engine.functional_benefics, "derive_functional_benefic_malefic", "derive_functional_benefic_malefic"),
|
||||
timer.wrap(event_engine, "build_candidate_static_context", "build_candidate_static_context"),
|
||||
]
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = _call(kind, request)
|
||||
finally:
|
||||
for restore in restores:
|
||||
restore()
|
||||
elapsed = time.perf_counter() - started
|
||||
samples.append(elapsed)
|
||||
segments.append({key: round(value, 4) for key, value in timer.totals.items()})
|
||||
sizes.append(len(_canonical(_strip(result, _strip_keys(kind)))))
|
||||
counts.append(result.get("candidate_count") or len(result.get("candidate_scores") or []))
|
||||
median_segments: dict[str, float] = {}
|
||||
for key in segments[0]:
|
||||
median_segments[key] = _median([item[key] for item in segments])
|
||||
rows.append({
|
||||
"window": name,
|
||||
"candidate_count": counts[0],
|
||||
"median_seconds": _median(samples),
|
||||
"segment_median_seconds": median_segments,
|
||||
"response_bytes": int(statistics.median(sizes)),
|
||||
})
|
||||
finally:
|
||||
event_probes.date = date
|
||||
return {"runs": 5, "rows": rows}
|
||||
|
||||
|
||||
def dump(out_dir: Path) -> list[dict[str, str]]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
written = []
|
||||
event_probes.date = _FrozenDate
|
||||
try:
|
||||
for row in scenarios():
|
||||
result = _call(row["kind"], row["request"], internal=True)
|
||||
payload = _strip(result, _strip_keys(row["kind"]))
|
||||
raw = _canonical(payload)
|
||||
digest = hashlib.sha256(raw).hexdigest()
|
||||
path = out_dir / f"{row['name'].replace(':', '')}.json"
|
||||
path.write_bytes(raw)
|
||||
written.append({"name": row["name"], "sha256": digest, "bytes": str(len(raw))})
|
||||
print(f"{row['name']}\t{digest}\t{len(raw)}")
|
||||
finally:
|
||||
event_probes.date = date
|
||||
return written
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=("bench", "dump"))
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "artifacts" / "rect-scoring-perf-20261013")
|
||||
args = parser.parse_args()
|
||||
if args.command == "bench":
|
||||
print(json.dumps(bench(), ensure_ascii=True, indent=2))
|
||||
else:
|
||||
dump(args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,8 +30,11 @@ from scripts.research.sealed_holdout_rerun import (
|
||||
OFFSETS = (-30, -20, -15, -10, -8, -5, -3, 0, 3, 5, 8, 10, 15, 20, 30)
|
||||
RADII = (15, 30, 60)
|
||||
MINUTE_STEP = 1
|
||||
FREEZE = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_ephemeris_nutation_2026_10_08.json"
|
||||
# 原值: docs/research/reported_offset_ephemeris_nutation_2026_10_08.{freeze.json,json}
|
||||
# 新值: docs/research/reported_offset_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 原因: BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/reported_offset_scoring_perf_2026_10_13.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_scoring_perf_2026_10_13.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/reported_offset_2026_09_20.json"
|
||||
|
||||
|
||||
|
||||
@@ -27,8 +27,11 @@ from scripts.minute_rectification_feature_facts_v4 import build_feature_fact_row
|
||||
from scripts.minute_rectification_holdout_validator import validate
|
||||
|
||||
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.json"
|
||||
# 原值: docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08.{freeze.json,json}
|
||||
# 新值: docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 原因: BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/sealed_holdout_rerun_2026_09_20.json"
|
||||
ARCHIVE = ROOT / "docs/research/history/rectification_pre_cross_midnight_2026_09_20"
|
||||
PRODUCTION_FILES = [
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Baseline copies for TASK-rect-scoring-perf-20261013.
|
||||
|
||||
Source commit: 77057a819
|
||||
git show 77057a819:scripts/rectification/event_probes.py sha256: 9bbadc246e6b8e94bb047522c3e5645613f508b3473ef40b38a207c77fdad58c
|
||||
git show 77057a819:scripts/rectification/dasha_transition_proximity.py sha256: 0afba494aa12899621173c3cc941c6c1d0be9cec0bdf4c4a1c9bcd004ab22de8
|
||||
|
||||
The helpers these functions call are unchanged by this task and are imported
|
||||
from the current modules. Only guided_collect_windows and
|
||||
merge_transition_proximity differ from the copies below.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Sequence
|
||||
|
||||
from scripts.rectification.dasha_transition_proximity import (
|
||||
_context_time as _proximity_context_time,
|
||||
representative_event_date,
|
||||
score_transition_proximity,
|
||||
)
|
||||
from scripts.rectification.event_probes import (
|
||||
GUIDED_ANY_DOMAIN,
|
||||
GUIDED_COLLECT_LIMIT,
|
||||
GUIDED_DOMAIN_ORDER,
|
||||
_GUIDED_TRACK_DOMAIN,
|
||||
_birth_year,
|
||||
_context_time as _probe_context_time,
|
||||
_guided_track_starts,
|
||||
_guided_year_windows,
|
||||
_narayana_start_dates,
|
||||
_remaining_contexts,
|
||||
_representative_pairs,
|
||||
_scoreable,
|
||||
_static_contexts,
|
||||
_vim_start_dates,
|
||||
)
|
||||
|
||||
# Both copies call `_context_time`. They are rebound onto separate globals
|
||||
# below so each keeps the helper it had in its source module.
|
||||
_context_time = _probe_context_time
|
||||
|
||||
def guided_collect_windows(
|
||||
request: dict[str, Any],
|
||||
built: dict[str, Any],
|
||||
*,
|
||||
candidate_times: Sequence[str],
|
||||
today: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Boundary windows for guided collect. Does not change auto probe generation."""
|
||||
birth_date = str(request.get("birth_date") or "").strip()
|
||||
birth_year = _birth_year(birth_date)
|
||||
if birth_year is None:
|
||||
return []
|
||||
try:
|
||||
datetime.strptime(birth_date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return []
|
||||
remaining = _remaining_contexts(built, candidate_times)
|
||||
if len(remaining) < 2:
|
||||
remaining = _static_contexts(built)
|
||||
work = [item for item in remaining if _scoreable(item)]
|
||||
if len(work) < 2:
|
||||
return []
|
||||
declined = {
|
||||
str(item).strip()
|
||||
for item in (request.get("declined_domains") or [])
|
||||
if str(item).strip() in GUIDED_DOMAIN_ORDER
|
||||
}
|
||||
now = today or date.today()
|
||||
lo, hi = birth_year + 5, min(now.year, birth_year + 80)
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
starts_by_time: dict[str, dict[str, list[date]]] = {}
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
if not time:
|
||||
continue
|
||||
starts_by_time[time] = _guided_track_starts(
|
||||
context,
|
||||
birth_date=birth_date,
|
||||
lo=lo,
|
||||
hi=hi,
|
||||
include_pratyantar=True,
|
||||
varga_narayana=True,
|
||||
vim_cache=vim_cache,
|
||||
narayana_cache=narayana_cache,
|
||||
)
|
||||
rows: list[dict[str, Any]] = []
|
||||
seen: set[tuple[int, int, int, str]] = set()
|
||||
for left, right in _representative_pairs(work):
|
||||
left_time = str(_context_time(left) or "")
|
||||
right_time = str(_context_time(right) or "")
|
||||
left_tracks = starts_by_time.get(left_time) or {}
|
||||
right_tracks = starts_by_time.get(right_time) or {}
|
||||
for track in ("vim", "nara", "nara:d9", "nara:d10"):
|
||||
left_dates = left_tracks.get(track) or []
|
||||
right_dates = right_tracks.get(track) or []
|
||||
if not left_dates or not right_dates:
|
||||
continue
|
||||
for index, (one, two) in enumerate(zip(left_dates, right_dates)):
|
||||
if one == two:
|
||||
continue
|
||||
gathered: list[date] = []
|
||||
for context in work:
|
||||
time = _context_time(context)
|
||||
dates = (starts_by_time.get(time or "") or {}).get(track) or []
|
||||
if index < len(dates):
|
||||
gathered.append(dates[index])
|
||||
if len(gathered) < 2 or min(gathered) == max(gathered):
|
||||
continue
|
||||
ordered = sorted(gathered)
|
||||
cut = ordered[0] + (ordered[-1] - ordered[0]) / 2
|
||||
left_n = sum(1 for item in gathered if item <= cut)
|
||||
right_n = len(gathered) - left_n
|
||||
if left_n < 1 or right_n < 1:
|
||||
continue
|
||||
# BUG-749: only the D9 / D10 Narayana tracks carry a domain.
|
||||
# Vimshottari and natal Narayana boundaries say nothing about
|
||||
# which area of life moved, so the window is asked open
|
||||
# ("any") instead of being round-robined onto one domain.
|
||||
track_domain = _GUIDED_TRACK_DOMAIN.get(track)
|
||||
domain = (
|
||||
track_domain
|
||||
if track_domain and track_domain not in declined
|
||||
else GUIDED_ANY_DOMAIN
|
||||
)
|
||||
for year, month_lo, month_hi in _guided_year_windows(gathered):
|
||||
if year < lo or year > hi:
|
||||
continue
|
||||
key = (year, month_lo, month_hi, domain)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
rows.append({
|
||||
"year": year,
|
||||
"month_lo": month_lo,
|
||||
"month_hi": month_hi,
|
||||
"domain": domain,
|
||||
"split": {"left": left_n, "right": right_n},
|
||||
})
|
||||
rows.sort(key=lambda row: (
|
||||
abs(row["split"]["left"] - row["split"]["right"]),
|
||||
abs(now.year - int(row["year"])),
|
||||
-int(row["year"]),
|
||||
int(row["month_lo"]),
|
||||
str(row["domain"]),
|
||||
))
|
||||
return rows[:GUIDED_COLLECT_LIMIT]
|
||||
|
||||
|
||||
def merge_transition_proximity(
|
||||
matrix: dict[str, dict[str, dict[str, Any]]],
|
||||
events: Sequence[dict[str, Any]],
|
||||
static_contexts: Sequence[dict[str, Any]],
|
||||
birth_date: str,
|
||||
*,
|
||||
public_technique_layers: Callable[[str, Sequence[str]], list[str]],
|
||||
) -> None:
|
||||
by_time = {
|
||||
time: context
|
||||
for context in static_contexts
|
||||
if isinstance(context, dict) and (time := _context_time(context))
|
||||
}
|
||||
vim_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
pd_cache: dict[tuple[Any, ...], list[date]] = {}
|
||||
narayana_cache: dict[tuple[Any, ...], list[date] | None] = {}
|
||||
for event in events:
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
event_id = str(event.get("id") or "")
|
||||
cells = matrix.get(event_id)
|
||||
if not event_id or not isinstance(cells, dict):
|
||||
continue
|
||||
event_date = representative_event_date(event)
|
||||
if event_date is None:
|
||||
continue
|
||||
precision = str(event.get("precision") or "")
|
||||
lo, hi = event_date.year - 1, event_date.year + 1
|
||||
for time, cell in cells.items():
|
||||
context = by_time.get(str(time)[:5])
|
||||
if not isinstance(cell, dict) or not isinstance(context, dict):
|
||||
continue
|
||||
moon = (context.get("planet_longitudes") or {}).get("Moon")
|
||||
if not isinstance(moon, (int, float)):
|
||||
continue
|
||||
# Native contexts retain the local date even when the window crosses midnight.
|
||||
# Legacy time-only contexts still use the request's date.
|
||||
candidate_at = context.get("candidate_at")
|
||||
candidate_date = candidate_at.date().isoformat() if isinstance(candidate_at, datetime) else birth_date
|
||||
vim_key = (candidate_date, round(float(moon), 6), lo, hi)
|
||||
if vim_key not in vim_cache:
|
||||
vim_cache[vim_key] = _vim_start_dates(candidate_date, float(moon), lo, hi)
|
||||
pd_cache[vim_key] = _vim_start_dates(
|
||||
candidate_date,
|
||||
float(moon),
|
||||
lo,
|
||||
hi,
|
||||
include_pratyantar=True,
|
||||
)
|
||||
planets = context.get("planet_longitudes") or {}
|
||||
asc = context.get("ascendant_index")
|
||||
narayana_key = (
|
||||
candidate_date,
|
||||
int(asc) if isinstance(asc, int) else None,
|
||||
lo,
|
||||
hi,
|
||||
round(float(moon), 6),
|
||||
)
|
||||
if narayana_key not in narayana_cache:
|
||||
narayana_cache[narayana_key] = (
|
||||
_narayana_start_dates(int(asc), planets, candidate_date, lo, hi)
|
||||
if isinstance(asc, int) and isinstance(planets, dict)
|
||||
else None
|
||||
)
|
||||
ad_starts = vim_cache[vim_key]
|
||||
ad_set = set(ad_starts)
|
||||
pd_only = [item for item in pd_cache[vim_key] if item not in ad_set]
|
||||
scored = score_transition_proximity(
|
||||
event_date=event_date,
|
||||
precision=precision,
|
||||
vim_starts=ad_starts,
|
||||
vim_pd_starts=pd_only,
|
||||
narayana_starts=narayana_cache[narayana_key] or [],
|
||||
)
|
||||
if scored["points"] <= 0 and not scored["rule_ids"]:
|
||||
continue
|
||||
cell["points"] = round(float(cell.get("points") or 0) + float(scored["points"]), 4)
|
||||
cell["rule_ids"] = sorted({
|
||||
*list(cell.get("rule_ids") or []),
|
||||
*scored["rule_ids"],
|
||||
})
|
||||
domain = str(event.get("domain") or cell.get("domain") or "")
|
||||
cell["technique_layers"] = public_technique_layers(domain, cell["rule_ids"])
|
||||
|
||||
|
||||
import types as _types
|
||||
|
||||
def _rebind(function, context_time):
|
||||
globs = dict(function.__globals__)
|
||||
globs["_context_time"] = context_time
|
||||
return _types.FunctionType(function.__code__, globs, function.__name__)
|
||||
|
||||
|
||||
guided_collect_windows = _rebind(guided_collect_windows, _probe_context_time)
|
||||
merge_transition_proximity = _rebind(merge_transition_proximity, _proximity_context_time)
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Output-identical speedups for rectification scoring (BUG-1323, 1324, 1325, 1345)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import time
|
||||
from datetime import date, datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.active_rectification_event_engine import (
|
||||
_active_narayana,
|
||||
build_candidate_static_context,
|
||||
compute_event_candidate_rows,
|
||||
)
|
||||
from scripts.rectification.api_service import (
|
||||
MAX_CANDIDATES_PER_REQUEST,
|
||||
TooManyCandidates,
|
||||
_candidate_count,
|
||||
_reject_if_too_many_candidates,
|
||||
block_scan,
|
||||
diagnostics,
|
||||
score_candidates,
|
||||
)
|
||||
from scripts.rectification.contracts import normalize_rectification_request
|
||||
from scripts.rectification.dasha_transition_proximity import merge_transition_proximity
|
||||
from scripts.rectification.event_probes import guided_collect_windows
|
||||
from scripts.rectification.scoring_service import build_event_contribution_matrix, sha256
|
||||
import scripts.active_rectification_event_engine as event_engine
|
||||
from scripts.rectification import dasha_transition_proximity as proximity
|
||||
from scripts.rectification import event_probes
|
||||
from scripts.rectification import scoring_service
|
||||
from tests._baseline_rect_scoring_20261013 import (
|
||||
guided_collect_windows as baseline_guided_collect_windows,
|
||||
merge_transition_proximity as baseline_merge_transition_proximity,
|
||||
)
|
||||
|
||||
FROZEN_TODAY = date(2026, 9, 16)
|
||||
EVENT_SPECS = (
|
||||
("education", "education_start", "2008-01-01", "2008-12-31", "year"),
|
||||
("career", "career_entry", "2012-06-01", "2012-06-30", "month"),
|
||||
("relocation", "relocation", "2014-03-01", "2014-03-31", "month"),
|
||||
("relationship", "relationship_commitment", "2016-10-01", "2016-10-31", "month"),
|
||||
("career", "promotion", "2018-01-01", "2018-12-31", "year"),
|
||||
("finance", "asset_change", "2019-05-01", "2019-05-31", "month"),
|
||||
("family", "family_event", "2020-08-01", "2020-08-31", "month"),
|
||||
("health", "self_health_event", "2022-01-01", "2022-12-31", "year"),
|
||||
)
|
||||
|
||||
|
||||
class _FrozenDate(date):
|
||||
@classmethod
|
||||
def today(cls) -> date:
|
||||
return FROZEN_TODAY
|
||||
|
||||
|
||||
def _events() -> list[dict]:
|
||||
rows = []
|
||||
for index, (domain, kind, start, end, precision) in enumerate(EVENT_SPECS, start=1):
|
||||
rows.append({
|
||||
"id": f"00000000-0000-4000-8000-{index:012d}",
|
||||
"domain": domain,
|
||||
"event_kind": kind,
|
||||
"date_start": start,
|
||||
"date_end": end,
|
||||
"precision": precision,
|
||||
"summary": "虚构",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def _request(start: str, end: str, **extra) -> dict:
|
||||
body = {
|
||||
"birth_date": "1990-01-01",
|
||||
"start_time": start,
|
||||
"end_time": end,
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"events": _events(),
|
||||
**extra,
|
||||
}
|
||||
return normalize_rectification_request(body, today=FROZEN_TODAY)
|
||||
|
||||
|
||||
def _count(monkeypatch, owner, name: str) -> list[int]:
|
||||
original = getattr(owner, name)
|
||||
counter = [0]
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
counter[0] += 1
|
||||
return original(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(owner, name, wrapped)
|
||||
return counter
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def wide_case():
|
||||
request = _request("11:00", "13:00")
|
||||
captured: dict = {}
|
||||
original = scoring_service.merge_transition_proximity
|
||||
|
||||
def grab(matrix, events, contexts, birth_date, *, public_technique_layers):
|
||||
captured["matrix"] = copy.deepcopy(matrix)
|
||||
captured["events"] = list(events)
|
||||
captured["contexts"] = contexts
|
||||
captured["birth_date"] = birth_date
|
||||
captured["layers"] = public_technique_layers
|
||||
return original(
|
||||
matrix, events, contexts, birth_date,
|
||||
public_technique_layers=public_technique_layers,
|
||||
)
|
||||
|
||||
with patch.object(event_probes, "date", _FrozenDate), patch.object(
|
||||
scoring_service, "merge_transition_proximity", grab,
|
||||
):
|
||||
built = build_event_contribution_matrix(request)
|
||||
return request, built, captured
|
||||
|
||||
|
||||
def test_guided_collect_windows_matches_baseline_and_is_linear(wide_case, monkeypatch) -> None:
|
||||
request, built, _captured = wide_case
|
||||
times = list(built["candidate_times"])
|
||||
assert len(times) == 121
|
||||
variants = [
|
||||
(request, times),
|
||||
(request, times[::2]),
|
||||
(request, times[:3]),
|
||||
({**request, "declined_domains": ["relationship"]}, times),
|
||||
]
|
||||
with patch.object(event_probes, "date", _FrozenDate):
|
||||
for variant_request, variant_times in variants:
|
||||
assert guided_collect_windows(
|
||||
variant_request, built, candidate_times=variant_times, today=FROZEN_TODAY,
|
||||
) == baseline_guided_collect_windows(
|
||||
variant_request, built, candidate_times=variant_times, today=FROZEN_TODAY,
|
||||
)
|
||||
short_built = {
|
||||
**built,
|
||||
"static_contexts": list(built["static_contexts"])[:21],
|
||||
"candidate_times": times[:21],
|
||||
}
|
||||
full_calls = _count(monkeypatch, event_probes, "_context_time")
|
||||
guided_collect_windows(request, built, candidate_times=times, today=FROZEN_TODAY)
|
||||
full = full_calls[0]
|
||||
short_calls = _count(monkeypatch, event_probes, "_context_time")
|
||||
guided_collect_windows(request, short_built, candidate_times=times[:21], today=FROZEN_TODAY)
|
||||
short = short_calls[0]
|
||||
assert full <= 16 * 121
|
||||
assert short <= 16 * 21
|
||||
assert full / short <= (121 / 21) * 1.2
|
||||
|
||||
|
||||
def test_merge_transition_proximity_matches_baseline(wide_case, monkeypatch) -> None:
|
||||
_request_wide, _built, captured = wide_case
|
||||
vim_calls = _count(monkeypatch, proximity, "_vim_start_dates")
|
||||
nara_calls = _count(monkeypatch, proximity, "_narayana_start_dates")
|
||||
fresh = copy.deepcopy(captured["matrix"])
|
||||
base = copy.deepcopy(captured["matrix"])
|
||||
merge_transition_proximity(
|
||||
fresh, captured["events"], captured["contexts"], captured["birth_date"],
|
||||
public_technique_layers=captured["layers"],
|
||||
)
|
||||
baseline_merge_transition_proximity(
|
||||
base, captured["events"], captured["contexts"], captured["birth_date"],
|
||||
public_technique_layers=captured["layers"],
|
||||
)
|
||||
assert fresh == base
|
||||
assert vim_calls[0] <= 2 * len(captured["contexts"])
|
||||
assert nara_calls[0] <= len(captured["contexts"])
|
||||
|
||||
midnight = _request("23:20", "00:40")
|
||||
midnight_captured: dict = {}
|
||||
original = scoring_service.merge_transition_proximity
|
||||
|
||||
def grab(matrix, events, contexts, birth_date, *, public_technique_layers):
|
||||
midnight_captured["matrix"] = copy.deepcopy(matrix)
|
||||
midnight_captured["events"] = list(events)
|
||||
midnight_captured["contexts"] = contexts
|
||||
midnight_captured["birth_date"] = birth_date
|
||||
midnight_captured["layers"] = public_technique_layers
|
||||
return original(matrix, events, contexts, birth_date, public_technique_layers=public_technique_layers)
|
||||
|
||||
with patch.object(event_probes, "date", _FrozenDate), patch.object(
|
||||
scoring_service, "merge_transition_proximity", grab,
|
||||
):
|
||||
build_event_contribution_matrix(midnight)
|
||||
fresh_night = copy.deepcopy(midnight_captured["matrix"])
|
||||
base_night = copy.deepcopy(midnight_captured["matrix"])
|
||||
merge_transition_proximity(
|
||||
fresh_night, midnight_captured["events"], midnight_captured["contexts"], midnight_captured["birth_date"],
|
||||
public_technique_layers=midnight_captured["layers"],
|
||||
)
|
||||
baseline_merge_transition_proximity(
|
||||
base_night, midnight_captured["events"], midnight_captured["contexts"], midnight_captured["birth_date"],
|
||||
public_technique_layers=midnight_captured["layers"],
|
||||
)
|
||||
assert fresh_night == base_night
|
||||
|
||||
|
||||
def test_static_context_caches_match_fallback_and_stay_out_of_features(wide_case, monkeypatch) -> None:
|
||||
request, built, _captured = wide_case
|
||||
contexts = list(built["static_contexts"])
|
||||
assert len(contexts) == 121
|
||||
for context in contexts:
|
||||
assert "functional_profile" not in context["feature"]
|
||||
assert "narayana_antardasha_by_md" not in context["feature"]
|
||||
assert context.get("functional_profile")
|
||||
assert context.get("narayana_antardasha_by_md")
|
||||
features = [context["feature"] for context in contexts]
|
||||
cleared = []
|
||||
for context in contexts:
|
||||
clone = dict(context)
|
||||
clone["functional_profile"] = None
|
||||
clone["narayana_antardasha_by_md"] = None
|
||||
cleared.append(clone)
|
||||
engine_request = {
|
||||
"birth_date": "1990-01-01",
|
||||
"start_time": "11:00",
|
||||
"end_time": "13:00",
|
||||
"lat": 39.9,
|
||||
"lon": 116.4,
|
||||
"tz": 8,
|
||||
"events": [{
|
||||
"id": "00000000-0000-4000-8000-000000000002",
|
||||
"domain": "career",
|
||||
"event_kind": "career_entry",
|
||||
"date": "2012-06-15",
|
||||
"precision": "day",
|
||||
"summary": "虚构",
|
||||
}],
|
||||
}
|
||||
assert compute_event_candidate_rows(engine_request, static_contexts=contexts) == compute_event_candidate_rows(
|
||||
engine_request, static_contexts=cleared,
|
||||
)
|
||||
assert sha256(features) == sha256([dict(feature) for feature in features])
|
||||
|
||||
functional_calls = _count(
|
||||
monkeypatch, event_engine.functional_benefics, "derive_functional_benefic_malefic",
|
||||
)
|
||||
narayana_calls = [0]
|
||||
original = event_engine.narayana_dasha.get_current_narayana_dasha
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
caller = inspect.stack()[1].filename.replace("\\", "/")
|
||||
names = [frame.function for frame in inspect.stack()]
|
||||
if caller.endswith("/active_rectification_event_engine.py") and "_candidate_row" in names:
|
||||
narayana_calls[0] += 1
|
||||
return original(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(event_engine.narayana_dasha, "get_current_narayana_dasha", wrapped)
|
||||
with patch.object(event_probes, "date", _FrozenDate):
|
||||
scored = score_candidates(request, include_internal=True)
|
||||
functional_during_score = functional_calls[0]
|
||||
compact = score_candidates(request)
|
||||
assert functional_during_score == 121
|
||||
# Probe path (_score_year) is out of scope and still calls get_current.
|
||||
# The scoring loop (_candidate_row) must not.
|
||||
assert narayana_calls[0] == 0
|
||||
assert "event_contribution_matrix" in scored
|
||||
assert "candidate_decision_receipt" in scored
|
||||
assert "event_contribution_matrix" not in compact
|
||||
assert "candidate_decision_receipt" not in compact
|
||||
|
||||
def without_timing(value):
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: without_timing(item)
|
||||
for key, item in value.items()
|
||||
if key not in {"column_compare_ms", "event_contribution_matrix", "candidate_decision_receipt"}
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [without_timing(item) for item in value]
|
||||
return value
|
||||
|
||||
assert without_timing(compact) == without_timing(scored)
|
||||
assert len(scored["candidate_feature_snapshot"]["features"]) == 121
|
||||
assert scored["candidate_feature_snapshot"]["feature_hash"] == compact["candidate_feature_snapshot"]["feature_hash"]
|
||||
|
||||
|
||||
def test_cached_narayana_matches_live_lookup_across_ages(wide_case) -> None:
|
||||
_request, built, _captured = wide_case
|
||||
contexts = list(built["static_contexts"])[:3]
|
||||
assert len(contexts) == 3
|
||||
for context in contexts:
|
||||
periods = context["narayana_periods"]
|
||||
table = context["narayana_antardasha_by_md"]
|
||||
birth = context["candidate_at"]
|
||||
planets = context["planet_longitudes"]
|
||||
asc = context["ascendant_index"]
|
||||
|
||||
def at_age(age: float) -> datetime:
|
||||
return birth + timedelta(seconds=age * 365.2425 * 86400)
|
||||
|
||||
ages = [index / 100 for index in range(0, 12001)]
|
||||
total = sum(period["years"] for period in periods)
|
||||
cycle_start = min(float(period.get("start_age", 0)) for period in periods)
|
||||
edges = [cycle_start, cycle_start + total]
|
||||
edges.extend(float(period["start_age"]) for period in periods)
|
||||
edges.extend(float(period["end_age"]) for period in periods)
|
||||
for edge in edges:
|
||||
for delta in (-1e-9, 0.0, 1e-9):
|
||||
if edge + delta >= 0:
|
||||
ages.append(edge + delta)
|
||||
for age in ages:
|
||||
event_at = at_age(age)
|
||||
cached = _active_narayana(asc, planets, birth, event_at, periods, table)
|
||||
live = _active_narayana(asc, planets, birth, event_at, periods, None)
|
||||
assert cached == live
|
||||
|
||||
|
||||
def test_candidate_cap_rejects_before_charts_and_allows_product_windows() -> None:
|
||||
assert _candidate_count(_request("00:00", "03:59")) == 240
|
||||
assert _candidate_count(_request("00:00", "04:00")) == 241
|
||||
assert _candidate_count(_request("00:00", "04:01")) == 242
|
||||
_reject_if_too_many_candidates(_request("00:00", "03:59"))
|
||||
_reject_if_too_many_candidates(_request("00:00", "04:00"))
|
||||
with pytest.raises(TooManyCandidates) as rejected:
|
||||
_reject_if_too_many_candidates(_request("00:00", "04:01"))
|
||||
assert rejected.value.code == "too_many_candidates"
|
||||
assert rejected.value.limit == MAX_CANDIDATES_PER_REQUEST == 241
|
||||
assert "count=242" in str(rejected.value)
|
||||
|
||||
full_day = _request("00:00", "23:59")
|
||||
assert _candidate_count(full_day) == 1440
|
||||
started = time.perf_counter()
|
||||
with pytest.raises(TooManyCandidates) as full:
|
||||
score_candidates(full_day)
|
||||
assert time.perf_counter() - started < 0.05
|
||||
assert "count=1440" in str(full.value)
|
||||
with pytest.raises(TooManyCandidates):
|
||||
diagnostics(full_day)
|
||||
|
||||
step_10 = _request("00:00", "23:59", minute_step=10)
|
||||
assert _candidate_count(step_10) == 144
|
||||
_reject_if_too_many_candidates(step_10)
|
||||
step_2 = _request("00:00", "03:00", minute_step=2)
|
||||
step_5 = _request("00:00", "06:00", minute_step=5)
|
||||
assert _candidate_count(step_2) <= 91
|
||||
assert _candidate_count(step_5) <= 121
|
||||
_reject_if_too_many_candidates(step_2)
|
||||
_reject_if_too_many_candidates(step_5)
|
||||
with patch(
|
||||
"scripts.rectification.api_service.score_candidates",
|
||||
return_value={"candidate_scores": [], "decision_receipt": {}},
|
||||
) as scored:
|
||||
block_scan(step_10)
|
||||
block_scan(step_2)
|
||||
block_scan(step_5)
|
||||
assert scored.call_count == 3
|
||||
|
||||
|
||||
def test_one_candidate_context_exposes_top_level_caches() -> None:
|
||||
request = _request("12:00", "12:00")
|
||||
context = build_candidate_static_context(request, datetime(1990, 1, 1, 12, 0))
|
||||
assert "functional_profile" not in context["feature"]
|
||||
assert "narayana_antardasha_by_md" not in context["feature"]
|
||||
assert context["functional_profile"]["functional_benefics"] or context["functional_profile"]["functional_malefics"]
|
||||
assert set(context["narayana_antardasha_by_md"]) == {period["sign_idx"] for period in context["narayana_periods"]}
|
||||
@@ -100,11 +100,16 @@ HISTORICAL_SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53"
|
||||
CURRENT_POLICY_VERSION = "rectification-candidate-policy-v4"
|
||||
# Nutation fix commit that moves these scores (BUG-1275). v4 keeps V4_SOURCE_COMMIT.
|
||||
SOURCE_COMMIT = "e5f2a75183381c4a027014c7f1e89ddcb8393bd3"
|
||||
# 原值: 4 个缓存键(ashtakavarga / shadbala / vimshottari / narayana_periods)
|
||||
# 新值: 再加 functional_profile、narayana_antardasha_by_md,共 6 个
|
||||
# 原因: BUG-1324。这两项只依赖候选分钟,放在静态上下文顶层;置 None 仍现算,输出应相同
|
||||
CACHE_LAYER_KEYS = (
|
||||
"ashtakavarga_result",
|
||||
"shadbala_result",
|
||||
"vimshottari_timeline",
|
||||
"narayana_periods",
|
||||
"functional_profile",
|
||||
"narayana_antardasha_by_md",
|
||||
)
|
||||
# TASK-rectification-engine-memoization-fix-20260915 §2 measured max
|
||||
# cross-machine drift of 1.1e-3 on decision_receipt.margin_percent
|
||||
|
||||
@@ -412,7 +412,14 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
scored = score_candidates(normalized)
|
||||
diagnostic_result = diagnostics(normalized)
|
||||
self.assertFalse(scored["can_confirm_exact_minute"])
|
||||
self.assertIn("event_contribution_matrix", scored)
|
||||
# 原值: assertIn("event_contribution_matrix", scored)
|
||||
# 新值: 默认 assertNotIn;include_internal=True 时仍在
|
||||
# 原因: BUG-1325,前台没有读取点,默认响应不再带内部矩阵
|
||||
self.assertNotIn("event_contribution_matrix", scored)
|
||||
self.assertNotIn("candidate_decision_receipt", scored)
|
||||
internal = score_candidates(normalized, include_internal=True)
|
||||
self.assertIn("event_contribution_matrix", internal)
|
||||
self.assertIn("candidate_decision_receipt", internal)
|
||||
self.assertEqual(diagnostic_result["diagnostics"]["leave_one_event_out_retention_rate"], 1)
|
||||
self.assertFalse(diagnostic_result["can_confirm_exact_minute"])
|
||||
|
||||
@@ -490,7 +497,10 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
self.assertEqual(candidate["rank"], rank)
|
||||
UUID(candidate["candidate_id"])
|
||||
|
||||
receipt = first["candidate_decision_receipt"]
|
||||
# 原值: first["candidate_decision_receipt"]
|
||||
# 新值: first["decision_receipt"](同一个对象)
|
||||
# 原因: BUG-1325,默认响应不再重复挂回执
|
||||
receipt = first["decision_receipt"]
|
||||
self.assertEqual(receipt["receipt_version"], "candidate-decision-receipt-v2")
|
||||
self.assertTrue(receipt["selection_allowed"])
|
||||
self.assertTrue(receipt["acceptance_allowed"])
|
||||
@@ -592,7 +602,8 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
|
||||
result = score_candidates(normalized)
|
||||
|
||||
receipt = result["candidate_decision_receipt"]
|
||||
# 原值: result["candidate_decision_receipt"];新值: result["decision_receipt"];原因: BUG-1325
|
||||
receipt = result["decision_receipt"]
|
||||
self.assertFalse(receipt["acceptance_allowed"])
|
||||
self.assertIn("insufficient_domain_diversity", receipt["reasons"])
|
||||
self.assertIn("tied_top_score", receipt["reasons"])
|
||||
@@ -637,7 +648,8 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
|
||||
result = score_candidates(normalized)
|
||||
|
||||
receipt = result["candidate_decision_receipt"]
|
||||
# 原值: result["candidate_decision_receipt"];新值: result["decision_receipt"];原因: BUG-1325
|
||||
receipt = result["decision_receipt"]
|
||||
self.assertTrue(receipt["acceptance_allowed"])
|
||||
self.assertTrue(receipt["selection_allowed"])
|
||||
self.assertFalse(receipt["confirmation_allowed"])
|
||||
@@ -678,7 +690,8 @@ class RectificationV5ServicesTest(unittest.TestCase):
|
||||
), patch("scripts.rectification.api_service.run_diagnostics", return_value=diagnostic_values):
|
||||
result = score_candidates(normalized)
|
||||
|
||||
receipt = result["candidate_decision_receipt"]
|
||||
# 原值: result["candidate_decision_receipt"];新值: result["decision_receipt"];原因: BUG-1325
|
||||
receipt = result["decision_receipt"]
|
||||
self.assertTrue(result["candidate_decisions"])
|
||||
self.assertFalse(receipt["acceptance_allowed"])
|
||||
self.assertFalse(receipt["gates"]["date_quality"]["passed"])
|
||||
|
||||
@@ -242,7 +242,10 @@ def _collision_spans(path: str, source: str, rules: tuple[Rule, ...]) -> list[tu
|
||||
return []
|
||||
spans = []
|
||||
for node in ast.walk(functions[0]):
|
||||
if not isinstance(node, ast.Set) or node.lineno not in {1182, 1190}:
|
||||
# 原值: node.lineno not in {1182, 1190}
|
||||
# 新值: node.lineno not in {1195, 1203}
|
||||
# 原因: BUG-1325 在本文件前面加了三栏注释,这两处时钟集合整段下移,数值本身没变
|
||||
if not isinstance(node, ast.Set) or node.lineno not in {1195, 1203}:
|
||||
continue
|
||||
try:
|
||||
values = ast.literal_eval(node)
|
||||
|
||||
@@ -43,6 +43,11 @@ IDENTITY_FORBIDDEN_PREFIXES = (
|
||||
# 原因: ERR-110,章动修正与 scoring-12 改了冻结身份,另存新记录,旧记录留在清单外(BUG-1275)
|
||||
"docs/research/sealed_holdout_rerun_ephemeris_nutation_2026_10_08",
|
||||
"docs/research/reported_offset_ephemeris_nutation_2026_10_08",
|
||||
# 原值: 排除到 ephemeris_nutation_2026_10_08
|
||||
# 新值: 再排除 scoring_perf_2026_10_13 两份冻结路径
|
||||
# 原因: BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
"docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13",
|
||||
"docs/research/reported_offset_scoring_perf_2026_10_13",
|
||||
"docs/research/history/",
|
||||
"artifacts/",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user