Files
Jyotisha/docs/tasks/TASK-rect-scoring-perf-20261013.md
T
Jesse_ChenandClaude Opus 5.5 2994588ad4 docs(tasks): fold product decisions into audit briefs (BUG-1344..1346)
- dasha-unify: fix Ashtottari birth balance (BUG-1344)
- rect-scoring-perf: server-side candidate cap 241 (BUG-1345)
- node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346)
- consult-prep-parallel: classifier/reserve concurrency declined
- answer-effort-ab: model key supplied by product at acceptance
- drop stricter-than-test line-count red lines (AGENTS.md section 6)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-14 00:41:02 +08:00

32 KiB
Raw Blame History

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 同一张盘):

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. 开工前置命令

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