Both reports were produced by a harness that had just changed, so their stamps named a commit without that code in it. Re-running them on an idle machine puts the stamp and the source back in sync. Recall did not move: all seven leg configurations reproduce to the third decimal, which is the part of the number anyone should act on. Latency moved within its usual load band, and the encoder probe came in slower this time, so the picture index's build cost is now quoted as a band rather than a single remembered figure.
83 KiB
83 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 71.7ms / p95 217.7ms / max 780.5ms,机器 load [2.31, 3.72, 7.9] / 32 核 / 每查询 4 线程;基线 p50 20.16s ⇒ 快 281 倍。同一脚本在更空的机器上跑到过 p50 56.5ms / 58.7ms(load 1.5–1.8)——延迟随负载走,所以引用报告那一次,不挑最好看的。同一探针集上的质量与延迟扫描见bench/reports/TERMS.md(n=173 holder 作用域,0.05 档 recall@10 0.803、MRR 0.652、p50 69.5ms / p95 131.5ms)。- 根因不是 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 是扫出来的,扫描器已入库成证据:bench/term_sweep.py→bench/reports/TERMS.md(n=173,冻结库的副本):0.20→0.792/420.3ms、0.10→0.792/467.5ms、0.05→0.803/69.5ms、0.02→0.792/33.4ms、0.01→0.775/19.2ms、0.005→0.740/10.8ms —— 召回峰值就在 0.05。早先 scratch 脚本扫出的 0.893/0.899/0.870 已被取代:那些数字没有入库的产物,且是在检索改写(候选放宽规则、向量腿入库)之前测的。为此rare_terms(connection, terms, *, fraction=None)改成调用时解析上界——def 期默认值扫不动。 - 计数缓存
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 线程跑完这 258 秒 音频:两份已提交报告分别 3.26 秒与 3.12 秒(逐行 63.9–112.2× 与 68.4–111.2× 实时——转写秒数随 机器负载浮动,WER 与词数不随负载动)。走产品路径摄入后从库里读回: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 不是任何方向的界:字幕漏词也会掩盖模型漏词。 报告把分母写在表里,不写「上界」这种断言。
- review 第二轮 FAIL 的 4 条全是数字与措辞,也都改了:docstring 里的 1.7 秒是钉参数前那次运行的残留
(现在不写数字,只指向报告;并且不再把容器时长叫「语音时长」,因为
vad_filter=False); CHECKLIST 里的 3.55 秒 / 61.6× 与「31 段语音」同样是旧运行残留(钉死后是 38/39/3 段,加起来 正好是库里的 80 个 atom);报告那句「库里有 80 个 atom、0 个时序倒错」原本把只在模型输出上 算过的不变量说成库侧的,现在 harness 对库里存的 words 重算(out_of_order_in_store/before_their_segment_in_store)。速度只报区间,因为转写秒数跟机器负载走。 - 偏差与根因:
- 验收路径
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)。
- 验收路径
- 独立 review(
herdr里的pi,模型strata/qwen3.8-flash-next-abliterated-iq3_s:high): 第一轮 FAIL 11 条 → 第二轮 FAIL 4 条 → 第三轮 PASS。reviewer 不是读我的话,而是自己 重跑bench.asr_check(新进程)、重跑门禁、逐项核对 stamp 链与 18 条 CHECKLIST 断言, 并确认旧数字(1.7 秒 / 3.55 / 61.6 / 31 段 / 210.5 / 51×)零残留。三轮都是同一个 reviewer, 没有换人放水。
- p1-2.1
- p1-3 adapter 加载
- p1-3.1 按卡带/场景解析 6 个 adapter,不再只有
runtime.py:317 - p1-3.2 加载结果可观测:日志 +
GET /api/runtime/adapters - 验收:状态端点列每个 adapter 的 loaded/deferred + 原因;桩权重测试证明加载分支执行
backend/src/ely_eye/adapters.py是唯一的 adapter 解析处。runtime.py原先在 7 处调resolve_runtime_adapter,现在统一走_generation_adapter_fields();生成路径上的peft只在adapters.load_into_model里懒加载(训练路径training.py:443、training.py:767、proofs/training_trace.py:33仍直接 import peft,那是训练不是生成),RuntimeAdapter与resolve_runtime_adapter已删除。- 「场景」= 挂载的卡带:
AdapterManifest和CartridgeManifest都没有 scenario 字段(读出来的事实),所以解析按cartridge_store.mounted_ids选,报告写明来自哪个卡带,没有新造字段。 - 修掉的真 bug:
CartridgeService.attach_adapter(cartridge.py:230)把 adapter 拷进<卡带>/adapters/{kind}/,但解析从来不看卡带目录,挂上去的东西运行时根本不加载。现在挂载卡带里的副本优先于库里的同名 adapter。 - 测试写出来才发现的第二个 bug:
ready.sort(key=_preference)丢了原来的reverse=True,库里的 adapter 反而赢了挂载副本。 - 状态只由事实支撑:loaded(模型还活着——
record_load存weakref,且记录挂在具体QwenRuntime._attached上,不是进程全局)/ available(manifest 与adapter_model.safetensors都在;模型没了就说「attached earlier in this process … that model is gone」)/ deferred(base_model 不匹配或缺权重文件,原因里写两个模型名)/ missing(磁盘上没有该 kind)。落选者单独一行shadowed=True,且只在真有赢家时才有这行,原因点名赢家。 - 排序键是三个:挂载卡带 > manifest 带
cartridge_id> 权重 mtime。第一个版本漏了中间那个,reviewer 在 /tmp 里对比 parent9f0f799与本提交选中的文件,量出两者不同(可复现做法:先写一个带cartridge_id的 adapter,再写一个不带但 mtime 更新的,看resolve选哪个)——training.py:106写库内 adapter 时会带卡带标记,所以这不是风格问题。 - 验收命令:
pytest backend/tests/test_adapters.py -q→ 16 passed。test_stub_weights_prove_the_load_branch_runs用桩权重断言attached == [(ANY, 路径)]、model.evalled is True、LoadRecord 存在、端点该行state == "loaded";test_generation_reports_the_adapter_it_attached走完 transformers 生成路径,断言答案里的 adapter 路径 / kind / id / sha256 与output_tokens;test_unload_stops_claiming_a_model_that_is_gone断言 unload 后回到 available 并说明模型已消失,同时断言另一个实例不会继承这个声明;test_a_loaded_row_names_the_adapter_that_is_actually_attached与test_one_unload_does_not_declare_another_live_model_gone钉住第二轮那两条(行里的身份列必须跟加载记录一致;一个模型 unload 不能说成另一个活模型消失了)。两条都做了变异检验:把行身份改回按解析结果取 → 前者 FAILED;让live_attachments()忽略存活 → 后两个 FAILED。test_three_live_models_are_named_as_models_not_one_model钉住第三轮那条单复数 bug。 - 证明与生成解析一致:
proofs/checks_runtime.py的_generation_adapter()用discover+resolve(含挂载卡带),不再用只看库的解析,否则同一份证明里resolved_adapter与adapter两块会指向不同副本还静默。 - 新文件
backend/src/ely_eye/runtime_schemas.py(30 行):schemas.py已 498 行,离 500 行红线 2 行,报告类型不放进去。 - 门禁:
pytest -q228 passed;ruff 干净;mypy 冷缓存查 3 个改动源文件 → 2 errors,与git stash后同一命令在 HEAD 树上的实测完全同两条(qwen_vl_utils无 stubs、model_class重赋值),净新增 0;pyright 同文件 → 2 errors,HEAD 也是 2 条,都是process_vision_info的双返回(vision_process.py:532-534由return_video_kwargs决定返回 2 或 3 元组,产品用默认值取 2 个,运行时正确)。 - 独立 review(同一个
p1review里的 pi,模型strata/qwen3.8-flash-next-abliterated-iq3_s:high)第一轮 FAIL 6 条,全部核实并改掉:plan()用chosen_paths判落选,把「被拒绝的候选」也当成落选者,于是没有任何 adapter 胜出时却说「another adapter of this kind wins」,还把真正的 deferred 原因盖掉了(reviewer 挂载 cart-home 复现,hme_router、hme_vision_lora两行都在撒谎)。_LOADED是进程全局,而原因写「attached to the live model」——api.py每个请求新建一个QwenRuntime,_model is None也报 loaded。改成 weakref + 实例字段。- CHECKLIST 那句「peft 只在
adapters.load_into_model里」在全仓范围不真(training.py 两处、training_trace.py 一处),已限定为生成路径。 LoadRecord.loaded_at写了没人读 → 删。_preference丢了manifest.cartridge_id is not None这个次级键,换掉了原来会加载哪个 adapter(见上)。- 证明检查与生成解析不一致(见上)。
reviewer 还指出 4 条生成路径与
status()零覆盖,所以「没丢函数」当时只是读出来的——现在有了走完生成路径的测试。
- 第二轮 FAIL 1 条(同一 reviewer,它自己建 worktree +
PYTHONPATH跑 harness,并记下 editable install 指向主树、不加PYTHONPATH的 worktree 测试会假绿):plan()里 loaded 行的reason按加载记录写,而path/adapter_id/base_model/origin/cartridge_id按当前解析结果写——实测:活模型挂 X,磁盘上又写了个更新的 Y,端点就返回adapter_id=adapter-Y却解释「attached from …/x-attached」。同一个根因的第二个症状:登记按 kind 存、后写覆盖,两个 runtime 时 unload 其中一个会把另一个还活着的模型说成「that model is gone」。修法:LoadRecord直接携带AdapterSource,行身份从记录取(_choice()只有一个身份来源);登记改成按 attachment 列表 + 各自 weakref,loaded只看是否还有活模型持有,多于一个时原因里写明另一个。 - 第三轮 FAIL 2 条(都是措辞与重复字段,reviewer 用三个活 runtime 实测):
adapters.py的plan()在三个以上模型同时持有时写「another live model still holds adapter-a and adapter-b」,单数主语把多个 adapter 塞给了一个模型;LoadRecord.sha256仍是source.manifest.sha256的存储副本,正是这次刚清掉的「第二个真相来源」。修法:主语按数量分单复数,sha256改成 property。两条都有变异检验:去掉复数分支 → 两个测试 FAILED;换回存储副本 → 生成路径测试 FAILED。reviewer 另记三条它判定不算缺陷的,这里补记:base_model在「磁盘上没有该 kind」的行上从「运行时的模型 id」改成null(更诚实:那一行根本没有 adapter);同一 adapter id 可能同时出现在 loaded 行与 shadowed 行(它自已被挂着,但挂载里另有副本胜出,两行都真);被加载的 adapter 从磁盘删掉且无解析结果时不再说「that model is gone」(missing行身份为 null,没有假断言)。 - 第四轮 PASS(同一个 reviewer,39 条命令、15分44秒,不读我的话只跑东西):它自己写探针复现了三个场景(挂 X 后写更新的 Y → 行说 adapter-x;两个 runtime 一个 unload → 仍 loaded/adapter-y 且不说 gone;两个都 unload → available + gone),自己跑变异测试(行身份改回解析结果 → 3 个测试 FAILED;
live_attachments()忽略存活 → 2 个 FAILED;去掉复数主语 → 2 个 FAILED),并逐项核了仓库规则(无文件 >500 行、新增#注释 0 行、4 个单行 docstring、无新造 scenario 字段、无新依赖、无 v1/v2、无死代码)。它另记一条有界陈旧:live_attachments()只在下次record_load时清理,所以刚被回收的模型在下一次加载前仍可能显示 loaded(docstring 写了)。它还提了一条「挂载是按连接的」——这条我实测推翻:mounts是表,同一个 home 上换一个Database对象读mounted_ids仍返回['cart-a'];那个测试断言mounted == []只是因为那个测试本身没挂任何东西。 - CLI 面:
ely-eye adapters打印同一份报告。
- p1-3.1 按卡带/场景解析 6 个 adapter,不再只有
- p1-4 工程地基
- p1-4.1 pytest 从零到有(后端)
- 验收写的路径是
tests/,实际在backend/tests/(19 个文件:18 个test_*.py+conftest.py);pytest -q全仓 253 passed(922d012上是 239,maintenance 那 14 个测试把它推到 253)
- 验收写的路径是
- p1-4.2 go test 从零到有(MCP)
- 5 个
*_test.go(internal/apiclient/client_test.go4 个、internal/mcpserver/write_tools_test.go6 个等),go build ./... && go vet ./... && go test ./... && gofmt -l .全干净
- 5 个
- p1-4.3 CI 在 Linux runner 上跑真测试
.github/workflows/ci.yml四个 job:backend(python 3.12 =requires-python的下限,pip install -e "./backend[dev]"→python -m pytest -q→ruff check backend/src bench)、mcp(go 1.25 build/vet/test + gofmt)、interface(node 22npm ci && npm run build)、image(docker build+ 起容器 curl/api/status与/)- 首跑 run 37736363930:image / interface / mcp 绿,backend 红在
No module named pytest—— pytest 不是任何地方的依赖,所以「跑测试」这件事在一个新克隆上根本不可执行。修法是把测试工具写进依赖声明:backend/pyproject.toml新增[project.optional-dependencies] dev = ["pytest>=9.1.0", "ruff==0.16.10"],CI 与新人装的是同一套,ruff 也不再靠 CI 里一个裸版本号漂移 - 为了让 CI 真能当门禁,先清了仓库既有的 lint 债:
ruff check backend/src bench在 HEAD 上是 17 errors,现在 All checks passed(14 条机械--fix,加profiles.py三个探针收窄异常面:linux_package_versions是(OSError, subprocess.SubprocessError, ValueError)——json.loads需要ValueError;runtime_launcher_probe与gpu_available是(OSError, subprocess.SubprocessError);并显式挡住json.loads返回 dict 的假设) - 第二跑 run 37738221392 仍红:
bench/tests/test_joint.py四条挂在SystemExit: bench/models/paraphrase-multilingual-MiniLM-L12-v2: model not downloaded。那四条断言的是「联合跑与单臂跑排序一致」「三个需要 BM25 的臂只算一次稀疏信号」——这是harness 的等价性,不是 embedding 的好坏,所以不该要求先下 470MB。修法是在两条路径共用的接缝注入确定性的 384 维替身模型与替身 reranker(_TinyDense/_TinyReranker/_stub_models),报告里的测量仍由真模型承担 - 本地按 CI 的条件验过:把
paraphrase-multilingual-MiniLM-L12-v2和bge-reranker-base两个目录移出仓库,全套 253 passed;移回来后门禁数字不变 - 绿:run 37741248855 @
4b367cb与 run 37744547992 @20c0416都是conclusion=success,四个 job(backend / mcp / interface / image)全绿;之后每个 push 都跑同一套
- p1-4.4
Dockerfile不再 COPY 缺失的PRD.md- 干净克隆实测:
git clone之后ls PRD.md→No such file or directory(PRD 一直没进 git),旧镜像在COPY README.md PRD.md ./那一步就建不出来 - 改两阶段:
node:22-bookworm-slim AS ui跑npm ci && npm run build(Ubuntu 24.04 自带的 node 18 建不了 Vite 8,frontend/package.json是vite ^8.0.0),运行阶段pip install ./backend(不再-e)+COPY --from=ui /ui/dist /srv/ely-eye/ui,node/npm 从运行阶段去掉 - 建了却没人服务也是假的:
config.py加ui_dir(envELY_EYE_UI_DIR),api.py用StaticFiles(html=True)同源挂/;目录被设了但不存在 →SystemExit点名npm run build,不静默降级。前端本来就打相对/api/...,所以同源不需要改客户端 - 实测:干净克隆
docker build -t ely-eye:ci .→Successfully built 09803cd49add(22.4GB,torch CUDA wheel);docker run -p 18765:8765→GET /api/status200(available:false+SGLang endpoint is not reachable,无模型服务器时应有)、GET /200 text/html 带/assets/index-C4NuxpJV.js(200,208733 字节)与 css 200 - 另加
.dockerignore(commit36f40c4):没有它从这台开发机建镜像要送 81G 构建上下文(du -sh .:bench/runs60.2GB、bench/corpus20.7GB、.venv2.5GB),而 Dockerfile 一个字都不读
- 干净克隆实测:
- p1-4.5 去掉
RTX 4090/Ubuntu硬编码,改可配置- 根因是量出来的:这台机器
printenv里WSL_DISTRO_NAME=Ubuntu已设,也就是在 WSL 内部地 shell 本来就是 Linux,wsl.exe那一跳只有从 Windows 宿主才需要。旧代码无条件wsl.exe -d Ubuntu,于是原生 Linux 上六个 profile 全被报成 missing,哪怕卡就在机器上 profiles.py重写宿主判定:uses_wsl()(WSL_DISTRO_NAME有值即假)、host_shell()、host_path()、gpu_available()(替掉wsl_gpu_available())、host_python_command()、launch_script()(按宿主选.ps1/.sh);Settings.wsl_distro可配;schemas.py:442字段名wsl_gpu→gpu_available(Linux 上叫 wsl 是撒谎)- 设备名致命这条也拆了:
proofs/training_trace.py新增cuda_bf16_training(environment),判据是cuda_available and bf16_supported(training.training_environment()本来就记了这两个事实),不再"RTX 4090" in device_name;training.py:1271报错里那句 PRD 卡名删掉 - 验收就是桩:PATH 上放一个打印
NVIDIA GeForce RTX 5090的假nvidia-smi→gpu_available为真(顺带证明bash -lc会带 PATH);exit 127 的桩 → 假;cuda_bf16_training对 5090、4080 都为真,对bf16_supported=False的机器为假(测试自己伪造那个 flag;这台机器的卡真是 RTX 4090,而 4090 是 Ada 8.9、本身支持 BF16,所以拿卡名当判据正是这次要拆的东西);require_cuda_bf16的报错里不含 4090。backend/tests/test_host_profiles.py8 个测试,含「每个 profile 在本机都有对应脚本」
- 根因是量出来的:这台机器
- p1-4.6 补
.sh脚本,与.ps1对等- 14 个
.sh:六个 launch(sglang live/extreme、vllm live/extreme、ktransformers、llamacpp,flag 与.ps1逐条对齐)+ install / install-cuda-torch / setup-linux-runtimes / serve-backend / serve-frontend / verify / download-models / run-prd-finetune;全部chmod +x且bash -n干净 - 六个跨 WSL 的
.ps1改成$distro = if ($env:ELY_EYE_WSL_DISTRO) { $env:ELY_EYE_WSL_DISTRO } else { "Ubuntu" }再wsl.exe -d $distro - 诚实记录两处没证到的:这台机器没有 PowerShell 解析器,那六个
.ps1的改动是机械替换、未经语法校验;scripts/run-all-prd-finetunes.ps1(100 行)还没有.sh对应
- 14 个
- p1-4.7
LICENSE- 选 AGPL-3.0-only,依据是查来的不是猜的:
pymupdf安装元数据写Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License,而gh repo view --json visibility→ PUBLIC,分发真的发生,所以 AGPL 的 network 条款真的会咬人。PyMuPDF 是 PDF 页/区块 atom 的承重墙,只要它还在,写 MIT 就是不诚实 LICENSE与 gnu.org 原文逐字节相同(sha2560d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0);backend/pyproject.toml加license = "AGPL-3.0-only",pip wheel实测元数据出License-Expression: AGPL-3.0-only;frontend/package.json同值;README 新增 License 段写明版权人与「买 Artifex 商业授权才谈得上宽松许可」- 这是业务决定不是工程决定:owner 若要 MIT,得先处置 PyMuPDF(买授权或换 reader),我在 README 与这里都标出来
- 选 AGPL-3.0-only,依据是查来的不是猜的:
- p1-4.8
backup/restore/doctorbackend/src/ely_eye/maintenance.py+cli_maintenance.py(走仓库已有的register(app, settings)模式,因为cli.py已 490 行,再塞三个命令就破 500 规则)。backup 用 SQLite 自己的在线备份 API(服务在跑也能备,不是shutil.copy拷一个正在写的 WAL 库),清单记 schema 版本 / 行数 / sha256 / 体积 / 版本,行数是从副本读回来的- restore 的顺序是校验在先:sha256 对不上清单 → 拒绝;
PRAGMA integrity_check不是 ok → 拒绝;缺必需表 → 拒绝;schema 比本构建新 → 拒绝;被替换的库连同-wal/-shm留成.before-restore;然后Database(settings).init(),所以旧备份是「进来时被迁移」而不是要求人再跑一次 - doctor 分两级:problems 让库停摆(integrity、打不开、挂载的卡带文件没了、ui_dir 设了但没建),warnings 只说明降级(快照缺失、视觉索引落后、dense 索引落后)
- 真数据实测(
bench/runs/baseline,948,822,016 字节 = 904.8 MiB / 165,973 atoms / 50,278 sources):当时库是 schema 7、mtime2026-10-08 02:33:36,backup清单里的 sha256c815c6049c5bb562…用sha256sum独立核对一致;restore到新 home 后读回 165,973 atoms / 50,278 sources / schema 7 全等;doctorok=true、0 problems,并给出真降级警告:「the store holds 21362 picture atoms but no CLIP snapshot is set (ELY_EYE_CLIP_MODEL_DIR), so they are findable only by the text already on them」 - 那个 sha256 是那份快照的指纹,不是目录的:同一状态下连备两次哈希逐字节相同(所以它是确定的),但库被写过,哈希就跟着走。p2-1 之后这个文件是 mtime
2026-10-08 15:54:54、948,822,016 字节、sha25661244b6de73d478c4210ef79af2c4a21ef753cbc90b2806bb25a3617f236fe26、schema 8;现在再backup得到0d2d31bcda9d790b3d03cd9094e11af25664cf05d209fe7e060bc142fb783150(2.56 秒,读回 165,973 atoms / 50,278 sources / schema 8)。把它推到 schema 8 的是产品 CLI而不是 harness:p2-1.4 建 CLIP 图片索引时ely-eye index-visual在 15:54 打开了这个库,而bench/scratch.py那条「harness 只动副本」的规则管不到产品命令;harness 那侧复核过,python -m bench.scale_check跑完文件逐字节不变。秒数不写死:我这次 backup 4.0 秒 / restore 2.8 秒,reviewer 复跑 2.24 秒 / 5.65 秒,p2-1 这次 backup 2.56 秒,同一台机器负载不同就差这么多,只有行数和哈希是可复现的 - doctor 第一版是假健康:它只对「路径设了但目录不在」报警,对「库里有 21,362 个图片 atom 却压根没配 CLIP」报 ok=true。现在按库里的 modality 计数判,
test_doctor_names_the_senses_a_store_holds_but_cannot_use钉住这条 - 测试写出来才发现的两条:
Database.__init__每次构造都跑迁移,所以 doctor 永远看不到旧 schema,我原先那句 remediation「runely-eye statusto migrate」是编的 → 改成 catchSystemExit报「the store cannot be opened by this build: …」;另一条是 fixture 假设错(_store()往已有库里加,被替换的库确实有 3 个 atom),改测试不改产品
- 验收:CI 绿;干净 clone 上
docker build成功(已实测09803cd49add+ 容器 curl 通);桩nvidia-smi打印 RTX 5090 证明设备名不再致命(已实测) - 独立 review(同一个
p1review里的 pi,模型strata/qwen3.8-flash-next-abliterated-iq3_s:high,三轮,不读我的话只跑东西):第一轮 FAIL 9 条(全是 CHECKLIST 里的数字与断言不重现:测试文件数、套件规模、构建上下文体积、profiles.py异常元组、training.py行号、cli.py行数、把 4090 说成不支持 BF16、备份摘要不可复现、秒数写死)→ 第二轮 FAIL 1 条(_TinyDense那两行 docstring 是 p1-4 唯一新增的多行注释,跟 p1-6.4 的「≤1 行」相冲)→ 第三轮 PASS。reviewer 自己跑gh run view、pytest、ruff、mypy(冷缓存逐文件比对908157f与 HEAD → 两边都是 24 错/6 文件,逐条同消息)、pyright、go 全套、bash -n15 个.sh、pip wheel、sha256sum LICENSE与 gnu.org 逐字节比对、拉起 22.4GB 容器 curl 三个 URL,并在真 worktree 里做变异测试(去掉 reranker 桩 → 2 failed;去掉 dense 桩 → 正好复现 CI 那 4 条;只翻bench/systems.py:414的排序键 → 正好fusion_rerank那条挂)。它还量了全仓多行 docstring 数114 → 114,确认 p1-4 现在新增 0 条。三轮同一个人,没换人放水。 - 门禁:
pytest -q253 passed;ruff check backend/src benchAll checks passed(HEAD 是 17 errors);mypy 冷缓存查改动文件与 HEAD 同错、净新增 0;pyright查 maintenance 三个文件 → 0 errors, 0 warnings;go build/vet/test + gofmt 干净;bash -n15 个.sh干净;pip wheel ./backend能建且元数据带 license 表达式
- p1-4.1 pytest 从零到有(后端)
- p1-5 产品面
- p1-5.1 搜索端点接上前端
- 验收写的是
GET /api/search,实际是 p0-3 就有的POST /api/search:查询体带 holder / scenario / modality / image,GET 塞不下这些。前端search()调它
- 验收写的是
- p1-5.2 前端:搜索、浏览、forget、挂载卡带、时间显示、图像渲染
App.tsx517 行拆成 131 行的壳 +components/{MemoryPanel,CartridgePanel,SensesPanel,AdapterPanel,ProofPanel,IngestPanel,CompilerPanel,ui}.tsx+lib/format.ts,每个面板自己取数,前端没有一个文件超 500 行- 为此新增的后端能力在
backend/src/ely_eye/browse.py:GET /api/atoms(游标翻页 + modality / holder / source 过滤,读v_atoms_current)与GET /api/atoms/{atom_id}/image(FileResponse;没图 → 404 点名那颗 atom) - 时间显示:
formatAgo(just now / 5m / 2h / 3d / 1y)与formatStamp(写死 UTC,2026-10-08 09:03Z);每行是holder · origin · scenario · 时间 · trust - 图像渲染:
<img class="thumb" src=/api/atoms/{id}/image>,E2E 断言naturalWidth > 0——真解码出像素,不是有个标签就算
- p1-5.3
types.ts补回记忆维度- 验收写的
time在 p0-1 已改名ingested_at,另有event_time / valid_from / valid_to。补的是source_id, ingested_at, holder_id, origin, scenario, event_time, event_time_precision, valid_from, valid_to, trust。relations没补:至今没人写 atom 的 relations(p0 记过这条),前端拿它没东西可渲染 - 另加
AtomPage, SearchFilters, SenseStatus, CartridgeManifest, MountState, AdapterChoice, AdapterLoadReport, Runtime*Profile*, ForgetReport;MemoryMap类型随端点一起删
- 验收写的
- p1-5.4 未用端点:接上或删除
- 删 8 个:
/api/benchmarks/{registry,sources,samples,predictions}、/api/research/modules、/api/index/dense、/api/visual-contradiction-proof、/api/memory-map(前端是 memory-map 唯一的消费者,状态卡本来就带 layer 与卡带数)。底层函数一个没删——cli.py与proofs/checks_visual.py在用;删的是没人调的 HTTP 壳 - 加 2 个:
/api/atoms、/api/atoms/{atom_id}/image;/api/status现在带senses。HTTP 面从 36 条到 30 条 - 反漂移门禁:
backend/tests/test_product_api.py::test_the_http_surface_is_exactly_what_someone_uses把 30 条路由钉成集合,多一条少一条都红
- 删 8 个:
- p1-5.5 E2E 五项
scripts/ui_smoke.sh:mktemp -d起一个干净 home(ELY_EYE_DEFAULT_HOLDER=wife、ELY_EYE_UI_DIR=frontend/dist),API 自己用StaticFiles(html=True)同源挂 UI,浏览器只对一个 origin 说话,收尾删 home- 五项都在真浏览器里做:① 搜
spare glasses+ scenariohome→ 那行必须同时写wife和home(「谁在什么场景下知道」就是这个意思)② 按 modality 过滤 → 无字图片渲染出真像素;翻页用行的data-atom身份证明,不用行数——搜索列表和浏览列表共用.atom-row,「行数变了」可以只是上面那条搜索命中在闪。断言:第一页正好PAGE_SIZE=12行(与MemoryPanel.tsx:10同值)、下一页与第一页零重叠、末页行数 =total − 12(API 报的 total)、Prev 回到原来那串 id ③ Forget → 提示forgotten: N atoms,再搜同一句返回「Nothing in the store answers」④ Mount → 状态真的变了 ⑤ Adapter Matrix 每个 kind 一行,状态落在loaded|available|deferred|missing内且必须带原因 - 实测:拒绝挂载的原文是
cannot verify <cartridge_id> on this machine: model_hash, tokenizer_hash, chat_template_hash, rope are unknown; set ELY_EYE_MODEL_DIR to the model snapshot(cartridge.py:169-173,p0-4 那条「没有 model_dir 就拒绝挂载」的偏差在 UI 里看得见;smoke 打印前 200 字符,所以整句都在);adapters: 6 kinds: missing ×6,每条原因是「no adapter of kind X is on disk」;KPI 显示Runtime ready、Qwen/Qwen3.5-9B、NVIDIA GeForce RTX 4090 20901/23028 MiB、Cartridges 1 - 失败留证据:截图
ui-failure.png+ Memory 面板文本 + 那一刻/api/atoms?modality=…返回的 JSON - 网络噪声分开判:
404 /api/proof-suite/latest(没跑过证明的库就该 404,面板写 pending)和被拒的挂载 400 算预期,其余 ≥400 一律红;JS 未捕获异常一律红
- 三条是 E2E 逼出来的真缺陷,都不是样式问题:
AdapterPanel的指标条渲染load.gpu_available与load.probe_status,而AdapterLoadReport(runtime_schemas.py:27)只有model_id / mounted / adapters——没 GPU 的机器上它会永远显示「no gpu visible」,而这台机器有 4090。删掉那两个 span 和types.ts里编出来的五个字段,只渲染服务端真给的- 我第一版 E2E 硬写
modality=image,结果 0 行:ingestion_media.py:277按文件名判 kind(ui_markers = ("screenshot","screen","figma","ui","wireframe","mockup")),我那个文件叫ui-smoke-wordless.png→ 存成ui_screenshot。改成从/api/atoms读回那颗 atom 的 modality 再选,库说是啥就是啥。顺带记一条本轮没动的产品缺陷:"ui"是子串匹配,guide.png、quickstart.png都会被叫成 UI 截图;不改是因为冻结的 bench 数字是在现规则下量的 - playwright 1.60 在 Ubuntu 26.04 上没有 chromium 构建(
Playwright does not support chromium on ubuntu26.04-x64),npm i -D playwright@1.64.0才有;而且它的 npm 包不自动下浏览器,要npx playwright install chromium
- 顺手挖掉的产品根因:
/api/status与doctor各算一遍「这台机器能感知什么」,两份真相。抽成backend/src/ely_eye/senses.py::sense_facts(),两边只渲染它。图片计数从 modality 改成「有没有image_ref」:按 modality 数会漏掉 pdf_region 的截图裁块,也看不见字节已被删走的 atom。现在picture_bytes_missing会让 doctor 说「N picture atoms with no bytes behind them; re-ingest the source they came from」,而 CLIP 那条警告只数还能编码的 - 验收:
bash scripts/ui_smoke.sh→PASS: the console searches, pages, forgets, mounts and reports adapter states;pytest -q263 passed;ruff check8 个改动文件 All checks passed;mypy 冷缓存查这 8 个文件Success: no issues found,全仓仍是 24 错 / 6 文件(与 p1-4 记的基线相同,净新增 0);pyright这 8 个文件 0 errors, 0 warnings;go build/vet/test + gofmt 干净;npm run build+eslint .干净;scripts/mcp_write_smoke.sh仍 PASS - 独立 review(herdr 里的 pi,模型
strata/qwen3.8-flash-next-abliterated-iq3_s:high)。第 1 轮 FAIL 两条,都在我那条提交里:schemas.py被我从 498 顶到 534 行,破了「单文件 ≤500 行」;同一提交净增 3 条多行 docstring(114→117)。修在6df713d:自报家族(CacheLayerStatus/MemoryStatus/RuntimeStatus/三个 state 别名/SenseStatus/SystemStatus)搬进backend/src/ely_eye/status_schemas.py(它只需要schemas.py的TrainingAdapterKind,反向不依赖),schemas.py 回到 469;三条 docstring 各压成一行,全仓多行 docstring 回到 114,与父提交同数。 - 第 2 轮 FAIL 三条:① 翻页其实没被测到——E2E 等的是「行数变化」,而搜索列表与浏览列表共用
.atom-row,那一刻的 13 行里有一条是搜索命中;把nextPage()里的setCursor+loadBrowse删掉、或把翻页改成追加而不是替换,E2E 都还 PASS。改成data-testid="browse-list"+ 每行data-atom,断言第一页正好 12 行、下一页零重叠、末页 =total−12、Prev 复原;两个变异现在各自红:Next page did not swap the page: the pager is not paging与the next page repeated 12 of 12 atoms from the first page② CHECKLIST 写App.tsx「130 行的壳」,wc -l是 131 ③ CHECKLIST 引的拒绝挂载原文被 smoke 的.slice(0,120)砍掉了后半句; set ELY_EYE_MODEL_DIR to the model snapshot,改成 slice 200 并照原文记。 - 同一轮实测为真:263 passed;ruff 干净;全仓 mypy 24 错/6 文件(净新增 0);go build/vet/test+gofmt 干净;
npm run build+eslint .干净;ui_smoke.shPASS(4.0s);mcp_write_smoke.shPASS(3.2s);30 条路由与HTTP_SURFACE一致;senses 单一真相(api.py:52/:105与maintenance.py:238都只渲染sense_facts());26 个types.ts接口对着 pydantic 模型逐个 diff 零编造字段;8 条被删路由无人引用,它们背后的函数都还从 CLI/proof 可达。pyright 在那 8 个文件:reviewer 的工作树 0 errors,我这棵树多两条runtime.py:283的 tuple 告警——它只在qwen_vl_utils那个无类型导入被解析时出现,HEAD 的副本同样有,不是本轮引入;两边都不是新增。 - 第 3 轮 PASS(
9f91ff7..b0895eb,~40 条 shell / 14 次工具调用,本轮 34 分钟)。reviewer 是跑过不是读过:ui_smoke.shexit 0(4.26s,拒绝挂载整句可见);三个变异各自红——删nextPage()的 setCursor+loadBrowse →the pager is not paging,翻页改追加 →repeated 12 of 12,它自己加的第三个(删previousPage()的复原)→Timeout 15000ms;改完git checkout后回到 exit 0。门禁重跑:263 passed、npm build+lint 干净、go build/vet/test+gofmt 干净、mcp_write_smoke.shPASS 3.26s、ruff 干净、mypy 冷缓存查 8 个文件 Success、全仓 mypy 24 错/6 文件。数字全对:schemas.py 469、status_schemas.py 74(只从 schemas 拿TrainingAdapterKind,反向无导入)、多行 docstring 114、App.tsx 131、拒绝文案与cartridge.py:169-173逐字一致、30 条路由与HTTP_SURFACE完全相等且 8 条删掉的都报 gone、PAGE_SIZE=12两处同值、末页 135−12=123 行、types.ts26 个接口、前端最大文件styles.css492。它同时指出两点不是缺陷但记着:PAGE_SIZE是手拄到 harness 的(组件改了会变红不会变绿,已在记录里写明);末页的total−12读的是未过滤的GET /api/atoms?limit=1,只对是因为前一行刚好把 modality 清掉了。
- p1-5.1 搜索端点接上前端
- p1-6 代码卫生
- p1-6.1 每个文件 ≤500 行
- 验收命令
python -m bench.filesize_check→OK: no tracked source file exceeds 500 lines。这个脚本是本轮加的:500 行规则之前只靠记忆执行 evals.py2413 行在 p0-6 已拆成proofs/*(最大 430)。本轮拆的:db.py503→396(BM25 检索搬进store_search.py116)、bench/run.py609→341(query_set.py83 +report.py211)、benchmarks.py619→424(benchmark_scoring.py262)、training.py1630→94(training_{common,encode,metrics,datasets,lora,retrieval}.py= 108/177/307/387/408/358,TrainingService留成薄壳喂cli.py的 11 个调用点、7 个方法)、schemas.py534→469(p1-5 拆出status_schemas.py74)- 「拆分不改行为」的证据:脚本把每个搬走函数的函数体去缩进后与 HEAD 逐字节比对——training 侧 53 个、benchmarks 侧 21 个,全部相同,0 处不同,0 个缺失。方法转函数只重写 def 头,
self.settings→settings这类替换是正文里唯一的改动 - 新增
backend/tests/test_training_split.py(6 条):CPU 能跑的纯函数逐个测;最后一条用inspect.signature断言薄壳参数表 = 模块函数参数表去掉注入的两个参数——这条就是抓self泄漏那类 bug 的
- 验收命令
- p1-6.2 删 dead code 与未调用函数
- 度量口径(第一版误报 201 个,因为只查了别的文件):每个顶层
def对所有跟踪文件计数(含自己),再排除 pytest 用例与 typer 命令(框架按名字调)→ 8 个零引用,全删:cartridge_store.py:47 cartridges_holding_atom、cartridge_store.py:97 clear_mounts、training.py:1316 gradient_checkpointing_enabled、retrieval.py:172 group_hits_by_source、bench/ingest_baseline.py:239 ingested_paths、proofs/report.py:83 is_finite_number、runtime.py:432 parse_runtime_json、ingestion_repository.py:317 write_sources_json;重扫 0 - 两个为什么是死的:
memory.py靠读每颗卡带自己的sources.json回答归属(导出的卡带可以不在库里);set_mounts(database, [], stamp)换集合本来就能清空挂载表 - 顺手删
backend/build/(712KB 的python -m build残留,gitignore 内,0 个跟踪文件)——它让 grep 撒谎
- 度量口径(第一版误报 201 个,因为只查了别的文件):每个顶层
- p1-6.3 删 v1/v2/v3 废弃端口
- 量完的结论是没有可删的,不是没做:每个
v1/v2/v3都承重——磁盘格式标签(ely-eye-memory-capsule-v1proofs/checks_cartridge.py:128、ely-eye-{text,visual}-vectors-v1、ely-eye-kivi-replay-v1cartridge_assets.py:100,127,158)、迁移阶梯(migrations.py:181,186,239,241,243,行号从 commit 本身量:git show <sha>:path | grep -n,工作区会被 pi-lens 重排而漂 9 行)、矛盾语料 fixture 名(proofs/contradictions.py:114-135)、第三方名字(MiniLM-L12-v2、ScreenSpot-v2、LongBench v2)、OpenAI 兼容的/v1URL、缓存键里的compiler-v2salt。没有废弃端口、别名或双代码路径
- 量完的结论是没有可删的,不是没做:每个
- p1-6.4 注释压到 ≤1 行
- 口径:
tokenize找连续#行块。全仓 39 块 / 86 行 → 0 块,每块压成一行,压的时候留承重那句,实测数字与错误原文不删(Unable to allocate 582. MiB for an array with shape (5632, 9024, 3)、288,266 rows for 165,973 atoms、[^[^\W\s]在 3.14 会吃掉 CJK) - docstring 不动(全仓多行 docstring 114 条,与父提交同数):docstring 是模块与函数的契约,不是注释;p1-4 review 用的口径也是「新增多行 docstring 数不得增加」。第一轮 review 抓到搬家途中塌了 3 条——
store_search.py的search_atoms(那条「每个过滤条件都是同一条字面 SQL 里的? IS NULL OR谓词」规则和as_holder的共享规则)和FtsPage、bench/run.py的模块 docstring——已按原文补回,行数仍全部在 500 以内
- 口径:
- 门禁:
pytest -q269 passed;ruff check backend/src/ely_eye backend/tests benchAll checks passed;mypy 冷缓存 24 errors / 9 files(基线 24/6,training.py 那 6 条随拆分落到 4 个新文件,净新增 0);pyright查 46 个改动文件 → 4 errors,与 HEAD 单个training.py在包副本里量到的 4 条同源(qwen_vl_utils 元组返回、两处save_pretrained(Path)、bitsandbytes未解析);go build/vet/test + gofmt 干净;npm run build+ eslint 干净 - herdr review(同一个
p1reviewpi,strata/qwen3.8-flash-next-abliterated-iq3_s:high)三轮:round 1 FAIL 4 → round 2 FAIL 1 → round 3 PASS。round 1 它自己跑出门禁、行数、搬体同一性、死代码、注释压缩、v1/v2/v3 全部承重,报的 4 条里 1 条是真产品缺陷:拆文件时掉了一些多行 docstring(全仓 114→111),掉的是契约不是装饰——search_atoms的「每个过滤条件都是同一条字面 SQL 里的? IS NULL OR谓词」规则和as_holder共享规则、FtsPage的游标理由、bench/run.py的五臂说明(commit28959c5按原文补回,114 条同父提交);其余 3 条是我写错的数:store_search.py/bench/run.py/query_set.py行数、cli.py调用点数(实为 11 个调用点亗 7 个方法)。round 2 剩 1 条:migrations.py行号引成了 248/250,在 commit 里那是 PRAGMA 和空行——根因是我在工作区量的,而 pi-lens 会在本轮之外重排文件把行号推下 9 行;从此行号一律git show <sha>:path | grep -n从 commit 量(commit9682657)。round 3 它逐条重验:迁移阶梯 181/186/239/241/243 全中(并指出:243本身不带版本 token,grep "v1\|v2"拓不到),CHECKLIST 那一行其余引用全部重验(checks_cartridge.py:128、cartridge_assets.py:100/127/158、contradictions.py:114/135、config.py:20/27、cache_fabric.py:21、benchmarks.py:85、bench/build_screenspot.py:1/28),git diff --stat 28959c5..9682657只动一行,门禁重跑 269 passed / ruff / filesize_check 全绿
- p1-6.1 每个文件 ≤500 行
阶段 3 — P2:规模化、安全、分层与共享
- p2-1 规模
- p2-1.1
sqlite-vecvec0增量索引(backend/src/ely_eye/vector_index.py,schema v8)- 事实:向量与 atom 同库同事务,写路径(
memory.py/ingestion)在同一提交里维护它;旧的dense_index.npy+dense_index_atoms.json那对会腐烂的旁文件删掉了,build_index()全量重编码也没了。sync()只在失配时补,sync_atoms()按批补删。 - 实测出来的 vec0 0.1.9 坑(都是跑出来的,不是猜的):
metadata json不是合法表选项(只有metadata text);KNN 是WHERE <列> MATCH ? AND k = ?(匹配列不是表);int8/bit 的插入与查询向量必须套 SQL 构造器vec_int8(blob)/vec_bit(blob),sqlite_vec.serialize_int8()返回的裸 BLOB 会被拒(expected to be of type int8, but a float32 vector was provided);float32 不需要包装,也没有vec_float32();默认距离是 L2,cosine 是表选项。 - 偏差:0.1.9 的 vec0 没有 ANN,逐条扫,所以延迟与字节随规模线性——DENSE.md 把这条假设印在外推表旁边,不假装是对数。
- 事实:向量与 atom 同库同事务,写路径(
- p2-1.2 量化与截断:测了,没上
python -m bench.quant_probe→bench/reports/QUANT.md(165,973 向量、200 探针、top 10,tie-aware 一致度;下面每个 p50 逐字抄自那张表):sqlite-vec float 41.2ms/1.0、+holder 过滤 29.8/0.599、int8 35.5/0.977、int8+holder 22.3/0.595、bit 2.6ms/0.728、bit+holder 4.1/0.513、numpy float32 全扫(真值)8.0/1.0、numpy 符号位 hamming 18.9/0.011、float32 前 192/128/64 维 7.5/0.722、7.9/0.57、4.0/0.43、bit 粗排@200 → numpy 里 float32 精排 23.7/0.983。每向量字节 f32 1536 / int8 384 / bit 48。- 结论:int8 省 4× 空间几乎不掉质量(0.977)但不省时间(35.5 vs 41.2);bit 省 32× 空间、省 15.8× 时间(41.2→2.6),只剩 0.728,只能当粗排;两级方案必须在 vec0 外面精排(在库里用
rowid IN (…)回捞反而更慢)。维度截断是在不是 MRL 训练的 MiniLM 上测的,所以那是真实代价不是白拿:前 192 维 7.5ms、前 128 维 7.9ms,对全维 8.0ms 只快 1–6%,前 64 维才快一半但一致度掉到 0.43;切片必须是存下来的连续维,不是查询时切(非连续视图点集会掉速度)。 - 偏差:精排腿(
bge-reranker-base)早在基线消融里就有并测过(加了它 recall@10 从 0.565 掉到 0.453),所以这里没再为它编一套故事;粗排+精排这条链测出 0.983 一致度但没接线,因为 dense 腿默认关(见下)。
- p2-1.3 100k 实测 + 1M 外推:
python -m bench.dense_scale→bench/reports/DENSE.md(stamp dff5977)- 实测 165,973 atom / 153,857 向量(DENSE.md,stamp 71733d86d):建库 1444.9s(106 向量/秒)、检索 p50 73.0ms / p95 75.7ms / max 85.6ms(300 查询)、其中编码问题 p50 11.3ms / p95 24.0ms、与暴力扫描 tie-aware 一致度 0.998(40 探针)、往满索引里写一条记忆 p50 508ms / p95 535ms(50 次写)、失配扫描 1.3s(不在检索路径上)、每向量 1,729 字节(dbstat 数 vec0 影子表:266,059,776 B)、机器 load [3.53, 4.63, 4.85]。
- 1M 外推(分母是向量数,1,000,000 / 153,857,不是 atom 数——两者差 7.9%,所以报告把分母印在外推表旁边):p50 475ms / p95 492ms、向量表 1.6 GiB、首次建库 156.5 分钟、单条写 3303ms(vec0 按 id 删除也要扫表,所以写同样随规模线性),假设随行印出(逐条扫 ⇒ 线性)。
- tie-aware 的必要性:语料里有重复 atom,分数并列 ⇒ top-K 的 id 集合本身有歧义,两个都对引擎按 id 比会读出 0.827 的假召回。判定改成「返回的 atom 真实 cosine ≥ 第 K 名 cosine」。
- 验收:100k dense 腿 p50 <100ms → 165,973 atom(bar 的 1.66 倍)p50 73.0ms(DENSE.md);新 atom 立即可查 →
pytest backend/tests/test_vector_index.py(写进去当场能搜到、视图藏起来的 dense 腿也藏、备份带走向量恢复后照样应答)。 - p2-1.4 每条腿值多少钱:
python -m bench.leg_check→bench/reports/LEGS.md(stamp 见报告首行,300 条 holder 作用域探针,7 个配置,每臂带 n / MRR / Wilson 下界 / p50 / p95 / 分模态召回)。测的是bench/runs/dense_scale的副本:冻结基线自己不带向量,dense 腿在其上只会排一个空索引,所以报告首行就把来源 store 与向量数印出来。- 只有词的 FTS 页:问 10 个 atom recall@10 0.832、MRR 0.683、p50 67.5ms;问产品配置的 24 个 0.848 / 0.688 / 67.6ms;页放宽到 30 0.855 / 0.690 / 72.0ms(放宽不要钱,10→24 就白捡 1.6 个点)。
- 开 dense 腿:10 个 atom 0.845 / MRR 0.663 / p50 474.8ms;页 30 0.858 / 0.669 / 416.0ms。多 1.3–2.6 个点召回、MRR 降、慢 6–7 倍。
- 开图片腿。索引实物印在 LEGS.md 里(不是这里口头说的):53,668 个 atom、512 维、109,912,192 字节;编码速度也是实测的(200 张样本):16.8–19.9 张/秒,据此那 53,668 张 ≈ 53.3–45.0 分钟——和我当时手工记的 2901.7 秒(48.4 分钟)落在同一个带里。结果:10 个 atom 0.815 / MRR 0.553 / 138.1ms;页 30 0.825 / 0.551 / 166.7ms。分模态才看得见它在干什么:video 0.583→0.708、pdf 0.849→0.907 涨,code 0.953→0.898、ui_screenshot 0.583→0.500 跌——它救的正是金标是图的题,代价是稀释别的。
- p50 是负载函数,不是性常数:两份已提交的 LEGS.md 里,words 腿 64.7–72.5ms、dense 腿 413.7–474.8ms、图片腿 132.5–166.7ms,召回三跑到小数点后三位一模一样(0.832 / 0.848 / 0.855 / 0.845 / 0.858 / 0.815 / 0.825)。所以比较只用召回,延迟看报告当次的 load。
- 产品的图片腿不是开关:
retrieval.py:136if image is None and not thin: return {},稀疏页薄到 2 条以下才问图。所以这几行测的是「腿可用」,不是强行常开融合。 - 没达到的验收线,写在这里:用户定的验收是 recall@10 ≥0.90(n≥300)。测出来最好的一行是 0.858(dense 腿开、页 30、p50 416.0ms),默认配置 0.848(p50 67.6ms),CURRENT.md 里按 10 个 atom 问是 0.832;仓内 Mem0/Zep 式对照在 holder 作用域是 0.866。差距集中在图(image 0.750、ui_screenshot 0.500–0.583、video 0.583–0.708),文字的题(code 0.953、pdf 0.919)已经过了线。诚实结论:延迟达标(281 倍于基线、p50 71.7ms),召回没达标,剩下 4 个点是跨模态召回,不是索引规模问题。
- 数字逼出来的决定:dense 腿默认关(
config.py:32 dense_embeddings_enabled=False)。同一 300 条 holder 作用域探针上(LEGS.md),开腿 recall@10 0.832→0.845、MRR 0.683→0.663、p50 67.5ms→474.8ms。多 1.3 个点的召回、慢 7 倍、首位质量还降——默认值就该是关,腿留着(已增量、已入库)等更大的语料或更好的嵌入模型。 - 门禁:
pytest -q全绿;ruff check干净;mypy 冷缓存 24 errors / 9 files,与 p1-6 基线(9cb46c1同口径 24/9)净新增 0——p2-1 一度引入 +2(vector_index.py与quant_probe.py的import sqlite_vec没有 stub),根因修法是在pyproject.toml的[[tool.mypy.overrides]]里声明sqlite_vec无 stub,而不是在代码里加# type: ignore;./node_modules/.bin/pyright对改动文件 0 errors;bench/filesize_check干净;go / npm 未动。 - 顺带修的测量缺陷:
copy_store()只拷 SQLite,所以任何要跑视觉腿的 harness 都得自己带上objects/(25 GB,探针里用符号链接)与cache/clip_image_index.npy;bench/scale_check.py原本直接打开冻结基线库、把它原地迁移了,现改成在副本上测(--scratch)。产品 CLI 不在这条规则里:p2-1.4 建图片索引时ely-eye index-visual就把冻结基线从 schema 7 升到了 8,所以 p1-4.8 那两条钉的是当前的 mtime / sha256 / schema,并指名是哪一步改的。
- 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 行并排