- 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
32 KiB
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/staging77057a819 - 调用链:前端
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-16cfb41daf3引入,BUG-747/749 期间)对每一对代表候选 × 4 条轨道 × 每个边界序号,都把全部候选重新扫一遍取_context_time再收集日期。收集结果只取决于(轨道, 边界序号),后续同一(轨道, 序号)产出的键都已在seen里,重复计算没有任何输出。复杂度 O(N²·B)。记忆化测试只用 3 个候选、引导窗口测试只用 5 个,没测到平方增长。从未设计,不是回归。 - BUG-1324:BUG-721 的防复发条款(「候选分钟不变量必须进 static context,不得留在事件循环里」)没有覆盖到三处:
_score_event每个(候选 × 事件 × 采样日)都调一次derive_functional_benefic_malefic,它只依赖上升星座;_active_narayana每次调用都通过get_current_narayana_dasha重新切分当前 MD 的 AD,还多算一层用不到的 PD;merge_transition_proximity(09-018743dcb10引入,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在 datasetfrozen_scoring.files(12 个遗留文件)里。输出不变也必须按研究协议重新冻结(§6 T6),否则不得合入。BUG-1047 当时就是因为没做这一步,同类优化半途放弃。
- BUG-721:记忆化只用请求内显式传递的 context 或局部字典,禁止模块级
- 产品 2026-10-14 决定:打分接口设候选数上限,数值由 Claude 评估定为 241(T4b 表格)。
- T4 默认不返回两个字段,是执行 BUG-728 的既有规则,不是新决定。Python 内部调用方要拿矩阵就显式传参。HTTP 接口不加开关(前台没有读取点)。
5. 硬红线
- 输出逐字节不变:同一请求下,除计时字段
column_compare_ms外,score_candidates/block_scan/diagnostics的返回值规范化 JSON(sort_keys=True)与基线完全相等;T4 删掉的两个键只在显式开关下出现,且出现时与基线逐字节相等。分数、排序、窗口、探针、回执、candidate_feature_snapshot(含feature_hash)都不准变。 - 不改算法、权重、阈值、采样规则、
GUIDED_COLLECT_LIMIT、MIN_BOUNDARY_DAYS;不升ALGORITHM_VERSION/POLICY_VERSION;不写新 golden。 - 新的缓存只放进请求内的静态上下文顶层(与
narayana_periods同级),不得放进feature负载(会改fingerprints与feature_hash)。禁止模块级缓存(BUG-721)。 - 不修改
narayana_dasha.py、functional_benefics.py、dasha_analyzer.py的任何函数(共享模块,报告和咨询也在用)。 scripts/jyotish_api_server.py不改(_attach_vedastro_minute_sensitive_to_score已对缺键容错);tests/test_api_server_growth_contract.py必须绿。- 不碰前端业务代码(唯一例外:T4b 在
engine-client.ts错误映射里认出too_many_candidates)与frontend/src/app/(app)/page.tsx;不改.gitea/workflows/**;不动表结构和迁移;不升级依赖;不顺手修别的 warning。 - 改既有断言按 AGENTS §7.3 写「原值 / 新值 / 原因」三栏;开工先 grep:
grep -rn "event_contribution_matrix\|candidate_decision_receipt" tests scripts frontend/tests frontend/src。 - 冻结重跑只能另存新记录,不得覆盖
ephemeris_nutation_2026_10_08的四份文件,也不得改历史归档(test_historical_artifacts_are_byte_preserved_not_refreshed)。 - 跨进程比较一律固定
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,文件头写来源 commit77057a819和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的源码行说明这一点;若发现不成立就停下来回报。 - 局部字典,函数返回即释放。
- 每个候选的 key 取
- 不改:
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 复验)。
- 新测试:121 候选默认响应不含两键;
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已有的错误映射里,把 400too_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。
- 场景矩阵(至少):
- 12:00–12:20;
- 11:45–12:15;
- 11:00–13:00;
- 跨午夜 23:20–00:40;
- 11:30–12:30 +
declined_domains: ["relationship","career"]; - 11:30–12:30 +
refresh_probes: true+column_times: ["11:40","12:10","12:20"]; - 10:00–13:00
minute_step: 2; block_scan00:00–23:59 步长 10;block_scan10:00–14:00 步长 5;- 记忆化测试的
public_score_request()��› 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:
- 所有代码改动(T1–T4)落定后才开始;之后若再改冻结文件,必须重做本步。
- 冻结与重跑:
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 分钟)。
- 正确性核对(本单最强的端到端证据):
- 新旧两份报告的
trials(规范化 JSON)与ephemeris_nutation_2026_10_08两份报告逐字节相等; metrics/summary相等;historical_comparison.changed_trial_count分别仍是 8 和 581。- 有任何一条不等,说明输出变了:停下回报,不得调参,不得推送。
- 新旧两份报告的
- 更新指针:
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 另存新记录」。
- 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 -q0 failed;- 前端
rectification-confirmation-gate.test.ts0 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. 让步顺序
时间不够时,从后往前砍:
- T3 的那罗延 AD 表(收益约 0.3–0.4 s),写进 PROGRESS「未做」;
- T3 的功能吉凶;
- 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_snapshot417 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.pyscripts/rectification/dasha_transition_proximity.pyscripts/active_rectification_event_engine.pyscripts/rectification/api_service.py- 读矩阵的研究脚本(显式开关)
- 脚本与测试:
scripts/research/rect_scoring_perf_20261013.pytests/test_rect_scoring_perf_20261013.pytests/_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.mdCHANGELOG.mddocs/research/pre_work_error_ledger.md(ERR-110 追加)
- 不改:
frontend/DESIGN.md、迁移、workflow、jyotish_api_server.py