A second review found three numbers left behind when the report was regenerated with pinned decoding: - the transcriber docstring claimed 1.7 s for 88.9 s of speech; the report says 1.39 s, and 1.7 came from the unpinned run. The docstring now names no number, so it cannot go stale, and it stops calling container seconds speech with voice activity detection off. - the checklist still quoted 3.55 s and 61.6x / 73.0x / 97.7x; the frozen report says 3.26 s and 63.9x / 82.4x / 112.2x. - the checklist still said 31 speech segments for one mp4; with greedy decoding that video yields 38, which is where the store's 80 atoms come from (38 + 39 + 3). The report also credited the store with timing invariants that were measured on the model's answer. The harness now recomputes them from the words as stored (out_of_order_in_store, before_their_segment_in_store) and says so. Verification: 212 passed, ruff clean, mypy clean on both touched files with a cold cache, pyright 0 errors.
38 KiB
38 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 报告:在改动前的代码上跑,冻结提交(
bench/reports/BASELINE.md+baseline.json,冻结于 2916363,机器负载 1.0–1.7;P0 之后的同机同语料数字另存bench/reports/CURRENT.md,同一套bench/run.py生成,标 commit + store schema 版本,--name CURRENT不覆盖基线)- 偏差记录:BASELINE.md 里「产品路径 top-10 与快路径重合 0.0%」是 harness 的身份 bug,不是检索缺陷——gold 用
mmlongbench:PH_2016.06.08_Economy-Final.pdf,库里是PH_2016_06_08_Economy_Final_10e57152f44d1a2b(storage.py 内容哈希),两边从不相等。修在bench/systems.py:_corpus_ids(复用bench/ingest_baseline.py:89 source_map()),CURRENT.md 里是修好的数。冻结文件保留原文,因为它是那一天的观测。
- 偏差记录:BASELINE.md 里「产品路径 top-10 与快路径重合 0.0%」是 harness 的身份 bug,不是检索缺陷——gold 用
- 验收:
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。实测 165,973 atom(bar 的 1.66 倍)、300 探针、p50 57ms / p95 138ms,机器 load 1.76 / 32 核 / 每查询 4 线程;基线 p50 20.16s ⇒ 快 354 倍。带 holder 作用域再测一遍(n=173):recall@10 0.870、MRR 0.740、p50 62ms / p95 117ms。- 根因不是 SQL 慢,是 OR 表达式里那些「每个文档都有」的虚词:
"parsing"* OR "error"* OR "in"* OR "the"* …命中 127,145 行、305ms,而中文二元组短语查询 32 命中只要 1.0ms。bm25 必须给每条命中打分,延迟就跟着命中数走。 - 修法:用索引自己当 IDF 源,按文档频率丢掉虚词(
text_index.rare_terms,COMMON_TERM_FRACTION = 0.05,无手写停用词表,换语料自动换答案)。0.05 是扫出来的:n=173 上 0.20→0.893/112.9ms、0.05→0.899/58.6ms、0.02→0.870/27.2ms、0.01→0.844/13.4ms、0.005→0.783/6.5ms —— 召回峰值就在 0.05。 - 计数缓存
term_df(schema v6):一个词只数一次,任何 atom 写入都清空它,所以缓存不会比它所描述的库更老。冷缓存 p50 73ms / 热 63ms,1,324 个词被缓存。
- 根因不是 SQL 慢,是 OR 表达式里那些「每个文档都有」的虚词:
- 验收:
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四动词(cartridge.py:CartridgeService;schema v2→v3 新增cartridge_atoms成员表与cartridge_mounts挂载表,重开时自动重建——挂载是机器状态,不是数据事实) - p0-4.2 检索按已挂载卡带过滤(
retrieval.py:search默认作作域 = 当前挂载集,显式cartridge_ids覆盖挂载;什么都没挂时仍是全库,空库行为与以前一致)——偏差:forget同时从所有卡带成员表里掉出,否则“忘了”还能从卡带里搜到 - p0-4.3
kv_fingerprint(kv_fingerprint.py):model / tokenizer / chat template hash、adapter hash、RoPE 块(factor / original_max_position_embeddings / mrope_interleaved / mrope_section / partial_rotary_factor)、context 长度、引擎版本、指纹格式版本;读不到的项写进unverifiable_fields,不留空 - p0-4.4 fingerprint 不匹配则拒绝挂载,错误信息逐字段点名差异(
home_x was built for a different runtime; refusing to mount: rope: factor: cartridge 4.0 vs local 2.0; …) - 验收:
pytest backend/tests/test_cartridge_scope.py backend/tests/test_kv_fingerprint.py绿(26 条:16 作用域 + 10 指纹);端到端bash scripts/mcp_write_smoke.sh→PASS: remember, supersede, search, forget and cartridge mounting work through MCP(挂载后 work 的记忆搜不到、上传后新写的记忆要 append 才能检索、unmount 后全库恢复、改 RoPE factor 后mount_cartridge报错含rope);偏差:checklist 写的tests/不存在,实际在backend/tests/ - 偏差记录:四动词同时有 HTTP(
/api/mounts、/api/cartridges/{id}/{mount,switch,unmount,append})、CLI(ely-eye mount/mounts/unmount/append)和 5 个 MCP 工具;append重写atoms.parquet+sparse_index.bm25.zst并重算 DNA,文件先写、后写库,失败不会留半个卡带;量化没进指纹——量化是引擎启动参数(profiles.py的--cache-type-k q4_0等),不在模型快照里,指纹只比快照真说得出的东西,否则就是拿不存在的字段做门禁 - 跑出来的两个真缺陷:
atoms.parquet根本导不出记忆(pyarrow 拒绝无子字段的 struct,metadata={}炸掉整表 → 六个 JSON 列改存 JSON 文本,读取端load_cartridge_atoms解码);materialize()先写成员表后写 cartridges 行,外键拒绝所有卡带 api.py超 500 行,卡带/挂载路由拆到api_cartridges.py;那个模块不能用from __future__ import annotations——FastAPI 会拿模块 globals 去解Depends(settings_dep),而它只是register()的参数,结果每条路由都要settings查询参数
- p0-4.1
- p0-5 holder registry + who_knows
- p0-5.1
holders表 + registry CRUD- 验收:
pytest backend/tests/test_transactive.py -k "registry or kind or blank or holding" -q通过;schema v5 在旧库上补表。 - 事实:
migrations.pySCHEMA_VERSION 4→5,holders进_BASE_TABLES(holder_id 主键、kind CHECK、knows_about_json、contact_json、created/updated)。CRUD 在holder_store.py(db.py已 498 行,逼近 500 上限,故另立模块)。删除保护:holder 'x' still holds N memories; forget them before deleting the holder→ HTTP 409。 - 偏差:方法名
list在类内遮蔽内置list,mypy 判list[Holder]注解非法(Function ... is not valid as a type)→ 改名list_holders(),不是加 cast 糊过去。
- 验收:
- p0-5.2
who_knows工具:返回 holder 与置信度,非空结果- 验收:
pytest backend/tests/test_transactive.py -k "directory or declared" -q;bash scripts/mcp_write_smoke.sh里 who_knows("护照") 首位是 wife 且带 contact.channel=signal。 - 事实:目录不按可见性过滤("她知道"就是答案),回答才按可见性过滤。置信度是序数的:
0.60*相对得分 + 0.25*min(1, matched/3) + 0.15*声明过这个话题,相对 = top_score / 候选集最高 top_score(避免 BM25 魔法常数:同一 query 在 3 文档库上量级 1e-7)。每条命中附 matched / top_score / latest_time 与 basis 说明。 - 根因修复:FTS5 的
bm25()不能聚合(MAX(-bm25())+ GROUP BY 报unable to use function bm25 in the requested context),子查询/窗口形式也被拒 → 用 FTS5 自带的rank列(-MAX(rank),实测与MAX(-bm25())相等,9.1786e-07)。 - 根因修复:
fts_query("钥匙在哪里")生成与问句等长的短语 → 0 命中(实测 钥匙 ✓ / 钥匙在哪里 ✗)。text_index.py新增_cjk_phrase:整段短语 OR 每个相邻二元组短语,相邻性保留(p0-3 的反精确测试仍成立),等长不再是条件。拉丁路径未动 ⇒ 冻结的 BASELINE 数字不受影响。
- 验收:
- p0-5.3 转介:visited-set + 深度上限 + 终止态
NO_HOLDER_KNOWS- 验收:
pytest backend/tests/test_transactive.py -k "walks or hop or nobody" -q。 - 事实:
ask()先查 asker 可见范围(hop 0),再按 who_knows 命中依次跳转,asked列表即 visited-set,不重复问同一 holder;max_hops限 1..10,越界ValueError("ask needs max_hops between 1 and 10, got N");终止态answered/referred/no_holder_knows(无人知道是结果,不是错误:basis=["no holder has a memory that answers this"])。 - 根因修复:
if len(asked) >= max_hops: break把 asker 自己算作一跳 ⇒max_hops=1谁也没问就返回 no_holder_knows → 改len(asked) - 1 >= max_hops,docstring 写明「max_hops 数的是 asker 之外被问的人」。
- 验收:
- p0-5.4 默认 answer-only 转介,不外泄原文
- 验收:
bash scripts/mcp_write_smoke.sh→PASS: remember, supersede, search, forget, cartridge mounting and the transactive referral work through MCP(两个进程、一个 SQLite:husband 通过 MCPask得到status=referred、answered_by=wife、answer为空且整份 JSON 里搜不到妻子的原文)。 - 事实:
include_text只对可读所有者(asker 本人或无主的 library 条目)显示原文;referred只带 contact 渠道。 - 根因修复:
_answered()里readable = owner in (as_holder, LIBRARY)是 SQL 可见性子句之外的第二份、更严的规则,与visible_to=[husband]冲突(共享记忆被错误隐藏)→ 删掉这份重复判断,SQL 的as_holder可见性子句是唯一权威。 - 偏差:Go 侧
limit/max_hops零值曾当真实数字发出,被 API 以greater_than拒 → 未设置的可选字段一律省略,由服务端定默认值。
- 验收:
- 验收:
pytest tests/test_transactive.py绿;钥匙测试场景通过
- p0-5.1
- p0-6 证明套件去自证
- p0-6.1 删 arena 答案入墨(
evals.py:1408-1409连接线、evals.py:1417-1426白圈)- 偏差:入墨是唯一的可读信号——
render_visual_hash只画噪声、不画字,答案槽被青色线+白圈标出后,模型不需要检索也能答。所以删墨的同时把 id 印上瓦片(proofs/arena.py:render_visual_hash白底黑字、每行 8 字符、字号tile_size//8),否则删墨 = 删题。
- 偏差:入墨是唯一的可读信号——
- p0-6.2 删 best-of-N 重摇与达标即停
collect_model_visual_proofs(evaluator, hops, token_equivalent, attempts)固定跑HASHHOP_ATTEMPTS = 8题;旧代码while len(proofs) < max_samples里一达标就 break(evals.py:2365),等于弱模型反复重摇到及格。
- p0-6.3 删自造矛盾场景(出题者≠判分者)
- 偏差:自造场景没有删掉(删了就没得测),而是从代码搬进数据:
proofs/contradiction_scenarios.json(12 场景,sha2565f93aba09cbfc32f…),corpus_path(settings.data_dir)优先读运维者放的第三方集,ely-eye contradiction-corpus写默认集。检查项永久 DEFERRED,直到磁盘上不是自带的那份;reason 里写明「生产者用自家例题判分不算证据」。
- 偏差:自造场景没有删掉(删了就没得测),而是从代码搬进数据:
- p0-6.4 删缓存/证据图自触发式检查
- 缓存:旧检查只要
any(layer.entries > 0)就算过(evals.py:1252-1265)。现在拿 store 里真实的一条 atom 拼一个带 nonce 的问题,编译两次,读cache_events的 miss→hit 计数;空 store 直接 failed(空上下文会让两次编译「一致」得毫无意义)。顺带修掉真实双计数:read_semantic_cache与compiler.compile各记一次事件,现在读是纯读、编译恰好记一次(缓存命中带 hits 才记 hit,空缓存记 miss 并附 reason)。 - 仓库覆盖:旧检查
indexed_file_count > 0+ 有调用图 + 有 git 历史就算过,不碰 store。现在必须sources > 0 && atoms > 0,否则 failed 并说明「仓库上下文建了但没进 store」。 - 研究模块:旧检查断言八个模块各返回非空列表,而那些向量是 SHA256 派生的假向量——不可能失败。改为
status="removed",artifact 记下为什么撤下、重新加回的条件。
- 缓存:旧检查只要
- p0-6.5 模型相关检查标
DEFERRED,不进 passProofCheck.status加deferred/removed+reason+rerun;_deferred_without_model()在model_dir缺失时产出 deferred 并点名缺什么。聚合改成failed > deferred > passed:只要还有没跑成的主张,整个套件就不说「通过」。每条 failed/deferred 都带 why 与可执行的 rerun 命令(proofs/table.py渲染)。
- p0-6.6 顺手还清的文件尺寸债
evals.py2427 行 →proofs/包 15 个模块,最大 445 行(checks_cartridge.py),全部 ≤500 行;suite.py218 行只剩「跑哪些检查、怎么定论」。proofs/__main__.py删掉(它的latest_report()与PRDProofSuite.latest()是两份 glob 不同的重复实现),渲染移到proofs/table.py,latest-proof-suite成为唯一入口并在 failed 时exit 1。
- 顺带修掉两个被证明测试逼出来的真 bug:
text_index.fts_query把拉丁词用空格连起来 = FTS5 隐式 AND,长问题永远查不到短答案(实测「Which Ely-Eye PRD requirements prove long context memory, HashHop, and adapters?」→ 0 命中)。改成 OR + bm25 rank 排序。profiles.windows_path_to_wsl()对本来就是 POSIX 的路径也拼/mnt/+ 空 drive,产出/mnt//home/...,六个 runtime profile 的探针全部probe_exit_code: 1。
- 验收:
python -m pytest backend/tests/test_proofs.py -q→ 12 passed;ELY_EYE_HOME=/tmp/p06home ely-eye proof-suite --cartridge-id p06_20cf30f7804d && ely-eye latest-proof-suite→ 15 行判决(5 failed / 3 passed / 6 deferred / 1 removed),每条带 why 与 rerun。 - 验收:
python -m ely_eye.proofs --report逐条 PASS/DEFERRED/REMOVED + 理由
- p0-6.1 删 arena 答案入墨(
阶段 2 — P1:让它成为产品
- p1-1 图像进 dense
- p1-1.1 选 embedding ≤500MB,双语 + 图像;记录模型 SHA 与体积
Xenova/chinese-clip-vit-base-patch16fp16 ONNX,361MB,onnx/model_fp16.onnxsha2560fad978faa89b85fda53a83a4c202116a9eb87c8f9723fb4d85e4483efbce4b2;CPU 可跑(仓库已有 onnxruntime);文本塔 76ms、图像塔 29ms/张(batch 8)- 契约是量出来的不是猜的:输入
input_ids/attention_mask/pixel_values (N,3,224,224),输出image_embeds/text_embeds (N,512);这是融合对比图,未用的塔必须喂占位行;分词器用BertTokenizer(按 CLIP 分词器加载会得到超出 vocab 的 id,Gather 直接炸)
- p1-1.2 图像 atom 入索引;query 侧支持图像
backend/src/ely_eye/visual_index.py(独立文件,retrieval.py 停在 499 行没有膨胀);ely-eye index-visual;search --image;APISearchRequest.image- 开关规则:
clip_model_dir指向含onnx/model_fp16.onnx的目录才算可用,不再多一个 bool - 14 个测试:句子找到图、图找到图、新图一到索引即失效、缺快照点名、map/vector 不一致点名、词够多时不付 encoder、forget 之后图片不再从内存回答
- p1-1.3 视觉 token 计数改 32×32 口径(1280×720 ≈ 900)
visual_token_equivalent = ⌈w/32⌉·⌈h/32⌉;migration v7 重算存量:21,325 个图像 atom 从 5,331 → 34,618 tokens;整库 29.9M → 84.2M,于是「100M 胶囊」在这份 50,278 文件语料上刚好装得下、再大就装不下——这才是那句 claim 的真实含义
- p1-1.4 VLM 描述:代码路径就绪,标
DEFERREDdescribe.py;ingestion 只在 OCR 没找到字时才调用(图上已有字是免费的);无快照 → atom metadatadeferred: no vision-language model snapshot;模型报错 →failed: …且 ingest 不死(策略在调用方,不在 describer);status显示两个视觉开关;5 个测试
- p1-1.1 选 embedding ≤500MB,双语 + 图像;记录模型 SHA 与体积
- p1-1 证据(我额外加的六臂基准,不是验收项):
bench/visual_recall.py,ScreenSpot-v2 的 554 个图标 crop + 408 张整屏,报告bench/reports/VISUAL.md- 图标池:picture 0.208 vs words 0.175 vs chance 0.018 —— 图片空间比瞎猜高 11.5 倍,并且在 427 个 OCR 没字的 crop 上超过了词
- 整屏池:words 0.384 > picture 0.215 —— 整屏的身份主要是文字,OCR 词在这里赢
- 整屏 → 图标:0.108 —— CLIP 不会在截图里定位小图标,它是整图相似模型,不是 grounding 模型
- 单屏内:MRR 0.908(recall@10 是 1.000 但 chance 也是 1.000,所以那一行只有 MRR 有意义)
- 本轮没测双语:ScreenSpot-v2 的指令全是英文,报告里写明了
- 验收:无文字图像被图像查询召回有 recall 数字;
pytest tests/test_visual_tokens.py绿
- p1-2 音频摄入
- p1-2.1
Modality加 audio;未知扩展名不再静默跳过 - p1-2.2 ASR 接入,转写 atom 带词级时间戳
- 验收:
pytest backend/tests/test_asr.py绿(13 个测试);ASR 实跑,不是 DEFERRED - 实测证据(
bench/reports/ASR.md,.venv/bin/python -m bench.asr_check生成):3 段 Video-MME 真视频 258.18 秒音频(容器时长 88.93 / 109.18 / 60.07),对着第三方字幕 WER 0.242——分母是 533 个参考词(听到 563 个词),三行各自的参考词数是 201 / 296 / 36。CPU int8 4 线程 3.26 秒转完,逐行 63.9× / 82.4× / 112.2× 实时。走产品路径摄入后从库里读回:80 个 audio atom、 563 个词级时间戳、0 个时序倒错、0 个早于自己所属片段(总共 133 个 atom)。模型Systran/faster-whisper-tiny(78.2MB,model.bin sha256 dcb76c6586fc06cb…),远低于 500MB 上限。 - 可复现性是量出来的:解码参数钉死
beam_size=1, temperature=(0.0,), condition_on_previous_text=False, vad_filter=False, cpu_threads=4(temperature给单元素 tuple 才是真的没有回退阶梯—— faster-whisper 1.2.1 没有temperature_increment_on_fallback这个参数,第一版我传了它, 直接TypeError)。同一台机器连跑两遍:WER 0.242 → 0.242,转写文本逐字节相同。 - 字幕不是逐字稿,所以这个 WER 不是任何方向的界:字幕漏词也会掩盖模型漏词。 报告把分母写在表里,不写「上界」这种断言。
- 偏差与根因:
- 验收路径
tests/test_audio_ingest.py不存在,实际是backend/tests/test_asr.py。 Modality只加audio,没加transcript:一份转写就是带时间戳的 audio atom, 两个成员是同一件事的两个名字。- faster-whisper 自带的音频加载器在 PyAV 19 上是坏的(
av.open(..., metadata_errors=)→TypeError),而它声明av时没写版本界。所以asr.decode_audio()自己用 PyAV 解码成 float32/16kHz 单声道再喂给model.transcribe(),不降级 pin 一个没有 cp314 wheel 的旧 av。 - PyAV 19 的异常面是量出来的不是猜的:
av.Error不存在(第一版会死在 except 里), 真实基类是av.error.FFmpegError;只 catch 它,我自己的ValueError消息才不会被吞。 - 未知扩展名:显式给路径 → 抛
UnsupportedFile(“no reader for .xyz”);目录扫描 → 记进IngestResponse.skipped带原因,继续摄入能读的。以前是返回空 batch,无声无息。 - 同一个静默毛病还有一处被 review 抓出来:
_ingest_text对超过max_text_file_bytes(10 MiB) 的文件也是直接返回空 batch。本仓库自己的语料里就有 277MB 的longmemeval_s_cleaned.json。 现在抛FileTooLarge(带字节数和上限),扫描照样记进skipped。 - 视频现在除了帧 atom 还会产出 speech atom(同一个 mp4:18 帧 + 38 段语音,即 38 个 audio atom)。
- 顺手把 p2-4 提前做了:
ingestion.py748 行超了 500 规则,拆成ingestion_common.py(102) /ingestion_media.py(ABC mixin, 278) /ingestion_repository.py(321) /ingestion.py(177);cli.py544 行也超了,按已有的cli_transactive.register模式拆出cli_senses.py(79) 和cli_common.py(json_dump),cli.py现在 479 行。 - review 第一轮 FAIL 的 11 条全部落地:钉死解码参数并跑两遍证明确定性;用容器真实时长
而不是「最后一段的结束时间」;表里露出参考词分母、删掉「上界」断言;speech atom 数改成
从库里读回(80,不是 bench 自己数的 66);报告在代码提交之后重新生成,stamp 指向含代码的
commit;
cli_senses里重复的json_dump删掉改用cli_common;Go 的search_atoms描述补上audio(MCP 调用方原本发现不了这个过滤值);test_a_file_with_no_audio_track_says_so原本 测的是 markdown,现在用 PyAV 写真·无音轨 mp4 命中asr.py那条分支,另加空 wav 命中 「音轨是空的」;删掉_seconds这个空壳 wrapper。 - 顺手改了个默认值(review 也点了):
config.py的embedding_model_id从Qwen/Qwen3-VL-Embedding-8B(16GB,违反本轮 ≤500MB 的界)换成sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2——冻结的 bench 数字用的就是它。 - 门禁里挖出的真根因:
bench/tests/*用from bench.x import而仓库根没有 conftest, 所以裸pytest会ModuleNotFoundError: No module named 'bench'(只有python -m pytest能跑)。pyproject.toml加[tool.pytest.ini_options] pythonpath=["."] testpaths=[…],两种起法都绿。 - 拆完顺手修掉搬过来的旧类型债:
MediaIngest._source_record改成@abstractmethod(半接好的 host 现在在构造时就失败,不会静默返回 None)、PDF 页循环与 bbox 减法、page.get_text的 union 收窄;pyproject.toml里失效的fitz.*mypy override 换成faster_whisper.*。 - 全仓 mypy:干净树上冷缓存实测 parent
03ca35726 个错 → 本任务 24(净修 2 个, 没新增;之前写的 29 是在半成品工作树上量的,不算);改动文件 pyright 0 错、 ruff 干净、212 个测试绿(裸pytest与python -m pytest都是 212)。
- 验收路径
- 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 行并排