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.
19 KiB
19 KiB
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.jsonl28 条,每条含url、retrieved_at、sha256、annotation_source、license、files[];python -m bench.manifest_check退出 0(实测 0);manifest sha256 =8fc1ed861c891d778853d41107630869f9eb267a669c5d1038e389de80114347
- b-1.1
- 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-2.1 统一 schema:
- b-3 检索 benchmark:五路消融 + 延迟规模
- b-3.1 指标实现:recall@k、MRR、nDCG@10,纯 Python,无模型(
bench/metrics.py+bench/tests/test_metrics.py11 条:Wilson 90/100→0.8257、10/10→0.7225、0/10→0.0 手算对齐;nDCG@101.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-3.1 指标实现:recall@k、MRR、nDCG@10,纯 Python,无模型(
- b-4 外部基线
- b-4.1 仓库内 Mem0/Zep 风格同构对照:扁平 store + holder 作用域 + 无失效(
bench/counterpart.py:split_items400 字切句、(holder, sha256(text))去重=Mem0、supersede按 (holder, subject) 时间序关闭旧条目=Zep;visible(holder, as_of)只对 temporal store 生效)+bench/tests/test_counterpart.py11 条 - 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_only0.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-4.1 仓库内 Mem0/Zep 风格同构对照:扁平 store + holder 作用域 + 无失效(
- b-5 反自证审计(
bench/audit.py+bench/tests/test_audit.py9 条)- 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不得 importely_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
- b-5.1 单次尝试:禁 best-of-N 重摇 —— AST 规则:
阶段 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-1.1
- 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 测试里 POSTholder_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-2.1
- 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.shPASS 证明;偏差:checklist 写的tests/test_retrieval_parity.py不存在,实际在backend/tests/与 Go 测试里
- p0-3.1
- 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-4.1
- 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-5.1
- 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 + 理由
- p0-6.1 删 arena 答案入墨(
阶段 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-2.1
- p1-3 adapter 加载
- p1-3.1 按卡带/场景解析 6 个 adapter,不再只有
runtime.py:317 - p1-3.2 加载结果可观测:日志 +
GET /api/runtime/adapters - 验收:状态端点列每个 adapter 的 loaded/deferred + 原因;桩权重测试证明加载分支执行
- p1-3.1 按卡带/场景解析 6 个 adapter,不再只有
- 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-5.1
- p1-6 代码卫生
- p1-6.1
evals.py2413 行拆到 ≤500 行/文件 - p1-6.2 删 dead code 与未调用函数
- p1-6.3 删 v1/v2/v3 废弃端口
- p1-6.4 注释压到 ≤1 行
- 验收:
python -m bench.filesize_check报无文件 >500 行;vulture/grep 证据
- p1-6.1
阶段 3 — P2:规模化、安全、分层与共享
- p2-1 规模
- p2-1.1
sqlite-vecvec0增量索引 - p2-1.2 MRL 截断 + int8/binary 粗排 + reranker 精排(reranker ≤2GB)
- p2-1.3 100k 实测;1M 外推表 + 假设
- 验收:100k dense 腿 p50 <100ms 实测;新 atom 立即可查有测试
- p2-1.1
- 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无未跑模型分数残留;每条数字可重跑
- p2-5.1
终验
- final-accept Gaps.md §6「钥匙测试」7 条场景逐条跑通
- 验收:核对表列场景、命令、实测数字、通过与否;未通过不标绿;benchmark 报告与基线、外部基线、chance 行并排