From 28959c51c67a18cd202ab19639a2f9c6e7aae3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 10:02:24 -0400 Subject: [PATCH] Restore the contracts the split dropped and correct the record A split moves code; it does not get to rewrite what the code promises. The first pass collapsed three multi-line docstrings into one-liners and the line counts in the checklist were measured before that happened. - store_search.py: search_atoms keeps its filter-shape rule and the as_holder sharing rule, FtsPage keeps why the cursor is an offset - bench/run.py: the module docstring says what the five arms are again - CHECKLIST: 116/341/83 lines, 11 call sites over 7 methods, and the migration ladder now cites 181/186/248/250 instead of the parent's lines Repo-wide multi-line docstrings are back to 114, the parent's count. --- CHECKLIST.md | 6 +++--- backend/src/ely_eye/store_search.py | 16 +++++++++++++--- bench/run.py | 9 ++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHECKLIST.md b/CHECKLIST.md index 2441480..7ec9536 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -321,7 +321,7 @@ - [x] **p1-6** 代码卫生 - [x] p1-6.1 每个文件 ≤500 行 - 验收命令 `python -m bench.filesize_check` → `OK: no tracked source file exceeds 500 lines`。这个脚本是本轮加的:500 行规则之前只靠记忆执行 - - `evals.py` 2413 行在 p0-6 已拆成 `proofs/*`(最大 430)。本轮拆的:`db.py` 503→396(BM25 检索搬进 `store_search.py` 105)、`bench/run.py` 609→351(`query_set.py` 80 + `report.py` 211)、`benchmarks.py` 619→424(`benchmark_scoring.py` 262)、`training.py` 1630→94(`training_{common,encode,metrics,datasets,lora,retrieval}.py` = 108/177/307/387/408/358,`TrainingService` 留成薄壳喂 `cli.py` 的 10 个调用点)、`schemas.py` 534→469(p1-5 拆出 `status_schemas.py` 74) + - `evals.py` 2413 行在 p0-6 已拆成 `proofs/*`(最大 430)。本轮拆的:`db.py` 503→396(BM25 检索搬进 `store_search.py` 116)、`bench/run.py` 609→341(`query_set.py` 83 + `report.py` 211)、`benchmarks.py` 619→424(`benchmark_scoring.py` 262)、`training.py` 1630→94(`training_{common,encode,metrics,datasets,lora,retrieval}.py` = 108/177/307/387/408/358,`TrainingService` 留成薄壳喂 `cli.py` 的 11 个调用点、7 个方法)、`schemas.py` 534→469(p1-5 拆出 `status_schemas.py` 74) - 「拆分不改行为」的证据:脚本把每个搬走函数的**函数体去缩进后与 HEAD 逐字节比对**——training 侧 53 个、benchmarks 侧 21 个,**全部相同,0 处不同,0 个缺失**。方法转函数只重写 def 头,`self.settings`→`settings` 这类替换是正文里唯一的改动 - 新增 `backend/tests/test_training_split.py`(6 条):CPU 能跑的纯函数逐个测;最后一条用 `inspect.signature` 断言薄壳参数表 = 模块函数参数表去掉注入的两个参数——这条就是抓 `self` 泄漏那类 bug 的 - [x] p1-6.2 删 dead code 与未调用函数 @@ -329,10 +329,10 @@ - 两个为什么是死的:`memory.py` 靠读每颗卡带自己的 `sources.json` 回答归属(导出的卡带可以不在库里);`set_mounts(database, [], stamp)` 换集合本来就能清空挂载表 - 顺手删 `backend/build/`(712KB 的 `python -m build` 残留,gitignore 内,0 个跟踪文件)——它让 grep 撒谎 - [x] p1-6.3 删 v1/v2/v3 废弃端口 - - 量完的结论是**没有可删的**,不是没做:每个 `v1/v2/v3` 都承重——磁盘格式标签(`ely-eye-memory-capsule-v1` `proofs/checks_cartridge.py:128`、`ely-eye-{text,visual}-vectors-v1`、`ely-eye-kivi-replay-v1` `cartridge_assets.py:100,127,158`)、迁移阶梯(`migrations.py:185,190,243`)、矛盾语料 fixture 名(`proofs/contradictions.py:114-135`)、第三方名字(MiniLM-L12-v2、ScreenSpot-v2、LongBench v2)、OpenAI 兼容的 `/v1` URL、缓存键里的 `compiler-v2` salt。没有废弃端口、别名或双代码路径 + - 量完的结论是**没有可删的**,不是没做:每个 `v1/v2/v3` 都承重——磁盘格式标签(`ely-eye-memory-capsule-v1` `proofs/checks_cartridge.py:128`、`ely-eye-{text,visual}-vectors-v1`、`ely-eye-kivi-replay-v1` `cartridge_assets.py:100,127,158`)、迁移阶梯(`migrations.py:181,186,248,250`)、矛盾语料 fixture 名(`proofs/contradictions.py:114-135`)、第三方名字(MiniLM-L12-v2、ScreenSpot-v2、LongBench v2)、OpenAI 兼容的 `/v1` URL、缓存键里的 `compiler-v2` salt。没有废弃端口、别名或双代码路径 - [x] 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 数不得增加」 + - 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 -q` **269 passed**;`ruff check backend/src/ely_eye backend/tests bench` All 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 干净 --- diff --git a/backend/src/ely_eye/store_search.py b/backend/src/ely_eye/store_search.py index e61cc1e..6a194ec 100644 --- a/backend/src/ely_eye/store_search.py +++ b/backend/src/ely_eye/store_search.py @@ -12,14 +12,15 @@ from .text_index import fts_expression, fts_terms, rare_terms @dataclass(frozen=True) class FtsPage: - """A page of (atom_id, score) plus whether more matches exist.""" + """One page of BM25 hits plus whether the corpus holds more: the cursor is an offset, and a + ranking this small re-reads rather than keeps a server-side position.""" rows: list[tuple[str, float]] has_more: bool def search_fts(database: Database, query: str, limit: int) -> list[tuple[str, float]]: - """Best BM25 matches over the whole corpus, no filters.""" + """Best BM25 matches over the whole corpus, no filters: the ranking the product needs most days.""" return search_atoms(database, query, limit=limit).rows @@ -39,7 +40,16 @@ def search_atoms( cartridge_ids: list[str] | None = None, as_holder: str | None = None, ) -> FtsPage: - """BM25 over the FTS index joined to memories that are open right now and readable by the asker.""" + """BM25 over the FTS index joined to memories that are open right now and readable by the asker. + + Every filter is a `? IS NULL OR` predicate in one literal statement: the ranking never + depends on string-built SQL, and the planner sees one shape. A memory with no event time + cannot answer a time filter, so it drops out when one is asked for. + + `as_holder` applies the sharing rule: a memory is readable by its owner, by anyone named + in its visible_to list, by everyone when it is public, and by anyone when nobody owns it. + A memory with no holder cannot be private to a holder that does not exist. Without an + asker the query runs over the whole library, which is what an operator debugging wants.""" with database.connect() as connection: expression = fts_expression(rare_terms(connection, fts_terms(query))) rows = connection.execute( diff --git a/bench/run.py b/bench/run.py index 3259315..68545cf 100644 --- a/bench/run.py +++ b/bench/run.py @@ -1,4 +1,11 @@ -"""Run the retrieval ablation over the frozen corpus and write the report: python -m bench.run [--arms bm25_only] [--limit 200] [--no-live].""" +"""Run the retrieval ablation over the frozen corpus and write the baseline report. + +Five arms, one database: sparse only, dense only, graph only, the product's weighted +fusion, and fusion plus a cross-encoder reranker. Every number in the report is either +measured or explicitly marked as chance. + +Run: python -m bench.run [--arms bm25_only] [--limit 200] [--no-live] [--out bench/reports] +""" from __future__ import annotations