Files
ely-eye/CHECKLIST.md
T
ZacharyZhang-NY 674c88e84e Rank memory search in SQL instead of rebuilding BM25 in Python
The product path pulled every atom into Python and rebuilt a BM25Okapi
index per query: p50 20.16s over 165,973 atoms. It now runs one SQLite
statement with LIMIT/OFFSET.

- text_index.py: one normalizer for both sides of the index. CJK runs are
  split per character (unicode61 cannot segment them), camelCase and
  separators are split so "search atoms" finds SearchAtoms, latin tokens
  get a prefix match. Rows written before this existed hold raw text in
  atom_fts, so schema v3 rebuilds the index on upgrade.
- retrieval.py: filters (holder / scenario / source / modality / since /
  until / min_trust), a base64 cursor, and reciprocal rank fusion in place
  of the 0.55/0.35/0.10 blend whose graph term was always zero.
- An empty result is {status: "no_match"}, not a bare array.
- Go: search_atoms and compile_context rank through POST /api/search. The
  second Go tokenizer is deleted; two implementations could drift, which
  is what the MCP/HTTP parity requirement forbids.
- db.py passed 500 lines, so the row codec moved to row_codec.py.
- Tooling: installed the repo's own pyright. pyrightconfig.json had no
  exclude and swept the downloaded corpus (1171 errors); with excludes the
  count is 62, all pre-existing (evals 48, profiles 4, training 4,
  ingestion 3, runtime 2, cartridge_assets 1). backend/pyproject.toml
  carried a second [tool.pyright] that shadowed the root config; deleted.
- bench/scale_check.py measures the product search at corpus size. The
  100k-atom p50 number is not written until it is measured on an idle
  machine.

Verification: 112 pytest passed; go build/vet/test clean, gofmt clean;
ruff clean on every touched file; mypy clean on the 14 touched Python
files; repo pyright 62 errors, unchanged per-file ledger.
2026-10-07 16:02:40 -04:00

19 KiB
Raw Blame History

CHECKLIST.md — 执行进度真相

真相来源:Gaps.md 的 P0→P1→P2。从上到下一条一条做,禁止跳项。 每完成一条:跑「验收」列的命令,把结果贴进 证据: 行,再打勾。 主 agent 独自完成。禁止 subagent、worktree、新分支、并行。 每条完成后用 /herdr 在右侧 split panel 起 pi review(strata/qwen3.8-flash-next-abliterated-iq3_s Sol High),最多 5 轮。

状态记号:[ ] 待办 [~] 进行中 [x] 已过审 [D] DEFERRED(代码就绪、未真跑,不计入 pass)


