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

26 KiB
Raw Blame History

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 最前(子进程会继承):
# <临时目录>/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. 开工前置命令

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 部署后复核。