fix(analyze): store 版本校验缺失导致升级二进制后 stale 缓存死循环 - #157
Conversation
根因:STORE_VERSION 升级(如 7→8)只作废 store 本身,而 analyze 的 up-to-date 判定只比较文件指纹与无版本头的 manifest 边车,从不校验 store 版本 —— 指纹未变 即提前返回,旧布局 store 原样保留,所有读命令(stats/trace/export)因版本门禁 拒绝读取,且其错误信息建议的 codeweb analyze 恰恰不会重建,形成死循环。 修复: - store.rs 新增 peek_version()(仅读 13 字节 magic+version 头)与 file_is_current()(缺文件/无头旧格式/版本不符均视为 stale) - project/mod.rs analyze() 判定改为 changes.is_empty() && store_is_current(); 版本不符走既有重建路径自愈(save_bincode 覆写为当前版本并重写边车) - STORE_VERSION 保持 8 不变(本修复不改变 v8 布局) 测试:analyze_rebuilds_when_store_version_stale(失败→通过)+ peek_version 三个边界用例(正常头/无头旧格式/文件缺失) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
analyze 的 up-to-date 快路径从不加载 store,报告中的 nodes/edges 恒为 unwrap_or(0) 的占位值 —— 打印 "Up to date. N files, 0 nodes, 0 edges." 会 让用户误以为图谱为空(本次 bug 报告的混淆来源之一)。 提取 format_up_to_date_line():快路径只报文件数,不含计数;全量/增量构建 输出保持不变(那里有真实计数)。 测试:up_to_date_line_reports_files_without_zero_counts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
| ])) | ||
| } | ||
|
|
||
| /// True when the on-disk bincode store is readable under the current |
There was a problem hiding this comment.
[suggestion] file_is_current is documented as "True when the on-disk bincode store is readable under the current STORE_VERSION", but it only compares the 13-byte header. A truncated or corrupt file with a current magic+version (the unit test peek_version_returns_header_version even writes a header-only file and asserts file_is_current) still takes the up-to-date fast path, and load_bincode then fails with a deserialize error rather than the "run codeweb analyze" version gate. That is the same deadlock shape for crash/partial-write, not for the 7→8 bump this PR fixes. The rest of this docstring (and store_is_current in src/project/mod.rs) also retells the incident rather than stating the contract.
Suggestion: Document the actual predicate: header magic matches and header version == STORE_VERSION; missing/unreadable/headerless ⇒ not current. Payload readability is not checked. Trim the incident writeup; the WHY that belongs here is "manifest sidecar has no version, so analyze must probe the store header."
| } | ||
| match self.config.store.format { | ||
| config::StoreFormat::Bincode => GraphStore::file_is_current(&store_path), | ||
| config::StoreFormat::Json => GraphStore::load_json(&store_path).is_ok(), |
There was a problem hiding this comment.
[suggestion] For StoreFormat::Json, every analyze() up-to-date check now load_json()s the entire graph and throws it away. Bincode keeps the cheap 13-byte probe; JSON does not. Default format is bincode so this is not the reported bug, but a large pretty-printed store will make a previously instant "Up to date" analyze allocate the full graph (and, on files-changed rebuilds, load the old store only to discard it and reparse everything anyway). is_ok() also collapses version mismatch, corrupt JSON, and I/O errors into "stale" — fine for self-heal, expensive for the happy path.
Suggestion: Probe JSON the same way as bincode: decode only version (a tiny { version: u32 } struct is enough for the gate; extra fields are ignored). Keep full load_json for actual reads. Optionally reuse a successful probe by storing it in self.store so the fast path does not pay twice.
评审意见(PR #157, review 5130971483): 1. file_is_current 文档宣称 "readable under current STORE_VERSION",实际仅 比较 13 字节头 —— 截断/损坏但头部有效的文件会被判 current,load_bincode 报裸反序列化错误而非版本门禁提示。改为文档化真实谓词(头魔数 + 头版本; payload 可读性不在检查范围),并精简 store_is_current 的事故叙述。 2. JSON 格式的 up-to-date 检查原先 load_json 全量加载整个图后丢弃。新增 json_file_is_current:以 { version: u32 } 探测结构体只解码 version 字段 (serde 忽略其余文档),快路径不再构建完整图;缺文件/损坏/版本不符仍折叠 为 "stale"(自愈语义不变)。 测试:json_file_is_current 三个用例(当前版本全文档 / 旧版本 / 损坏与缺失) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
问题
升级 codeweb 二进制(STORE_VERSION 7→8,#148 引入)后,在旧项目目录出现死循环:
错误信息建议跑
analyze重建,但analyze因文件指纹未变拒绝重建 —— 循环无出口。根因
analyze()的 up-to-date 判定只比较文件指纹 vs manifest 边车(load_manifest_only→compute_changes),从不校验 store 文件本身:FileRecord布局 v7↔v8 未变,旧边车在新二进制下反序列化成功load_bincode版本门禁拒绝次生问题:up-to-date 提前返回时
self.store为None,报告的nodes/edges来自unwrap_or(0),输出永远显示0 nodes, 0 edges(与 store 实际内容无关),误导用户以为图谱为空。修复
Commit 1 — 核心修复(
src/graph/store.rs+src/project/mod.rs):GraphStore::peek_version():仅读 13 字节 magic+version 头,不做全量反序列化;缺文件/无头旧格式(pre-store: bincode 跨版本反序列化失败时错误信息无版本诊断价值 #110)/版本不符均返回NoneGraphStore::file_is_current():头版本 ==STORE_VERSIONProject::store_is_current():bincode 走头探测(快路径语义保持);JSON 格式复用load_json自带版本门禁(is_ok()即"当前布局")analyze()判定改为changes.is_empty() && self.store_is_current()—— 版本不符走既有重建路径自愈(save_bincode覆写为当前版本并重写边车)STORE_VERSION保持 8 不变(本修复不改变 v8 布局,无需再 bump)Commit 2 — 输出修复(
src/main.rs):format_up_to_date_line():快路径只报文件数,不再打印无意义的 0 计数;全量/增量构建输出不变(那里有真实计数)测试(TDD:失败 → 通过)
analyze_rebuilds_when_store_version_stale:模拟 v7 头 store + 指纹未变 → 断言不报 up-to-date、重建后 store 通过当前版本门禁peek_version_returns_header_version/peek_version_none_for_legacy_headerless_file/peek_version_none_for_missing_file:新 API 边界up_to_date_line_reports_files_without_zero_counts:输出不含nodes/edges计数验证矩阵
真实场景端到端验证(136 个 .sql 的遗留项目目录,store 头手动降级回 v7 复现原 bug):
Out of scope
diff命令未加版本校验(只展示文件差异,不读 store)🤖 Generated with Sisyphus