阶段 0 — 先立量尺

  • b-1 下载真实语料并冻结
    • b-1.1 yubo2333/MMLongBench-Doc:真实长文档 PDF + 人写问题 + evidence_page_ids + unanswerable 标记
    • b-1.2 xiaowu0162/longmemeval-cleaned(清单原写 xiaowu0162/longmemeval,该 id 不存在→401):会话记忆 QA,含 knowledge-update / temporal-reasoning / multi-session 类别与 answer_session_ids(字段实名,非 evidence_session_ids)
    • b-1.3 openbmb/VisRAG-Ret-Test-{SlideVQA,InfoVQA,ChartQA,MP-DocVQA,ArxivQA,PlotQA}:query→页面图像 qrels
    • b-1.4 OS-Copilot/ScreenSpot-v2:真实 UI 截图 + 指令 + bbox(实测 bbox 布局为 [x,y,w,h]:1271/1272 在界内 vs xyxy 61/1272)
    • b-1.5 lmms-eval/Video-MME:真实视频 + QA;清单原写「只取 short 档」,实取分片归档里全部 129 个视频(short 105 / medium 76 / long 34 题),因为 mp4 只覆盖 900 个视频中的 129 个,再砍 short 会饿死 n
    • [~] b-1.6 3 个真实 git 仓库(pyparsing / regex / tokio)已下载冻结,但 b-3 的 code 集改用 code-search-net/code_search_net(2.2 万条 docstring→函数,天然 ground truth,n 大两个数量级);仓库级设置挪到 b-5
    • 验收:bench/corpus/manifest.jsonl 28 条,每条含 url、retrieved_at、sha256、annotation_source、license、files[];python -m bench.manifest_check 退出 0(实测 0);manifest sha256 = 8fc1ed861c891d778853d41107630869f9eb267a669c5d1038e389de80114347
  • b-2 构建多模态查询标注集(answerable n=2928)
    • b-2.1 统一 schema:{query_id, query{modality,text}, gold[{source_id,region}], scope{holder,scenario,as_of}, answerable, answer, origin};用 source_id 而非 gold_atom_ids,因为 atom 是新系统的产物、source 才是第三方语料的稳定身份;region 按模态受限(page/bbox/turn/path+symbol/{})
    • b-2.2 gold 映射:全部来自第三方标注(qrels / evidence_page_ids / answer_session_ids+has_answer / bbox / func_path+func_name),零模型判断
    • b-2.3 配比(answerable):文档 766≥90、图像 240≥60、截图 320≥40、视频 215≥30、代码 1237≥30、记忆 150≥90;unanswerable 2380/5308 = 44.8%≥15%
    • b-2.4 陷阱类别齐:知识更新 25 / 时间推理 25 / 多会话 25(LongMemEval 原生类别)+ 无答案 2380(4 种构造:cross-holder、held-out、native unanswerable、distractor)
    • 验收:python -m bench.validate_sets(清单原名 dataset_check/manifest_check,实作合并为一个)报 OK: 50789 sources, 2928 answerable queries, 2380 unanswerable 退出 0;它同时校验 gold 存在性、gold holder==query holder、observed_at <= as_of 时间可达性、以及每个 set 文件的 SHA256 是否仍等于冻结的 manifest(重跑两次 SHA 相同由此保证)
  • b-3 检索 benchmark:五路消融 + 延迟规模
    • b-3.1 指标实现:recall@k、MRR、nDCG@10,纯 Python,无模型(bench/metrics.py + bench/tests/test_metrics.py 11 条:Wilson 90/100→0.8257、10/10→0.7225、0/10→0.0 手算对齐;nDCG@10 1.5/1.6309297535714574;chance 公式经测试修正为 min(1.0, k/N))
    • b-3.2 消融五路已跑满 5,308 条(bench/systems.py:bm25_only / dense_only / graph_only / no_rerank / fusion_rerank,融合权重照抄产品 0.55/0.35/0.10,graph_only 如实返回常量序因为产品从不填 atom.relations)。recall@10:bm25 0.519、dense 0.424、graph 0.001、fusion 0.565、fusion+rerank 0.453 —— 加 reranker 反而掉 0.112,graph 等于 chance,融合比任何单信号高 0.046
    • b-3.3 延迟规模曲线(bench/run.py scale_curve,p50/p95);档位按语料实际取 500 / 2_000 / 10_000 / 50_000 个 source(清单原写 1k/10k/100k atom,语料冻结为 50,789 source,100k 档不存在)。首轮只有 50,000 一档出数:探针按 gold 是否在子集里筛,小前缀几乎不含 gold 就整档跳过 —— 已改为延迟不需要 gold(scale_probes + 测试),四档齐全
    • b-3.4 BASELINE 报告:在改动前的代码上跑,冻结提交
    • 验收:python -m bench.run --config bench/configs/ablation.json 出表(--config 已接,配置优先于默认、命令行仍可覆盖);每格带 n 与 Wilson 95% 下界;bench/reports/baseline_*.json 入库
  • b-4 外部基线
    • b-4.1 仓库内 Mem0/Zep 风格同构对照:扁平 store + holder 作用域 + 无失效(bench/counterpart.py:split_items 400 字切句、(holder, sha256(text)) 去重=Mem0、supersede 按 (holder, subject) 时间序关闭旧条目=Zep;visible(holder, as_of) 只对 temporal store 生效)+ bench/tests/test_counterpart.py 11 条
    • b-4.2 同语料同硬件跑,出对照数字:bench/reports/baseline_external.json + BASELINE_EXTERNAL.md(165,973 atom → 403,859 条 memory item,Zep 式 supersede 关闭 27,072 条;5,308 query 全量)
      • mem0_flat [holder scope] recall@10 0.866(Wilson 下界 0.855,MRR 0.629,p50 0.084 s)
      • mem0_flat [whole corpus] recall@10 0.412(p50 0.090 s)
      • zep_temporal [holder scope] 0.866;zep_temporal [whole corpus] 0.429
      • 读法:同一个 ranker、同一批 item,只把候选集从全库换成「这个 holder 的记忆」,recall@10 从 0.412 涨到 0.866(+0.454)——收益来自 holder 边界,不是更好的排序;Zep 式失效只在跨 holder 时有小幅正效应(conversation +0.091),在 holder 作用域内反而让 conversation 从 0.993 掉到 0.978、并把 5.8% 的不可答题变成拒答
      • 交叉校验:counterpart 的 whole-corpus 纯稠密 0.412 ≈ 产品 dense_only 0.424,两套 harness 互相印证
    • b-4.3 附录:真 Mem0 跑法与参考值,标明其 add() 依赖 LLM 的不可比因素(bench/reports/EXTERNAL_APPENDIX.md:Mem0 论文 LOCOMO 判分表 full-context 72.90 / Mem0^g 68.44 / Mem0 66.88 / Zep 65.99 / best RAG 60.97,附 token 预算与 p50/p95 延迟;四条不可比理由,其中关键一条是论文只报 search/answer 延迟、不报 add() 成本,Zep 的图是异步构建的)
    • 验收:bench/reports/baseline_external_*.json;对照基线与 ely-eye 同标注集同指标
  • b-5 反自证审计(bench/audit.py + bench/tests/test_audit.py 9 条)
    • b-5.1 单次尝试:禁 best-of-N 重摇 —— AST 规则:while 循环的测试或 break 守卫里出现分数词(pass_rate/target/recall/accuracy/score/hit_rate)即违规;函数名含 best_of/reroll/until_pass 即违规。已抓到产品现行违规 backend/src/ely_eye/evals.py:2351(Visual HashHop 达标即停)
    • b-5.2 禁达标即停:跑满 n 才算分 —— 报告的 meta.queries/answerable 必须等于冻结集 5308/2928,否则必须记录 limit_per_modality
    • b-5.3 每个分数旁必有 chance 基线行 —— 报告缺 chance.recall_at_10 或缺任一格的 hit_at_10_wilson_lower 即报错
    • b-5.4 producer ≠ verifier —— bench/metrics.py 不得 import ely_eye/transformers/sentence_transformers/torch/bench.systems/bench.counterpart;每条 gold 的 origin.dataset 必须在 bench/corpus/manifest.jsonl 里且 revision 等于冻结 revision(实测 6 个 set 全通过)
    • 验收:python -m bench.audit --self-test 退出 0(注入的 best-of-N 与缺 chance 两例都被拒);python -m bench.audit --product 现存 3 条违规退出 1(1 条产品重摇 + 2 份报告未落盘)
    • 遗留:b-1.6 挪过来的「仓库级代码检索设置」(pyparsing/regex/tokio 三仓库)尚未做成查询集,记在 P2

