# TASK · 校正打分提速:去掉重复计算、精简响应(issue #待定) - 日期:2026-10-13 - 来源:「2026-10-13 全仓屎山与性能排查」· 校正打分 `POST /api/rectification/v5/score` 慢与响应过大(P0,确定性计算提速) - 执行方:**Grok(Windows + Docker)**,分支 `codex/rect-scoring-perf-20261013`,只推这个分支 - 验收:Claude(Linux) - BUG 编号:**BUG-1323~BUG-1325、BUG-1345**(开工时核对 `docs/BUG_HISTORY.md` 最大号,写作时最大号 BUG-1317)(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起) - 开工前置:无(第一批立即派发) - 性质:**输出逐字节不变**的性能改动。算法身份保持 `rectification-v5-matrix-scoring-12`、策略 `rectification-candidate-policy-v4`,不升版本、不写新 golden。 ## 1. 基线 - 基线 commit:`origin/staging` `77057a819` - 调用链:前端 `frontend/src/lib/rectification-agentic/v9/engine-client.ts::runV9CandidateScore` → `POST /api/rectification/v5/score`(超时 60 s,`busyRetry`)→ `scripts/jyotish_api_server.py::_compute_rectification_v5_score` → `scripts/rectification/api_service.py::score_candidates` → `scoring_service.py::build_event_contribution_matrix`(含 `dasha_transition_proximity.py::merge_transition_proximity`)→ `decision_policy.py::build_decision_receipt` → `refinement_packet.py::build_refinement_packet` → `event_probes.py::guided_collect_windows`。 - 时段扫描 `block_scan` 也走 `score_candidates`,同样受益。 - 生产窗口有多大(已核实,修正排查报告的假设): - `engineRequestBody` 不传 `minute_step`;`contracts.py::normalize_rectification_request` 只在 ≠1 时保留该键,引擎按 `request.get("minute_step") or 1` 取 **1 分钟**。 - `search-window.ts`:`MINUTE_GRID_MAX_SPAN_MINUTES = 120`,跨度 ≤120 分钟才进分钟阶段(打分接口),所以**生产单次打分最多 121 个候选**;更宽的窗口走 `block_scan`,步长 2/5/10(全天 10 分钟 = 144 候选)。 - 排查报告里「4 小时 240 候选 12.3 s」「12 小时 721 候选 117 s」在产品流程里到不了,只在直接调用接口时出现(服务端没有候选数上限,本单不做,见 §10)。 ## 2. 事故实证(`77057a819`,本机 8 核、有其他会话并行负载,数字有 ±20% 抖动) 复现夹具(虚构事件 + 公开 1990-01-01 北京烟测盘,与 `tests/test_rectification_engine_memoization.py::public_score_request` 同一张盘): ```python EVENTS = [ # id 依次 00000000-0000-4000-8000-00000000000{1..8},summary 统一 "虚构" ("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"), ] # 请求:birth_date 1990-01-01, lat 39.9, lon 116.4, tz 8;normalize_rectification_request(body, today=date(2026,9,16)) # event_probes.date 打补丁冻结在 2026-09-16(同记忆化测试 _FrozenDate) ``` ### 2.1 总耗时与响应大小(`score_candidates` 无 profiler,墙钟) | 窗口 | 候选 | 耗时 | 响应 JSON | | --- | --- | --- | --- | | 12:00–12:20 | 21 | 0.45–0.50 s | 393 KB | | 11:45–12:15(常见 ±15) | 31 | 0.78 s | — | | 11:00–13:00(分钟阶段上限) | 121 | **3.8–4.8 s** | **1.91 MB** | | 10:00–14:00(产品到不了) | 241 | 10.5 s | 3.45 MB | | `block_scan` 00:00–23:59 步长 10 | 144 | 2.35–2.6 s | 310 KB | 生产 2 vCPU、重计算闸门 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2`:一次 121 候选打分占一个闸门槽 4 s 以上,两人同时答题第三个就 429。 ### 2.2 121 候选的分段(计时包装,含少量包装开销) | 段 | 耗时 | 调用次数 | | --- | --- | --- | | `event_probes.py::guided_collect_windows` | 2.3 s(cProfile 下占总时间 68%) | 1 次;内部 `_context_time` **2,782,032 次**、`_guided_year_windows` 22,982 次 | | `merge_transition_proximity` | 0.8–1.0 s | 1 次;每候选×每个事件年份区间各算一遍大运起点 | | `_active_narayana` → `get_current_narayana_dasha` | ≈0.5 s | 6,171 / 6,519 次;`_subdivide_narayana_period` 25,350 次 | | `functional_benefics.derive_functional_benefic_malefic` | ≈0.2 s | 6,519 次(只依赖上升星座,每候选只需 1 次) | | 候选静态上下文 | 0.5 s | 1 次(BUG-721 已收敛,正常) | **更正排查报告**:报告里 `derive_functional_benefic_malefic` ≈1.05 s、`_active_narayana` ≈2.4 s、`merge` 最高 6 s 是 cProfile 放大后的数字(cProfile 下整次 18.8 s)。无 profiler 时这三项合计约 1.5 s,大头是 `guided_collect_windows`。 ### 2.3 原型验证(scratch 原型,不入库,只证明可行) 同进程、同一请求,剔除计时字段 `column_compare_ms` 后整份响应逐字节比较: | 原型 | 121 候选 | 一致性 | | --- | --- | --- | | `guided_collect_windows` 按(轨道, 边界序号)去重 | 1.89 s → 0.37 s(函数本身,5×);`_context_time` 2,782,032 → 1,331(每候选 11 次) | 全候选 / 半数 / 3 个 / 拒答领域 4 种入参全部一致 | | `merge_transition_proximity` 每候选算一次全量起点再按年份过滤 | 0.79–0.95 s → 0.22–0.25 s | 一致 | | 四项合并 | 整次 4.05 s → 1.9–2.7 s | 2 小时、跨午夜 23:20–00:40、拒答两领域、`refresh_probes`+`column_times`、步长 2 共 5 个场景全部一致 | 原型里功能吉凶用了 deepcopy、那罗延只做了 id 记忆,都比正式方案慢;按 §6 T3 的正式方案(进静态上下文、不复制、不算用不到的 PD),预计 121 候选可到 1.5 s 左右。 ### 2.4 响应体(121 候选,紧凑 JSON 1.91 MB,带空格 2.04 MB) | 键 | 大小 | 读取点 | | --- | --- | --- | | `event_contribution_matrix` | 804 KB | `frontend/src` **零读取**;`frontend/tests/rectification-v9-status-security.test.ts` 还断言它不得出现在前台投影里。Python:`tests/test_rectification_v5_services.py`(`assertIn`)、`scripts/research/midnight_date_anchor_regression.py::projection` 读 `result["event_contribution_matrix"]` | | `candidate_decision_receipt` | 219 KB | 与 `decision_receipt` **是同一个 dict 序列化两遍**。`frontend/src` 零读取;`jyotish_api_server.py::_attach_vedastro_minute_sensitive_to_score` 已对缺键容错;`tests/test_rectification_v5_services.py` 两处读取;`diagnostics()` 也返回 | | `candidate_feature_snapshot` | 417 KB | **有读取点**:`v9/segment-contract.ts::candidateSegmentMetadata` 读 `features[*]`。本单不动 | 去掉前两项:1.91 MB → 0.91 MB,gzip 89 KB → 65 KB。Python `json.dumps` 只省约 8 ms,所以这一项是卫生(BUG-728 规则),不是提速主力。`mcp_server.py` 不调用 v5 打分。 ## 3. 根因 - **BUG-1323**:`guided_collect_windows`(2026-09-16 `cfb41daf3` 引入,BUG-747/749 期间)对每一对代表候选 × 4 条轨道 × 每个边界序号,都把**全部**候选重新扫一遍取 `_context_time` 再收集日期。收集结果只取决于(轨道, 边界序号),后续同一(轨道, 序号)产出的键都已在 `seen` 里,重复计算没有任何输出。复杂度 O(N²·B)。记忆化测试只用 3 个候选、引导窗口测试只用 5 个,没测到平方增长。**从未设计,不是回归。** - **BUG-1324**:BUG-721 的防复发条款(「候选分钟不变量必须进 static context,不得留在事件循环里」)没有覆盖到三处: 1. `_score_event` 每个(候选 × 事件 × 采样日)都调一次 `derive_functional_benefic_malefic`,它只依赖上升星座; 2. `_active_narayana` 每次调用都通过 `get_current_narayana_dasha` 重新切分当前 MD 的 AD,还多算一层用不到的 PD; 3. `merge_transition_proximity`(09-01 `8743dcb10` 引入,09-20 改为候选日期)的缓存键带事件年份区间 `(lo, hi)`,同一候选对每个事件重新遍历整条大运时间轴。 同属 BUG-721 一族:旧条款还在,只是新代码没照做。 - **BUG-1325**:`score_candidates` 无条件返回内部矩阵,并把同一个回执挂两个键名,违反 BUG-728「没有读取点的字段不得进入前台响应」。 ## 4. 决策记录 - 2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行。本单为 P0 确定性提速:**不需要模型 A/B**,需要改前改后耗时对比与差分脚本。 - **不推翻任何红线**: - BUG-721:记忆化只用请求内显式传递的 context 或局部字典,**禁止模块级 `lru_cache` / 全局 dict**(即使键只是上升星座也不行)。本单照此执行。 - BUG-733 / BUG-747:不得对浮点整体 `==` 比较 golden,不得调用 `write_golden()`。记忆化 golden v5 文件一个字节都不改。 - ERR-110 / BUG-1047:`event_probes.py`、`dasha_transition_proximity.py`、`scoring_service.py`、`api_service.py`、`refinement_packet.py`、`functional_benefics.py` 在 `scripts/research/sealed_holdout_rerun.py::PRODUCTION_FILES` 里;`active_rectification_event_engine.py`、`narayana_dasha.py` 在 dataset `frozen_scoring.files`(12 个遗留文件)里。**输出不变也必须按研究协议重新冻结**(§6 T6),否则不得合入。BUG-1047 当时就是因为没做这一步,同类优化半途放弃。 - **产品 2026-10-14 决定:打分接口设候选数上限**,数值由 Claude 评估定为 241(T4b 表格)。 - T4 默认不返回两个字段,是执行 BUG-728 的既有规则,不是新决定。Python 内部调用方要拿矩阵就显式传参。HTTP 接口不加开关(前台没有读取点)。 ## 5. 硬红线 1. **输出逐字节不变**:同一请求下,除计时字段 `column_compare_ms` 外,`score_candidates` / `block_scan` / `diagnostics` 的返回值规范化 JSON(`sort_keys=True`)与基线完全相等;T4 删掉的两个键只在显式开关下出现,且出现时与基线逐字节相等。分数、排序、窗口、探针、回执、`candidate_feature_snapshot`(含 `feature_hash`)都不准变。 2. 不改算法、权重、阈值、采样规则、`GUIDED_COLLECT_LIMIT`、`MIN_BOUNDARY_DAYS`;不升 `ALGORITHM_VERSION` / `POLICY_VERSION`;不写新 golden。 3. 新的缓存只放进请求内的静态上下文**顶层**(与 `narayana_periods` 同级),**不得**放进 `feature` 负载(会改 `fingerprints` 与 `feature_hash`)。禁止模块级缓存(BUG-721)。 4. 不修改 `narayana_dasha.py`、`functional_benefics.py`、`dasha_analyzer.py` 的任何函数(共享模块,报告和咨询也在用)。 5. `scripts/jyotish_api_server.py` 不改(`_attach_vedastro_minute_sensitive_to_score` 已对缺键容错);`tests/test_api_server_growth_contract.py` 必须绿。 6. 不碰前端业务代码(唯一例外:T4b 在 `engine-client.ts` 错误映射里认出 `too_many_candidates`)与 `frontend/src/app/(app)/page.tsx`;不改 `.gitea/workflows/**`;不动表结构和迁移;不升级依赖;不顺手修别的 warning。 7. 改既有断言按 AGENTS §7.3 写「原值 / 新值 / 原因」三栏;开工先 grep:`grep -rn "event_contribution_matrix\|candidate_decision_receipt" tests scripts frontend/tests frontend/src`。 8. 冻结重跑只能**另存**新记录,不得覆盖 `ephemeris_nutation_2026_10_08` 的四份文件,也不得改历史归档(`test_historical_artifacts_are_byte_preserved_not_refreshed`)。 9. 跨进程比较一律固定 `PYTHONHASHSEED=0`(ERR-111:Shadbala 指纹随哈希种子变);同进程比较不受影响。 ## 6. 任务分解 ### T1 · `guided_collect_windows` 去掉平方扫描(BUG-1323) - 改 `scripts/rectification/event_probes.py::guided_collect_windows`: - 在算完 `starts_by_time` 后,按 `work` 的顺序预取每个候选的轨道表一次,例如 `work_tracks = [starts_by_time.get(_context_time(c) or "") or {} for c in work]`,内层循环改为遍历它。 - 用局部集合 `done: set[tuple[str, int]]` 记录处理过的(轨道, 边界序号)。只有在通过 `one == two` 检查后才登记;已登记的直接 `continue`。这是等价变换:同一(轨道, 序号)的 `gathered`、`cut`、`split`、`domain` 都一样,重复时产出的键全都已在 `seen` 里。 - 保留 `_representative_pairs` 的遍历顺序、`seen` 去重、最后的排序与截断,一行都不改。 - 不改:`_guided_track_starts`、`_guided_year_windows`、`_representative_pairs`、任何常量。 - 验收: - 新测试 `tests/test_rect_scoring_perf_20261013.py`: - 把基线 `guided_collect_windows` 原样复制进 `tests/_baseline_rect_scoring_20261013.py`,文件头写来源 commit `77057a819` 和 `git show 77057a819:scripts/rectification/event_probes.py` 的 sha256。 - 用 §2 夹具真实引擎的 121 候选 `built`,覆盖全候选 / 隔一取一 / 前 3 个 / `declined_domains=["relationship"]` 四种入参,新旧输出 `==`。 - **计数断言**:`_context_time` 在一次调用内的次数 ≤ 16 × 候选数;121 候选与 21 候选的次数比 ≤ (121/21) × 1.2,证明是线性的。 - `tests/test_event_probes_guided_windows.py` 全绿,断言不改。 - Grok 自跑。 ### T2 · `merge_transition_proximity` 每候选只遍历一次大运(BUG-1324 之三) - 改 `scripts/rectification/dasha_transition_proximity.py::merge_transition_proximity`: - 每个候选的 key 取 `(candidate_date, round(moon, 6), asc)`,算一次不按年份过滤的 `_vim_start_dates`(AD 层、含 PD 层)和 `_narayana_start_dates`。年份参数传一个覆盖全部的区间,例如 `lo=1, hi=9999`;也可以在本模块里写不带年份参数的私有包装,但**不得改 `event_probes.py` 里这两个函数**。 - 每个事件再用 `[d for d in xs if lo <= d.year <= hi]` 过滤,保持原顺序。 - 等价依据:这两个函数只是在固定遍历顺序上按 `start.year` 过滤,返回 `None` 的条件与年份无关。执行方要在 PROGRESS 里引用 `event_probes.py::_vim_start_dates` / `_narayana_start_dates` 的源码行说明这一点;若发现不成立就停下来回报。 - 局部字典,函数返回即释放。 - 不改:`score_transition_proximity`、常量、`ad_set` / `pd_only` 的计算方式。 - 验收: - 新测试:基线函数原样复制进 `_baseline_rect_scoring_20261013.py`。用 §2 夹具 121 候选,再加跨午夜 23:20–00:40 的矩阵,对 deepcopy 后的同一份矩阵分别跑新旧两版,结果 `==`。 - **计数**:一次调用内,`_vim_start_dates` 调用 ≤ 2 × 去重候选数,`_narayana_start_dates` ≤ 1 × 去重候选数。 - `tests/test_dasha_transition_proximity_cross_midnight.py` 全绿。 - Grok 自跑。 ### T3 · 功能吉凶与那罗延 AD 表进静态上下文(BUG-1324 之一、之二) - `scripts/active_rectification_event_engine.py::build_candidate_static_context` 的返回值**顶层**新增两个键,不进 `feature`: - `functional_profile`:`functional_benefics.derive_functional_benefic_malefic(chart["ascendant"].get("sign"))`。 - `narayana_antardasha_by_md`:当 `narayana_periods` 非空时,等于 `{p["sign_idx"]: narayana_dasha.calc_narayana_antardasha(narayana_periods, p["sign_idx"]) for p in narayana_periods}`(键用 `sign_idx`,与 `get_current_narayana_dasha` 内部调用一致);否则 `None`。 - `_score_event` 增加可选参数 `functional: dict | None = None`,为 `None` 时照旧现算。`_candidate_row` 传 `context.get("functional_profile")`。`event_probes.py` 里调用 `_score_event` 的探针路径(约 809 行,`natal_chart=context["chart"]`)同样传 context 里的值。 - `_active_narayana` 增加可选参数 `antardasha_by_md`。为 `None` 时走原来的 `get_current_narayana_dasha`;不为 `None` 时: - 用与 `narayana_dasha.get_current_narayana_dasha` **逐行相同**的 `total_cycle` / `cycle_start` / `age_in_cycle` 与 MD、AD 查找逻辑,只返回 `(md sign_idx, ad sign_idx)`,不算 PD。 - 在代码注释里注明照抄的源行号。 - `_candidate_row` 传 `context.get("narayana_antardasha_by_md")`。 - 探针路径(`event_probes.py` 约 799 行)**本单不改**:它不传 periods,改成传入会让它从「现算」变成「用缓存」,虽然等价,但不在本单范围。 - 回退路径必须保留:上下文缺这两个键(旧调用方、测试里手搓的上下文)时,结果与现在相同。 - 不改:`functional_benefics.py`、`narayana_dasha.py`(红线 4);`compute_event_candidate_rows`;事件循环结构。 - 验收: - 扩展 `tests/test_rectification_engine_memoization.py` 的同进程 A/B(BUG-733 的做法):`CACHE_LAYER_KEYS` 加上这两个新键。A 带全部缓存、B 把六个键全置 `None`,`compute_event_candidate_rows` 输出严格相等。这是给常量加两项,属于扩展不是放宽,在 PROGRESS 里写三栏。 - **计数**:一次 `score_candidates` 内,`derive_functional_benefic_malefic` 调用次数 == 静态上下文个数(121 候选即 121,基线 6,519);`narayana_dasha.get_current_narayana_dasha` 在 `active_rectification_event_engine.py` 调用栈内 == 0(沿用该测试 `_count_calls(..., from_engine=True)` 的写法)。 - 性质测试:对 3 个虚构候选上下文,年龄从 0 到 120 岁、步长 0.01,`_active_narayana(…, antardasha_by_md=表)` 与 `antardasha_by_md=None` 结果全部相等;再加周期边界前后 ±1e-9 的点。 - `candidate_feature_snapshot` 的 `feature_hash` 与基线同进程相等(证明没进 `feature`)。 - Grok 自跑。 ### T4 · 打分响应去掉无读取点字段(BUG-1325) - `scripts/rectification/api_service.py::score_candidates(request, *, include_internal: bool = False)`:默认不放 `event_contribution_matrix` 和 `candidate_decision_receipt`;`include_internal=True` 时与基线逐字节一致。用函数参数,**不要**加成请求字段(加成请求字段会进 `fingerprint` / `result_id`)。 - `diagnostics()` 同样不再返回 `candidate_decision_receipt`(前端 `runV9Diagnostics` 只读 `decision_receipt`)。 - `block_scan` 本来就不返回这两个键,不动。 - 需要矩阵的 Python 调用方显式传 `include_internal=True`。至少包括 `scripts/research/midnight_date_anchor_regression.py`;开工时用 grep 找全,每处写进 PROGRESS。 - `tests/test_rectification_v5_services.py`:约 415 行 `assertIn("event_contribution_matrix", scored)`、约 493 / 595 行读 `candidate_decision_receipt`,按三栏改:默认断言 `assertNotIn`,另加一条 `include_internal=True` 时仍在;读回执的改读 `decision_receipt`(同一个对象)。`tests/test_rectification_v5_vedastro_validation.py` 约 301 行自造结果测服务端容错,保留。 - 验收: - 新测试:121 候选默认响应不含两键;`include_internal=True` 的响应剔除计时字段后与基线逐字节相等;默认响应等于开关响应去掉这两个键。 - 响应大小(紧凑 JSON)记进 PROGRESS,目标 ≤ 基线的 50%(实测预期 1.91 MB → 0.91 MB)。 - `frontend/tests/rectification-v9-engine-contract.test.ts`、`rectification-v9-status-security.test.ts` 不受影响(Claude 复验)。 ### T4b · 服务端候选数上限(BUG-1345,产品 10-14 决定) **上限定为每次请求 241 个候选**(主会话评估,产品已授权设上限): | 依据 | 数字 | | --- | --- | | 产品流程单次打分最多 | 分钟阶段 121(跨度 ≤120 分钟、步长 1);`block_scan` 全天步长 10 = 144,其余档位(2/5 步长)≤ 91 | | 上限 241 的来历 | 等于前端已有常量 `search-window.ts::WIDEN_MAX_INCLUSIVE_MINUTES`(4 小时、每分钟一个),是产品最大用量的约 1.7 倍,留给以后放宽窗口的余量 | | 上限下的最坏耗时 | 基线 241 候选 10.5 s;T1–T3 后预计约 4 s,不超过现在一次正常 121 候选打分 | | 现在不设上限的最坏情况 | 直接调接口传全天步长 1 = 1,440 个候选,占重计算槽数分钟 | - 实现:在 `scripts/rectification/api_service.py` 的 HTTP 入口路径(`score_candidates`、`block_scan`、`diagnostics`)按 `start_time` / `end_time` / 生效步长先算候选数,超过 241 直接拒绝,返回 **400**,错误码 `too_many_candidates`,消息里带上限值和本次候选数。**在开始排盘之前拒绝**,不得算到一半。 - 上限用模块常量 `MAX_CANDIDATES_PER_REQUEST = 241`;**不得**加成请求字段(会进 `fingerprint` / `result_id`)。研究脚本若需要超过上限,显式传关键字参数关闭;开工 grep 全部 Python 调用方(`scripts/research/**`、`tests/**`),确认 T6 冻结重跑的所有试验都在上限内、结果不变,逐一写进 PROGRESS。 - 前端:`engine-client.ts` 已有的错误映射里,把 400 `too_many_candidates` 归入「请求参数错」类(不重试、不当成忙);正常流程碰不到,只需保证不会被当成 429 去重试。若需改前端文件,只改错误映射这一处。 - 验收: - 新测试:240 / 241 候选放行,242 候选 400;全天步长 1(1,440)400,且耗时 < 50 ms(证明没开始排盘);全天步长 10(144)放行;步长 2/5 的产品档位全部放行。 - `tests/test_api_server_security.py` 若有对打分接口状态码的断言,按三栏补。 - Grok 自跑;Claude 复验。 - T4b 必须在 T6 之前完成(`api_service.py` 在冻结身份清单里)。 ### T5 · 差分与耗时脚本(交付物,T1–T4 都要用) - 新增 `scripts/research/rect_scoring_perf_20261013.py`,内置 §2 夹具,两个子命令: - `bench`:对 21 / 31 / 121 候选与 `block_scan` 全天步长 10 各跑 5 次取中位数,输出 JSON(窗口、候选数、中位耗时、分段耗时、响应字节数)。分段耗时只包 §2.2 那几段,计次器不得包 `_context_time` 这类热点小函数(包了会严重拖慢、失真)。 - `dump`:把 §6 场景矩阵的规范化输出(`sort_keys=True`、`ensure_ascii=True`、剔除 `column_compare_ms`、`include_internal=True`)逐场景写成文件,并打印各自的 sha256。 - 场景矩阵(至少): 1. 12:00–12:20; 2. 11:45–12:15; 3. 11:00–13:00; 4. 跨午夜 23:20–00:40; 5. 11:30–12:30 + `declined_domains: ["relationship","career"]`; 6. 11:30–12:30 + `refresh_probes: true` + `column_times: ["11:40","12:10","12:20"]`; 7. 10:00–13:00 `minute_step: 2`; 8. `block_scan` 00:00–23:59 步长 10; 9. `block_scan` 10:00–14:00 步长 5; 10. 记忆化测试的 `public_score_request()`; 11. `diagnostics` 用场景 3 的请求。 - 跨版本比较: - 一次性工作树检出基线:`git worktree add --detach ../rsp-base 77057a819`; - 两边都用 `PYTHONHASHSEED=0` 跑 `dump`; - 11 个场景 sha256 全部相等; - 输出写进 PROGRESS 表格。 - 同进程差分由 T1–T3 的测试承担(基线函数副本 + 缓存键置 `None`)。 - 跑完删除基线工作树。脚本只读,不写仓库以外的状态。 ### T6 · 按 ERR-110 重新冻结(必须,否则不合入) 改了冻结身份文件,`tests/test_rectification_validation_integrity_gate.py`(快速门)必红。照 `docs/tasks/PROGRESS-ephemeris-nutation-20261008.md`「ERR-110」节的做法,标签用 `scoring_perf_2026_10_13`: 1. 所有代码改动(T1–T4)落定后才开始;之后若再改冻结文件,必须重做本步。 2. 冻结与重跑: - `python scripts/research/sealed_holdout_rerun.py --freeze --freeze-path docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.freeze.json`,报告写到 `docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json`(按脚本现有参数,必要时把模块常量 `FREEZE` / `REPORT` 指到新文件); - `python scripts/research/reported_offset_sweep.py --freeze --freeze-path docs/research/reported_offset_scoring_perf_2026_10_13.freeze.json`,报告写到 `docs/research/reported_offset_scoring_perf_2026_10_13.json`(900 例,上次约 15 分钟)。 3. **正确性核对**(本单最强的端到端证据): - 新旧两份报告的 `trials`(规范化 JSON)与 `ephemeris_nutation_2026_10_08` 两份报告**逐字节相等**; - `metrics` / `summary` 相等; - `historical_comparison.changed_trial_count` 分别仍是 8 和 581。 - 有任何一条不等,说明输出变了:停下回报,不得调参,不得推送。 4. 更新指针: - `scripts/research/sealed_holdout_rerun.py` 与 `reported_offset_sweep.py` 的 `FREEZE` / `REPORT` 常量; - `references/rectification_sealed_holdout.v1.json` 的当前树指针,把 nutation 放进 `previous_*`; - `frontend/tests/rectification-confirmation-gate.test.ts` 的冻结路径; - `tests/test_sealed_holdout_contract_freshness.py::IDENTITY_FORBIDDEN_PREFIXES` 加两条新前缀。 - 每一处写三栏,原因写「BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录」。 5. nutation 四份文件一个字节不改。 - 验收: - `pytest tests/test_rectification_validation_integrity_gate.py tests/test_sealed_holdout_contract_freshness.py tests/test_reported_offset_research.py tests/test_rectification_confirmation_and.py -q` 0 failed; - 前端 `rectification-confirmation-gate.test.ts` 0 fail / 0 cancelled; - 第 3 条的逐字节核对结果贴进 PROGRESS。 - Grok 跑;Claude 验收时复跑第 3 条比较。 ### T7 · 记录 - `docs/BUG_HISTORY.md` 新增 **BUG-1323**(引导窗口平方扫描)、**BUG-1324**(候选不变量仍在事件循环:功能吉凶 / 那罗延 AD / 过渡邻近;关联 BUG-721,写明旧条款为何没拦住:新代码没照做、测试只用 3~5 个候选)、**BUG-1325**(打分响应带无读取点的内部矩阵与重复回执;关联 BUG-728)。防复发写进「计数断言随候选数线性」与「新响应字段先找读取点」。另新增 **BUG-1345**(打分接口无候选数上限,直接调用可占重计算槽数分钟;修复为 241 上限、超限 400 且不排盘)。 - `docs/research/pre_work_error_ledger.md`:在 ERR-110 下追加本次另存标签。 - `docs/tasks/PROGRESS-rect-scoring-perf-20261013.md`:改前 / 改后耗时表(5 次中位数,注明机器与负载)、11 场景 sha256 对照、计数表、三栏清单、冻结核对结果。 - `CHANGELOG.md`:一句「校正每轮重算更快(2 小时窗口约快一倍以上),结果不变」。Skill 版本不 bump。 - `frontend/DESIGN.md`:不改(无 UI 变化)。 ## 7. 目标数字(同一台机器、同一次运行里 A/B 交替各 5 次取中位数) | 指标 | 基线(本机实测) | 目标 | | --- | --- | --- | | 121 候选 `score_candidates` | 3.8–4.8 s | ≤ 基线中位数的 **45%**(本机约 ≤ 2.0 s) | | 21 候选 | 0.45–0.5 s | 不慢于基线 +5% | | `guided_collect_windows`(121) | ≈1.9–2.3 s | ≤ 基线的 25% | | `merge_transition_proximity`(121) | 0.8–1.0 s | ≤ 基线的 35% | | `block_scan` 全天步长 10 | 2.35–2.6 s | 不慢于基线;预期快约 25% | | 121 候选响应(默认) | 1.91 MB | ≤ 0.96 MB | | 正确性 | — | 11 场景 sha256 全等;冻结重跑 trials 全等 | 耗时目标没达到但正确性全过:照样交付,在 PROGRESS 里写实测与原因,由 Claude 判断。**正确性有一条不过:不交付。** ## 8. 让步顺序 时间不够时,从后往前砍: 1. T3 的那罗延 AD 表(收益约 0.3–0.4 s),写进 PROGRESS「未做」; 2. T3 的功能吉凶; 3. T4。 **T1、T2、T4b、T5、T6、T7 不可砍。** T6 必须在最终代码上做:砍掉任何一项后要重做 T6。 ## 9. 验收命令分工 | 谁 | 命令 | | --- | --- | | Grok 自跑(工作树 `.venv`) | `python -m pytest tests/test_rect_scoring_perf_20261013.py tests/test_rectification_engine_memoization.py tests/test_event_probes_guided_windows.py tests/test_dasha_transition_proximity_cross_midnight.py tests/test_rectification_v5_services.py tests/test_rectification_v5_vedastro_validation.py tests/test_rectification_validation_integrity_gate.py tests/test_sealed_holdout_contract_freshness.py tests/test_reported_offset_research.py tests/test_rectification_confirmation_and.py tests/test_api_server_growth_contract.py -q`;`python -m pytest tests/test_rectification_*.py -q`(BUG-747:改回执路径必须跑全量校正测试);T5 `bench` 与跨版本 `dump` 比较;T6 冻结与逐字节核对;前端单文件 `rectification-confirmation-gate.test.ts`。前端全量、Python 快速门**不跑**,以 Claude 为准 | | Claude 验收 | 上述全部 + `.venv/bin/python scripts/run_quality_gate.py --profile quick` + Python 全量 pytest 与基线逐名比对(feedback:引擎路径改动不能只看快速门)+ 前端全量 `npm test`(看 fail 与 cancelled)+ 独立跑 T5 跨版本 11 场景 sha256 + 复核 T6 trials 逐字节 + `tests/test_repo_privacy_markers.py` | | 前端(仅当 T4b 改了 `engine-client.ts`) | Grok:`tsc --noEmit`、`npm run lint`、`rectification-v9-engine-contract.test.ts` 单文件;Claude:前端全量 `npm test` | | 不需要 | `npm run test:db`(不动数据库) | ## 10. 本单不做(另议) - 探针路径 `_active_vimshottari(birth_date, …)` 用请求日期而不是候选日期,与打分路径口径不同;本单不碰,只记录。 - `candidate_feature_snapshot` 417 KB 只读 `features[*]` 里三个字段,瘦身要动前端契约,另开单。 - `guided_collect_windows` 与 `merge_transition_proximity` 共用每候选的大运起点表(还能省约 0.3–0.5 s)。 ## 11. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rect-scoring-perf-20261013 .worktrees/rect-scoring-perf-20261013 origin/staging cd .worktrees/rect-scoring-perf-20261013 git log -1 --format=%h # 应为 77057a819 或其后代 git diff --stat 77057a819 origin/staging -- scripts/rectification scripts/active_rectification_event_engine.py scripts/functional_benefics.py scripts/narayana_dasha.py docs/research references/rectification_sealed_holdout.v1.json # ↑ 若非空:有人改过冻结文件或冻结记录,先回报,§2 数字与 T6 的对照基准要重新取 grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -1 # 核对最大号,写作时为 BUG-1317 grep -n "PRODUCTION_FILES" -A15 scripts/research/sealed_holdout_rerun.py grep -rn "event_contribution_matrix\|candidate_decision_receipt" tests scripts frontend/tests frontend/src ``` ## 12. BUG 编号起点 BUG-1323(引导窗口)、BUG-1324(不变量)、BUG-1325(响应)、BUG-1345(候选数上限,产品 10-14 决定)。开工核对最大号;若已被占用,顺延并在 PROGRESS 写明。 ## 13. 交付物清单 - 代码: - `scripts/rectification/event_probes.py` - `scripts/rectification/dasha_transition_proximity.py` - `scripts/active_rectification_event_engine.py` - `scripts/rectification/api_service.py` - 读矩阵的研究脚本(显式开关) - 脚本与测试: - `scripts/research/rect_scoring_perf_20261013.py` - `tests/test_rect_scoring_perf_20261013.py` - `tests/_baseline_rect_scoring_20261013.py` - 记忆化测试扩展 - 冻结:`docs/research/{sealed_holdout_rerun,reported_offset}_scoring_perf_2026_10_13{.freeze,}.json` 与 §6 T6 的指针更新 - 记录: - `docs/BUG_HISTORY.md`(BUG-1323~1325) - `docs/tasks/PROGRESS-rect-scoring-perf-20261013.md` - `CHANGELOG.md` - `docs/research/pre_work_error_ledger.md`(ERR-110 追加) - 不改:`frontend/DESIGN.md`、迁移、workflow、`jyotish_api_server.py`