Files
Jyotisha/docs/tasks/TASK-api-disk-cache-retention-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

226 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · API 磁盘缓存只进不出:清理、留存期与缓存键规范化(issue #待定)
- 日期:2026-10-13
- 来源:「2026-10-13 全仓屎山与性能排查」P1 · 磁盘缓存与留存(chart 缓存不清理、VedAstro 快照与 artifact 留存出生资料、缓存键碎片)
- 执行方:**Grok(Windows + Docker)**,分支 `codex/api-disk-cache-retention-20261013`,只推这个分支
- 验收:Claude(Linux)
- BUG 编号:**BUG-1330~1332**(开工时核对 `docs/BUG_HISTORY.md` 最大号;写作时最大号 BUG-1317,BUG-1323~1329 已被同批其它任务书占用)(另:`TASK-upstream-sync7-20261014` 占 BUG-1318~1322,本批号段整体从 BUG-1323 起)
- 开工前置:**无**(第一批,立即派发)。注意:`TASK-api-chart-subprocess-gate-20261013` 也改 `scripts/jyotish_api_server.py`,排在本单**合入 staging 之后**开工。
- **主会话补充的串行链(2026-10-14)**:引擎/API 文件链按此顺序逐单合入后开工(`upstream-sync7` 也改 `jyotish_engine.py` / `jyotish_api_server.py`,若在途,后合一方 rebase):`api-disk-cache-retention` + `report-reference-perf` → `api-chart-subprocess-gate` → `pl9-render-split` → `python-hygiene` → `dasha-unify` → `api-server-service-extract`。本单的前置以这条链为准(与上一行不一致时以本行为准)。
## 1. 基线
- 基线 commit:`origin/staging` `77057a819`
- 生产 API 容器把整个 `scratch/local` 挂在命名卷 `api_scratch`(`deploy/docker-compose.server.yml`),**部署不清空**;下面列的每个目录都会跨部署一直累积。
- 本机复现环境:系统 `python3`,装了 swisseph、PyJHora,**没有** vedastro SDK,外网关闭。模拟生产依赖(无 PyJHora)用一个只含 `sitecustomize.py` 的目录挡掉 `jhora`(与 `vedastro`)导入,放到 `PYTHONPATH` 最前(子进程会继承):
```python
# <临时目录>/nojhora/sitecustomize.py
import sys
class _Block:
def find_spec(self, name, path=None, target=None):
if name.split('.')[0] in {'jhora', 'vedastro'}:
raise ModuleNotFoundError(f"No module named '{name}'")
return None
sys.meta_path.insert(0, _Block())
```
## 2. 事故实证(`77057a819`,虚构盘 1990-01-01 12:00 新德里 28.6139/77.2090 +5.5)
### 2.1 chart 响应缓存从不清理(BUG-1330)
| 事实 | 位置 |
| --- | --- |
| chart 缓存写在 `scratch/local/api_chart_cache/<sha256>.json`,记录只有 `cache_key / created_at / payload`,**没有 `expires_at_unix`** | `scripts/jyotish_api_server.py::_api_chart_cache_dir`、`::_store_api_chart_response_cache` |
| TTL(`JYOTISH_API_CHART_CACHE_TTL_SECONDS`,默认 900 s)只在**读**时比较,过期文件不删 | `::_load_api_chart_response_cache` |
| 清理函数遍历 `_async_job_dir(_API_CHART_CACHE_SCOPE)` = `scratch/local/api_chart_response_jobs/`(chart **异步任务**目录),按 `expires_at_unix` 删;`api_chart_cache/` 一次都没扫过 | `::prune_expired_async_jobs`、`::_async_job_dir` |
| 清理只在进程启动时跑一次;长时间不重启的容器里,到期的异步任务记录(含完整盘面结果)只在被轮询时才删 | `::start_server` 调 `prune_expired_async_jobs()` |
| 缓存键含 `today`,所以同一人每天一个新文件;带 VedAstro 概览的响应约 **160 KB**,带 `skip_vedastro_main_entry_overview` 约 **55 KB**(本机实测 `json.dumps` 字节数) | `::_build_api_chart_cache_payload` |
| 缓存文件按进程默认权限写(本机 0644),里面是完整出生资料与盘面 | 同上 |
### 2.2 缓存键用请求原值,同一张盘多份(BUG-1330)
`_build_api_chart_cache_payload` 直接把 `body.get('hour')` 等原值放进键。实测(复现命令见 §9):
| 输入变体 | 生成的不同键数 |
| --- | --- |
| `hour` = `12` / `12.0` / `"12"` | **3** |
| 基准 / `tz="5.5"` / 显式 `second=0` / `lat="28.6139"` / `ayanamsa="LAHIRI"` | **4**(`ayanamsa` 已经被 `_request_ayanamsa` 小写,另三种各成一键) |
计算侧对这些变体走 `_get_int` / `_get_float` / `_parse_timezone`,结果相同(T3 需用字节比对证实),缓存却各存一份、各 miss 一次。
### 2.3 VedAstro 官方快照的 artifact 与语义缓存(BUG-1331)
| 事实 | 位置 |
| --- | --- |
| 每次成功归一(含 `status=partial`)都写一份**缩进**的 `official_full_snapshot-<req12>-<resp12>.json` 到 `scratch/local/vedastro_adapter/`,无任何清理。本机无 SDK、外网关闭时一次 `/api/chart`(不带 skip)照样写出 **48–52 KB** 的文件,`request_manifest.requests[].body` 里有 `StdTime "12:00 01/01/1990 +05:30"` 与经纬度,文件权限 0644 | `scripts/vedastro_service_adapter.py::_write_artifact`(调用点:`::_normalize_success`、`::_normalize_range_scan_success`、`::_merge_range_scan_reports`、`::_normalize_official_full_snapshot_success`) |
| 文件名按「请求哈希 + 响应哈希」,同盘同日覆盖;不同人、不同日期都是新文件 | `::_artifact_path` |
| 读这些文件的只有离线工具与诊断:`::list_official_full_snapshot_artifacts`、`scripts/three_engine_parity_runner.py`(`VEDASTRO_ARTIFACT_DIR`)、`jyotish_api_server.py` 运行状态里的 `latest_artifact`;用户请求链路不读 | grep `ARTIFACT_DIR` / `artifact_path` |
| 语义缓存 `official_full_snapshot_cache/`:`run_official_full_snapshot_for_case` 对**任何 dict 结果**(成功、partial、blocked)都调 `_store_official_full_snapshot_semantic_cache`,TTL>0 即落盘 | `::run_official_full_snapshot_for_case` |
| TTL 取 `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS`,**没设时回落到 `VEDASTRO_CACHE_TTL_SECONDS`**,都没设为 0(不缓存)。`.env.cn.example` 两个都写 `604800`(7 天)→ 按样例部署时,一次失败的快照会被原样复用 7 天 | `::_official_full_snapshot_cache_ttl_seconds`、`.env.cn.example` 13–14 行 |
| 诊断接口报出的 `official_full_snapshot_cache_ttl_seconds` 只读自己的变量,**不含回落**,与实际生效值可能不一致 | `scripts/vedastro_gateway.py::gateway_status`(`_int_env("VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS", 0)`) |
| 业务响应缓存 `vedastro_adapter/response_cache/`:默认 TTL 86400 s,同样只在读时判断、不删 | `::_load_cached_payload`、`::_store_cached_payload` |
| 生产 `.env.production` 里这几个变量的实际值本单读不到(密钥文件,不得打印) | 环境缺口,见 §10 |
### 2.4 其它只进不出的目录(BUG-1332)
| 目录 | 写入方 | 现状 |
| --- | --- | --- |
| `scratch/local/vedastro_snapshot_cache/` | `scripts/vedastro_snapshot_cache.py::store_snapshot`(BUG-727) | 键含 UTC 参考日;`lookup_snapshot` 最多回看 `MAX_STALE_DAYS=7` 天,**更早的文件永远不会再被读,但从不删**。每人每天一个 |
| `scratch/local/vedastro_snapshot_negative_cache/` | `scripts/vedastro_consultation_snapshot.py::store_negative_snapshot`(BUG-1231,10-05 遗留) | 只认当天(`lookup_negative_snapshot` 比对 `reference_date`),**昨天及以前的全是死文件**,每人每天一个,无清理 |
| `scratch/local/vedastro_gateway_jobs/` | `scripts/vedastro_gateway.py::_write_job` / `::_raw_response_archive` | 仅队列模式写;无清理;job 文件含 `request.case` 出生资料,缩进写、默认权限(原始响应存档已是 0600) |
| `scratch/local/varga_segments/` | `scripts/rectification_varga_segments_api.py` | 按校正窗口一份,无清理 |
| `scratch/local/high_rigor_jobs/`、`api_chart_response_jobs/` | 异步任务 | 有 `expires_at_unix`,但只在启动时清(见 2.1) |
| `scratch/local/consult_full_data_cache/` | `scripts/consult_full_data_cache.py` | **已有**两天清理(`prune_cache`,每次 `get_or_build` 都 glob+stat 全目录)。本单只记录,见 T6 |
| `$TMPDIR/jyotisha-reports/` | `jyotish_api_server.py` 报告 HTML/PDF 产物(`REPORT_ARTIFACT_DIR`) | 不在命名卷里,容器重建即清;容器存活期间无清理。本单只记录 |
### 2.5 咨询卡片缓存体积(只评估)
虚构盘、模拟生产依赖,`consult_full_data_cache.get_or_build` 实测:未命中构建 1.9 s;文件 **5,097,751 字节**;命中时 `json.loads` 中位 **68.5 ms**,整次 `get_or_build` 命中 **82.5 ms**(含 `prune_cache` 全目录 stat)。每人每天一份、保留两天 ≈ 每个活跃用户 10 MB。
## 3. 根因
1. **chart 缓存(BUG-1330)**:写缓存的人给了读时 TTL,写清理函数的人以为 chart 缓存和 chart 异步任务在同一个目录、同一种记录格式;两者从未对上。清理只挂在启动钩子上。不是回归,是从一开始就漏设计。键不规范化同理:键直接取 `body` 原值,而计算侧会先解析成数字。
2. **VedAstro artifact(BUG-1331)**:`_write_artifact` 是早期为「外部 oracle 证据可审计」写的落盘,当时只在离线脚本里跑;后来同一归一函数进了 `/api/chart`、报告、校正等在线路径,落盘没有跟着关掉。快照语义缓存把「拿到了一个 dict」当成「拿到了可复用的结果」,没有区分失败。BUG-734 已确立「失败 TTL 必须显著短于成功 TTL」,这里没跟上。
3. **前台快照缓存与负缓存(BUG-1332)**:BUG-727 / BUG-1231 只设计了「何时还能读」,没有设计「何时可以删」。
4. 所有这些目录在生产都是持久卷,部署不会顺带清空,所以问题只会随时间变大。
关联:BUG-161(快速排盘与完整证据排盘必须不同缓存键)、BUG-301、BUG-727、BUG-734、BUG-1231、BUG-721(禁止模块级缓存跨请求持有出生资料派生数据)、BUG-994(不得用 `volume prune` 换磁盘)。
## 4. 决策记录
- 产品负责人 2026-10-13 同意按排查结论全部立单交 Grok 执行;本单为 P1,按「P1/P2 必须 A/B」要求执行。
- **artifact 默认不写**:`official_full_snapshot-*.json` 等审计落盘改为开关,默认关;需要做外部引擎比对时由离线脚本显式打开。依据 AGENTS.md §8「真实用户出生资料不得进入……任何提交」的精神扩展到服务器磁盘:在线请求不留存非必要的出生资料副本。
- **失败快照短 TTL**:沿用 BUG-734 的口径(失败 TTL 显著短于成功 TTL)。
- **不推翻任何既有红线**:
- BUG-161:skip 与不 skip 的 chart 请求仍是不同缓存键(规范化不得合并这两类)。
- BUG-727 / BUG-1231:`vedastro_snapshot_cache.snapshot_cache_key` 的键材料**本单不改**(BUG-1231 写明「整数小时和规范后的浮点小时靠原始请求对齐」,改键会打断这条对齐);本单只给它加「删过期文件」。
- BUG-301:前台仍取官方层;本单不改任何取数路径与等待预算。
- BUG-721:不得为省 T6 的 80 ms 加模块级/进程级的已解析数据包缓存(那是跨请求持有出生资料派生数据)。
- **本单不做,需拍板**:改 `deploy/docker-compose*.yml` 的卷配置、给卷设容量上限、改生产 `.env.production` 里的 TTL 值、手工清理生产卷上的存量文件(上线后由新清理逻辑自然清掉;若产品要立刻清,另行拍板并由有权限的人执行,不用 `volume prune`)。
## 5. 硬红线
1. `scripts/jyotish_api_server.py`:行数只按 `tests/test_api_server_growth_contract.py` 的粗护栏(AGENTS.md §6:不另加比测试更严的行数红线,也不得压行凑数),类方法数与 `JyotishAPIHandler.__new__` 伪造点不得增长。新逻辑全部进新模块(建议 `scripts/api_scratch_retention.py`),主文件只做薄调用;`prune_expired_async_jobs` 的函数体可以整体搬进新模块,主文件留同名薄包装以保住 `tests/test_api_async_job_contract.py` 的调用入口。
2. 不新增 `JyotishAPIHandler` 类方法;不新增端点。
3. 不改 `deploy/**`、`.gitea/workflows/**`、数据库结构与迁移。
4. 不改任何引擎数值、盘面结果、证据取舍;chart 接口在规范化前后**响应逐字节一致**(`runtime_cache` 字段除外)。
5. 清理只删**按现有读逻辑已经不可能再被读到**的文件,或本单新定义的留存期已过的文件;不删未知格式文件中 mtime 在 24 小时内的;任何清理异常只计数、不抛、不阻塞请求。
6. 清理不得在请求线程里同步扫大目录:周期清理走后台线程或「距上次清理 ≥ N 秒才跑」的节流,单次耗时写进返回计数。
7. 不顺手升级依赖、不修任务书外的 warning;改既有断言写「原值 / 新值 / 原因」三栏。
8. 测试与 A/B 只用虚构或公开名人盘;A/B 记录里不得出现生产路径下的真实文件名、哈希以外的出生资料。
## 6. 任务分解
### T1 · 统一的 scratch 留存模块 + chart 缓存清理(BUG-1330)
- 新模块 `scripts/api_scratch_retention.py`:
- 一张登记表,每项 = 目录 + 判定「可删」的规则 + 是否含出生资料。至少登记:`api_chart_cache`(`created_at + chart TTL` 已过)、`high_rigor_jobs` 与 `api_chart_response_jobs`(`expires_at_unix` 已过,即现有逻辑)、T2/T4 的各目录。
- `prune_all(now=None) -> dict`:逐目录返回 `scanned / removed / errors / elapsed_ms`,只计数不打印文件名。
- 节流的周期清理:`start_periodic_prune(interval_s)`,后台守护线程,默认间隔 `JYOTISH_SCRATCH_PRUNE_INTERVAL_SECONDS=1800`,`0` 关闭周期清理(启动清理仍跑)。
- `jyotish_api_server.py`:`start_server` 里把 `prune_expired_async_jobs()` 换成新模块的启动清理 + 启动周期线程(保持打印一行汇总的行为);`prune_expired_async_jobs` 函数体搬走,留薄包装。新逻辑放新模块,主文件只留调用。
- chart 缓存新写入的文件改 0600、改为临时文件 + `os.replace` 原子写(与 `_write_async_job_record` 一致)。
- 不改:TTL 默认值、读缓存逻辑、`runtime_cache` 字段。
- 验收标准:
- 新测试 `tests/test_api_scratch_retention.py`:在 `tmp_path` 下造三类 chart 缓存文件(未过期 / 已过期 / 坏 JSON 且 mtime 超 24 h),`prune_all` 只删后两者;异步任务目录行为与现有 `test_startup_cleanup_removes_only_expired_job_records` 一致;周期线程在 `interval=0` 时不启动;清理抛错时返回 `errors>0` 不抛。
- `tests/test_api_async_job_contract.py`、`tests/test_api_server_security.py -k "chart_cache or chart_response_cache"`、`tests/test_api_server_growth_contract.py` 全绿。
- 分工:Grok 自跑以上;Claude 复验 + 快速门。
### T2 · VedAstro artifact 默认不写、快照缓存失败短 TTL、TTL 诊断如实(BUG-1331)
- `scripts/vedastro_service_adapter.py::_write_artifact`:新开关 `VEDASTRO_WRITE_AUDIT_ARTIFACTS`(默认 `0`)。关时不落盘,`source_metadata.artifact_path` 写 `None`(字段保留,值为空;先 grep 所有读 `artifact_path` 的地方确认对 `None` 安全:`scripts/vedastro_user_entrypoint.py`、`scripts/diagnose_external_engine_adapters.py`、`scripts/three_engine_parity_runner.py`、`jyotish_api_server.py` 的 `latest_artifact`)。开时照旧写,但改为紧凑 JSON、0600、原子写。
- `run_official_full_snapshot_for_case`:只有「成功」结果按正常 TTL 缓存;失败 / partial / blocked 用短 TTL `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_NEGATIVE_TTL_SECONDS`(默认 300,且强制 ≤ 正常 TTL)。「成功」的判据由执行方读 `_normalize_official_full_snapshot_success` 与 `_run_official_full_snapshot_case` 的返回形状确定后写进 PROGRESS(预期:`available is True` 且 `status` 为成功态)。语义缓存记录增加 `negative: true/false`,读时按对应 TTL 判断。语义缓存文件改 0600、原子写。
- `scripts/vedastro_gateway.py::gateway_status` 的 `official_full_snapshot_cache_ttl_seconds` 改为调用 adapter 的 `_official_full_snapshot_cache_ttl_seconds()`(含回落),另加 `official_full_snapshot_negative_cache_ttl_seconds`。
- 留存:把 `vedastro_adapter/official_full_snapshot_cache/`、`vedastro_adapter/response_cache/`(按各自 `created_at + TTL`)、`vedastro_adapter/*.json` artifact(开关关时:mtime 超 24 h 即删;开关开时:保留 `VEDASTRO_AUDIT_ARTIFACT_KEEP_DAYS`,默认 7)登记进 T1 的表。
- `.env.cn.example`:在两个 604800 旁加注释说明「失败结果另有短 TTL;含出生资料,7 天为上限」——**不改数值**(是否把生产值调短,需拍板)。
- 不改:快照的取数、并发、超时、`official_closure_state` 判定。
- 验收标准:
- 新测试:开关默认关时,一次 `_normalize_official_full_snapshot_success`(用现有测试里的 fixture)不产生任何文件、`artifact_path is None`;开关开时产生 0600 文件;失败结果在 TTL=604800 下 301 s 后不再命中、成功结果仍命中;`gateway_status` 在只设 `VEDASTRO_CACHE_TTL_SECONDS=604800` 时报 604800。
- 既有 `tests/test_vedastro_official_artifact_manifest.py`、`tests/test_vedastro_official_full_snapshot.py`、`tests/test_vedastro_range_scan_replay.py`、`tests/test_three_engine_parity_runner.py`、`tests/test_vedastro_gateway.py`、`tests/test_vedastro_user_entrypoint.py`、`tests/test_vedastro_runtime_ops.py` 全绿。需要 artifact 文件的既有测试改为显式 `monkeypatch.setenv("VEDASTRO_WRITE_AUDIT_ARTIFACTS", "1")`——这算改测试前置,逐条写「原值 / 新值 / 原因」。
- 分工:Grok 自跑以上;Claude 复验。
### T3 · chart 缓存键规范化(BUG-1330)
- 新模块里写 `normalize_chart_cache_birth(body)`:`year/month/day` → `int`;`hour/minute/second/lat/lon/tz` 能解析成有限数字的 → `float`(**不四舍五入**,`12`、`12.0`、`"12"` 都成 `12.0`);解析不了的保留原值(不得把不同输入合并);`today` / `transit_date` 取前 10 位。`_build_api_chart_cache_payload` 改用它;`cache_schema_version` 5 → 6(旧缓存自然失效,由 T1 清掉)。
- `skip_vedastro_main_entry_overview` 维持独立字段(BUG-161);`vedastro_runtime` 指纹不动。
- **不改** `vedastro_snapshot_cache.snapshot_cache_key`、`consult_full_data_cache.identity_from_body`、`_official_full_snapshot_cache_key`(见决策记录)。
- 验收标准:
- 新测试:上面 §2.2 的 7 种变体中,数值等价的变体同键;`tz="Asia/Kolkata"` 之类的非数字时区与 `tz=5.5` 不同键;skip 与非 skip 不同键;`test_api_chart_cache_key_tracks_vedastro_runtime_state` 不改仍绿。
- **逐字节等价证明**:差分脚本对每组数值等价变体,在 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0` 下各算一次 `_compute_chart_sync`(skip=True 与 skip=False 各一轮),去掉 `runtime_cache` 后 `json.dumps(sort_keys=True)` 必须相同;任何一组不同即说明该字段不能合并,保留原值并写进 PROGRESS。
- 分工:Grok 自跑;Claude 复验并独立重跑差分脚本。
### T4 · 前台快照缓存、负缓存及其它目录的清理(BUG-1332)
- 登记进 T1 的表:
- `vedastro_snapshot_cache/`:`reference_date` 早于「今天(UTC)− `MAX_STALE_DAYS` − 1」即删(与 `lookup_snapshot` 的回看窗口一致,直接引用该常量,不另写数字)。
- `vedastro_snapshot_negative_cache/`:`reference_date` 早于今天(UTC)即删。
- `vedastro_gateway_jobs/`:`updated_at` 超 `VEDASTRO_GATEWAY_JOB_KEEP_SECONDS`(默认 86400)且状态为 completed / failed 的 job 及其 `.official_raw_response.json` 一并删;queued / running 不删。
- `varga_segments/`:mtime 超 `JYOTISH_VARGA_SEGMENTS_KEEP_DAYS`(默认 7)删。
- 所有删除只认 64 位十六进制文件名或该目录既有的命名格式,其它文件不碰。
- 不改:这些模块的读写逻辑、键、时效窗口。
- 验收标准:新测试覆盖每个目录「该删的删、该留的留、命名不符的不碰」;`tests/test_vedastro_snapshot_cache.py`、`tests/test_vedastro_consultation_snapshot.py` 全绿且断言不改。
- 分工:Grok 自跑;Claude 复验。
### T5 · A/B 验证(P1 必做)
- 脚本 `scripts/research/api_disk_cache_ab.py`(或 `tools/`,执行方定,写进 PROGRESS),**同进程**分别跑 A(`origin/staging` 原函数,通过 `git show 77057a819:<path>` 导出到临时目录再 import)与 B(新代码):
- 输入:固定 20 张虚构盘 × 3 种数值写法(`12` / `12.0` / `"12"`,`lat` 字符串 / 数字)× skip/非 skip × 模拟 7 个 `today`(连续日期),每组重复 3 次取中位数。
- 环境:模拟生产依赖(§1 的 `sitecustomize` 挡 `jhora`,Windows 侧装了 vedastro SDK 则**另跑一组不挡 vedastro 的**,外网可用与否如实记录);`JYOTISH_SCRATCH_ROOT` 或 monkeypatch 指到临时目录,不碰工作树 `scratch/`。
- 度量:每个目录的文件数与总字节(模拟 7 天后、各自清理跑过之后);chart 缓存命中率(命中次数 / 请求数);单次 `/api/chart` 计算耗时中位数(命中 / 未命中);一次全量清理耗时(目录里预置 10,000 个文件时)。
- 正确性:B 的 chart 响应与 A 去掉 `runtime_cache` 后逐字节一致(T3 差分脚本复用)。
- 目标(达不到如实写,不改目标):chart 缓存命中率在三种写法混合输入下 B ≥ A 的 2.5 倍;模拟 7 天后 `api_chart_cache` 文件数 B ≤ 当天活跃键数;`vedastro_adapter` 下 artifact 文件数 B = 0(开关默认关);负缓存目录只剩当天文件;清理 10,000 文件 < 2 s 且不在请求线程。
- 开关默认值按 A/B 结论定:artifact 开关默认关是决策项,不随 A/B 变;周期清理间隔、负 TTL 可按结论调整并写明理由。
- 记录写 `docs/testing/api-disk-cache-retention-ab-20261013.md`(表格:改前 / 改后 / 变化;命令可复制)。
- 分工:Grok 跑并交记录;Claude 在 Linux 上按同一命令复跑一遍,数字差 >20% 时以 Claude 为准并在验收里说明。
### T6 · 咨询卡片缓存体积(只记录,本单不改代码)
- 在 PROGRESS 写清 §2.5 的数字与两条可选方向(交产品/下一轮决定):(a) 卡片缓存按领域切片存、命中时只读所需领域;(b) `prune_cache` 改为节流(每 N 分钟一次)而不是每次 `get_or_build` 全目录 stat。**不得**做进程内已解析包缓存(BUG-721)。
- 若 T1 的周期清理已覆盖 `consult_full_data_cache`,可以把 (b) 顺手收进来(把它登记进表、`get_or_build` 去掉每次 `prune_cache()`),但必须证明两天留存语义不变,且 `tests/test_consult_card_full_source.py` 不改仍绿;做不到就不做。
### T7 · 记录
- `docs/BUG_HISTORY.md` 新增 BUG-1330(chart 缓存不清理 + 键碎片)、BUG-1331(VedAstro artifact 留存出生资料 + 失败快照长缓存 + TTL 诊断失真)、BUG-1332(前台快照/负缓存/网关任务/分盘段缓存无清理);每条写「防复发:新增磁盘缓存必须同时登记留存规则到 `api_scratch_retention`」,并在测试里加一条**合同**:`scripts/` 下新出现的 `scratch/local/<子目录>` 写入点必须在登记表里(grep 源码实现,白名单列出离线脚本)。
- `docs/tasks/PROGRESS-api-disk-cache-retention-20261013.md`。
- `CHANGELOG.md`:一句话(用户不可感知,写「服务器不再长期保留出生资料副本」)。
- `docs/tasks/README.md` 索引加本单。
- 不改 `frontend/DESIGN.md`(无 UI)。
## 7. 让步顺序
时间不够时按此顺序**往后砍**:T6(只记录)→ T4 中 `varga_segments` 与 `vedastro_gateway_jobs` → T3 → T4 其余。T1、T2、T5、T7 不可砍;砍掉的写进 PROGRESS「未做」与 `BLOCKED.md`(如因环境)。
## 8. 验收命令分工
| 谁 | 命令 |
| --- | --- |
| Grok 自跑 | `.venv` 下:`python -m pytest tests/test_api_scratch_retention.py tests/test_api_async_job_contract.py tests/test_api_server_growth_contract.py tests/test_vedastro_official_artifact_manifest.py tests/test_vedastro_official_full_snapshot.py tests/test_vedastro_range_scan_replay.py tests/test_three_engine_parity_runner.py tests/test_vedastro_gateway.py tests/test_vedastro_user_entrypoint.py tests/test_vedastro_runtime_ops.py tests/test_vedastro_snapshot_cache.py tests/test_vedastro_consultation_snapshot.py tests/test_consult_card_full_source.py -q`;`python -m pytest tests/test_api_server_security.py -q -k "cache"`;T3 差分脚本;T5 A/B 脚本。前端、Python 快速门、全量 pytest **不跑**,以 Claude 为准 |
| Claude 验收 | 上述全部 + `.venv/bin/python scripts/run_quality_gate.py --profile quick` + 全量 `pytest` 与 `origin/staging` 基线逐名比对 + 在模拟生产依赖(挡 `jhora`)下复跑 T3 差分与 T5 A/B + `wc -l scripts/jyotish_api_server.py` ≤ 10,970 |
| 不需要 | 前端任何命令(不改前端);`test:db`(不动数据库) |
## 9. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/api-disk-cache-retention-20261013 .worktrees/api-disk-cache-retention-20261013 origin/staging
cd .worktrees/api-disk-cache-retention-20261013
git log -1 --format=%h # 应为 77057a819 或其后代
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号,确认 1330–1332 未被占
wc -l scripts/jyotish_api_server.py # 记下开工行数
grep -rn "ARTIFACT_DIR\|artifact_path\|_write_artifact\|prune_expired_async_jobs\|_build_api_chart_cache_payload" scripts tests --include=*.py
# 键碎片复现:
PYTHONPATH=.:scripts python - <<'EOF'
import jyotish_api_server as s
b=dict(year=1990,month=1,day=1,minute=0,lat=28.6139,lon=77.2090,tz=5.5)
print(len({s._api_chart_cache_key(s._build_api_chart_cache_payload(dict(b,hour=h))) for h in (12,12.0,'12')})) # 改前 3
EOF
```
跑完任何 Python 后 `git status --short` 必须为空(`scratch/` 已在 `.gitignore`,但 A/B 必须写临时目录)。
## 10. 环境缺口(预告)
- 生产 / staging 的 `VEDASTRO_CACHE_TTL_SECONDS`、`VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 实际值未知:部署后由产品负责人在服务器内网查 `/api/vedastro_gateway/status` 的两个 TTL 字段(T2 修正后含回落),只回报数字,不贴其它内容。
- 生产卷上已有的存量文件数量与体积未知:部署后可让运维在容器内跑 `du -sh /app/scratch/local/*`(只回报各目录体积与文件数)。新清理上线后首次启动即会清掉按规则可删的部分。
- 真实 VedAstro 外网下的快照文件体积、成功率:本机无 SDK、无外网,A/B 只能在 Grok 侧(若有 SDK 与外网)或 staging 部署后复核。