阶段 1 — P0:让它成为记忆

  • p0-1 记忆模型加四维度
    • p0-1.1 holder_id + origin(self / observed / artifact / third_party)——字段与 DB CHECK 已上;「写入方不可伪造」是服务端盖章,归 p0-2.3
    • p0-1.2 双时间轴:event_time / event_time_end / event_time_precision / event_time_source + valid_from / valid_to + supersedes(旧 time 列重命名为 ingested_at,只有一个名字)
    • p0-1.3 scenario + visibility(scope=holder / visible_to / everyone + holders + purpose)+ retention_until(提升为真列,因为 current 视图要过滤它)
    • p0-1.4 PRAGMA user_version 迁移框架 + 旧库可重放(migrations.py:v1 库原地 ALTER + RENAME COLUMN 升到 v2,行不丢;版本高于本构建则 SystemExit)
    • p0-1.5 视图 v_atoms_current + 时间旅行查询——偏差记录:SQLite 视图不能带参数,所以 as-of 是 db.atoms_as_of(moment, holder=None),谓词与视图一致但去掉 retention(retention 是保存策略,不是当时是否为真)
    • 验收:pytest backend/tests/test_memory_schema.py 绿(8 条,含 v1→v2 升级不丢行、时间旅行返回历史版本);实际路径是 backend/tests/,不是 tests/
  • p0-2 写入路径
    • p0-2.1 remember / forget / supersede 三写,Python 单一写者(memory.py:MemoryService;同一 holder 同一句子是更新,两个 holder 各留一条)
    • p0-2.2 MCP 工具面 + HTTP POST /api/memory{,/supersede,/forget} + CLI 三面同源——偏差记录:Go 侧不碰 SQLite 写,三个工具经 HTTP 转给 Python 写者(ELY_EYE_API,默认 http://127.0.0.1:8765),写规则只有一份;顺带把 Go 读路径跟上 v2(atoms.time→ingested_at,读改走 v_atoms_current,fetch/search 现在带 holder/origin/scenario/event_time)
    • p0-2.3 服务端盖 provenance:ingested_at、holder_id、origin 由服务端写,客户端不可传(RememberRequest 里根本没有这三个字段;HTTP 测试里 POST holder_id=attacker&origin=third_party 仍落成 wife/self)
    • p0-2.4 forget 级联:删 atom 与 FTS 行、删证据边、清 cache_events(留一条 invalidated 轨迹)、删 dense 向量索引——偏差记录:卡带成员是报告而非重写,已引用 DNA 的导出卡带不该被偷偷改掉
    • 验收:pytest backend/tests/test_memory_write.py backend/tests/test_memory_api.py backend/tests/test_memory_cli.py 绿(21 条);端到端跨进程 bash scripts/mcp_write_smoke.sh → PASS: remember, supersede, search and forget work through MCP(Go MCP + Python API 两个进程读写同一 SQLite,supersede 后旧记忆不再可检,forget 后 fetch 报 not found);偏差:checklist 写的 tests/test_write_path.py / tests/test_forget_cascade.py 路径不存在,实际在 backend/tests/
    • 顺带修的根因:import fitz 把 deprecation 警告打到 stdout,污染所有 CLI 命令的 JSON 输出(改 import pymupdf as fitz);cli status 原本 echo 一个 Python dict repr,改 json_dump
  • p0-3 检索改 SQL-only
    • p0-3.1 retrieval.py 不再 list_atoms():一条 SQL 里 LIMIT ? OFFSET ?,候选窗口 CANDIDATE_WINDOW = 10(db.search_atoms() 返回 FtsPage{rows, has_more})
    • p0-3.2 分词修复:text_index.py 一处归一化,索引侧与查询侧共用(NFKC + camelCase 切分 + CJK 逐字 + 前缀 "token"* + CJK 串按短语匹配)——偏差记录:schema 升到 v3,_rebuild_fts() 重建旧 atom_fts;旧行存的是原文,「钥匙」在旧索引里永远匹配不上
    • p0-3.3 空命中是 SearchResult{status:"no_match"},HTTP 与 CLI 同形,不再是裸空数组
    • p0-3.4 七个过滤器(holder/scenario/source/modality/since/until/min_trust)+ base64 游标;since > until 报 "search since must not be after until",游标坏了报 "search cursor is unreadable"
    • p0-3.5 RRF(RRF_K = 60)取代 0.55/0.35/0.10;顺手删掉无人调用的 tokenize() / lexical_score() / _graph_score()(graph 项恒为 0,因为证据边从不参与排序)
    • p0-3.6 Go 的 search_atoms / compile_context 改走 POST /api/search,删掉 Go 侧第二套分词器(store.SearchAtoms / ftsQuery / tokenPattern)——两面不可能再漂开,因为只剩一个排序实现
    • p0-3.7 100k atom 下 p50 <100ms:python -m bench.scale_check → bench/reports/SCALE.md。基线是 p50 20.16s / 165,973 atoms(bench/reports/BASELINE.md)。未测不写数:机器负载 3.7–17.9 期间测出来的数不算数
    • 验收:pytest backend/tests/test_text_index.py backend/tests/test_search_api.py 绿(18 条,真 FTS5 表 + TestClient):「钥匙」命中「我们的钥匙放在玄关抽屉里」、search atoms 命中 func SearchAtoms(ctx context.Context)、forg 命中 forgotten、全角 Search Atoms 命中、游标翻页无重复、supersede 后旧记忆不再应答;MCP 一致性由 ely-eye-mcp/internal/mcpserver/search_tools_test.go(3 条)+ bash scripts/mcp_write_smoke.sh PASS 证明;偏差:checklist 写的 tests/test_retrieval_parity.py 不存在,实际在 backend/tests/ 与 Go 测试里
  • p0-4 卡带变成可挂载作用域
    • p0-4.1 mount / unmount / switch / append 四动词
    • p0-4.2 检索按已挂载卡带过滤(作用域,不再是全局)
    • p0-4.3 kv_fingerprint:model / tokenizer / chat template / adapter hash / rope / 量化 / 引擎版本
    • p0-4.4 fingerprint 不匹配则拒绝挂载,错误信息点名差异字段
    • 验收:pytest tests/test_cartridge_scope.py tests/test_kv_fingerprint.py 绿
  • p0-5 holder registry + who_knows
    • p0-5.1 holders 表 + registry CRUD
    • p0-5.2 who_knows 工具:返回 holder 与置信度,非空结果
    • p0-5.3 转介:visited-set + 深度上限 + 终止态 NO_HOLDER_KNOWS
    • p0-5.4 默认 answer-only 转介,不外泄原文
    • 验收:pytest tests/test_transactive.py 绿;钥匙测试场景通过
  • p0-6 证明套件去自证
    • p0-6.1 删 arena 答案入墨(evals.py:1408-1409 连接线、evals.py:1417-1426 白圈)
    • p0-6.2 删 best-of-N 重摇与达标即停
    • p0-6.3 删自造矛盾场景(出题者≠判分者)
    • p0-6.4 删缓存/证据图自触发式检查
    • p0-6.5 模型相关检查标 DEFERRED,不进 pass
    • 验收:python -m ely_eye.proofs --report 逐条 PASS/DEFERRED/REMOVED + 理由

阶段 2 — P1:让它成为产品

  • p1-1 图像进 dense
    • p1-1.1 选 embedding ≤500MB,双语 + 图像;记录模型 SHA 与体积
    • p1-1.2 图像 atom 入索引;query 侧支持图像
    • p1-1.3 视觉 token 计数改 32×32 口径(1280×720 ≈ 900)
    • p1-1.4 VLM 描述:代码路径就绪,标 DEFERRED
    • 验收:无文字图像被图像查询召回有 recall 数字;pytest tests/test_visual_tokens.py 绿
  • p1-2 音频摄入
    • p1-2.1 Modality 加 audio/transcript;未知扩展名不再静默跳过
    • p1-2.2 ASR 接入,转写 atom 带词级时间戳
    • 验收:pytest tests/test_audio_ingest.py 绿;ASR 跑通或明确 DEFERRED
  • p1-3 adapter 加载
    • p1-3.1 按卡带/场景解析 6 个 adapter,不再只有 runtime.py:317
    • p1-3.2 加载结果可观测:日志 + GET /api/runtime/adapters
    • 验收:状态端点列每个 adapter 的 loaded/deferred + 原因;桩权重测试证明加载分支执行
  • p1-4 工程地基
    • p1-4.1 pytest 从零到有(后端)
    • p1-4.2 go test 从零到有(MCP)
    • p1-4.3 CI 在 Linux runner 上跑真测试
    • p1-4.4 Dockerfile 不再 COPY 缺失的 PRD.md
    • p1-4.5 去掉 RTX 4090 / Ubuntu 硬编码,改可配置
    • p1-4.6 补 .sh 脚本,与 .ps1 对等
    • p1-4.7 LICENSE
    • p1-4.8 backup / restore / doctor
    • 验收:CI 绿;干净 clone 上 docker build 成功;桩 nvidia-smi 证明设备名不再致命
  • p1-5 产品面
    • p1-5.1 GET /api/search
    • p1-5.2 前端:搜索、浏览、forget、挂载卡带、时间显示、图像渲染
    • p1-5.3 types.ts 补回 time / source_id / trust / relations
    • p1-5.4 12 个未用端点:接上或删除
    • 验收:E2E 测试覆盖五项;grep 无未接端点残留
  • p1-6 代码卫生
    • p1-6.1 evals.py 2413 行拆到 ≤500 行/文件
    • p1-6.2 删 dead code 与未调用函数
    • p1-6.3 删 v1/v2/v3 废弃端口
    • p1-6.4 注释压到 ≤1 行
    • 验收:python -m bench.filesize_check 报无文件 >500 行;vulture/grep 证据

阶段 3 — P2:规模化、安全、分层与共享

  • p2-1 规模
    • p2-1.1 sqlite-vec vec0 增量索引
    • p2-1.2 MRL 截断 + int8/binary 粗排 + reranker 精排(reranker ≤2GB)
    • p2-1.3 100k 实测;1M 外推表 + 假设
    • 验收:100k dense 腿 p50 <100ms 实测;新 atom 立即可查有测试
  • p2-2 安全与隐私
    • p2-2.1 FastAPI 鉴权 + Origin 校验
    • p2-2.2 写入信任分级与隔离:未佐证不得被引用
    • p2-2.3 注入消毒:外来文本里的指令进隔离区
    • p2-2.4 擦除级联(GDPR 风格 forget)
    • 验收:无 token 写请求被拒;投毒用例在隔离区;级联测试绿
  • p2-3 短期/长期分层
    • p2-3.1 工作记忆层
    • p2-3.2 巩固台账 + watermark,可重放不重复计数
    • p2-3.3 importance / recall_count / last_recalled_at / decay / archived_at
    • p2-3.4 冲突记录持久化
    • 验收:LongMemEval 风格的知识更新与时间推理两类有分数;被 supersede 的旧事实不当作现行
  • p2-4 共享记忆
    • p2-4.1 多用户/多 agent 作用域 + ACL
    • p2-4.2 默认可见性:answer-only 转介
    • p2-4.3 能力卡:让外部 agent 能路由进 ely-eye
    • 验收:两 holder 互不可见;越权被拒;一次真实跨 holder 会话证明能力卡可用
  • p2-5 文档单一真相
    • p2-5.1 AGENTS.md 重写并停止 gitignore
    • p2-5.2 README.md 每条宣称指向可重跑命令
    • p2-5.3 TRAINING_ARTIFACTS.md 未跑分数删除
    • p2-5.4 Gaps.md 更新为已闭合状态
    • 验收:grep 无未跑模型分数残留;每条数字可重跑

终验

  • final-accept Gaps.md §6「钥匙测试」7 条场景逐条跑通
    • 验收:核对表列场景、命令、实测数字、通过与否;未通过不标绿;benchmark 报告与基线、外部基线、chance 行并排