Ask the repository that owns the path, and pin the wirings that had no test

Round four of the independent review found seven record and coverage
defects. The functional side it verified by running; these are the fixes.

- `_tracked` asked the current checkout's index, so a reviewer in a worktree
  pointing at the main checkout's committed report was told it was untracked.
  It now asks the repository that owns the path, and the guard no longer
  compares directories: any tracked file is committed evidence.
- `retrieval.py` claimed the dense index "catches up inside search". It does
  not: `_stale` is only used by `sync()` and `stats()`, so a store upgraded
  from v11 ranks an empty dense leg until `ely-eye build-dense-index` runs.
  The docstring now states that, and `doctor` already names it.
- Four wirings had no discriminating test, each proven by mutation:
  a stubbed `_tracked` (now a test uses the real git index), `copy_store`
  claiming the scratch (now a live holder blocks the copy before a byte is
  copied), `_stale` reading the atoms table (now a closed memory that loses
  its vector is owed one), and the v10/v11 to v12 `DROP VIEW` gate (now a
  hand-made v11 store is upgraded and its view body re-checked).
- The record corrected against the reviewer's measurements: the five
  mutations did not each turn one test red (M1 turned two, M3 turned two,
  `_stale` alone turned none), the pyright/mypy file set was wrong, and the
  round added seven tests rather than three.

Gates: 422 passed; ruff clean; filesize clean; audit clean; mypy 24 errors in
9 files repo-wide, equal to the 9cb46c1 baseline; pyright 0 on the nine
touched files; zero whole-line comment blocks over 157 tracked files.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-10 01:59:43 -04:00
1 parent e81923f62e
commit f8a9428f5a
7 files changed
+99 -19

No files matched your search

+8 -5
View File
@@ -421,7 +421,7 @@
- **评审第 4 轮 FAIL**(同一独立 pi,7 项全跑,~557 工具轮;D40/D41 的修法实测成立,16 对逐字复现、五对 D40 全记上):三条——D42 就是上面 P1 那句被实验推翻(Go 层重发缺失字段,旧服务照样过);D43 `:412` 的机制句**方向写反**,我写「主语之前的裸数字并入主语键」,而代码的改名槽是 `index + 1`(主语 token 的**下一个**,`trust.py:275-276`),实测 `3 room holds 22 degrees` 主语是 `3`、不会向前融合,三个例子里只有 `2026 saw …` 数字在前、走的是 `trust.py:270-272` 首数字分支;D44 `_numbers_follow` 的**漏报侧没披露**:改不改名取决于每条记忆自己后面还留着没有第二个数字,`温度 22 度` + `温度 24 度 湿度 40`(`温度`/`温度24`)、`温度 22 度 湿度 40` + `温度 24 度`、`温度 22 度 湿度 40` + `温度 24 度 湿度 60`(`温度22`/`温度24`)、`预算 5000 元 超了 8000` + `预算 6000 元`(`预算5000`/`预算`)四对全 0 条,方向 fail-closed(两条都留在页上)。D44 按评审建议只**披露 + 加测试**(`test_a_second_number_in_one_memory_hides_the_update`),不用「未改名主语兜底比对」,那会把 `Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 退回 D37 的误报。四条精度说明全部采纳:P-a `degrees` 键也吞年份(`in 2026` → `{'degrees':'2026'}`),P5 那句已改成「第二个数字,含年份」;P-b 反向实验的红名单是 4 条不是 5 条(全套 4 failed / 379 passed,无连带);P-c 「数字只要站在某个词之后就是量不是名字」已就地加限定词(且不在紧挨主语那个位置);P-d 8799 守卫仍只在开跑时探一次,现在指纹补上了**旧构建**答题这一口——同树残留的服务占住端口时指纹仍给 200(评审实测:同树同 `ELY_EYE_AUTH_TOKEN` 的服务把 5 条 atom 落进它自己的 home,直到挂载卡带才失败),那种情形仍只有 busy-port 守卫能挡。该轮同时实测确认:门禁全绿且 mypy 两边逐字相同、tokenize 0 块、最大文件 `bench/systems.py` 498 行;D41 的新测试文案与实测一致;P4 的 stamp 判断站得住(`bench/leg_report.py` 两处 blob 全同 `40596d29…`)。判定:p2-3.1/3.2/3.3 与两条验收都能 `[x]`,p2-3.4 因 D43/D44 的记录级缺陷暂缓。 - **评审第 4 轮 FAIL**(同一独立 pi,7 项全跑,~557 工具轮;D40/D41 的修法实测成立,16 对逐字复现、五对 D40 全记上):三条——D42 就是上面 P1 那句被实验推翻(Go 层重发缺失字段,旧服务照样过);D43 `:412` 的机制句**方向写反**,我写「主语之前的裸数字并入主语键」,而代码的改名槽是 `index + 1`(主语 token 的**下一个**,`trust.py:275-276`),实测 `3 room holds 22 degrees` 主语是 `3`、不会向前融合,三个例子里只有 `2026 saw …` 数字在前、走的是 `trust.py:270-272` 首数字分支;D44 `_numbers_follow` 的**漏报侧没披露**:改不改名取决于每条记忆自己后面还留着没有第二个数字,`温度 22 度` + `温度 24 度 湿度 40`(`温度`/`温度24`)、`温度 22 度 湿度 40` + `温度 24 度`、`温度 22 度 湿度 40` + `温度 24 度 湿度 60`(`温度22`/`温度24`)、`预算 5000 元 超了 8000` + `预算 6000 元`(`预算5000`/`预算`)四对全 0 条,方向 fail-closed(两条都留在页上)。D44 按评审建议只**披露 + 加测试**(`test_a_second_number_in_one_memory_hides_the_update`),不用「未改名主语兜底比对」,那会把 `Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 退回 D37 的误报。四条精度说明全部采纳:P-a `degrees` 键也吞年份(`in 2026` → `{'degrees':'2026'}`),P5 那句已改成「第二个数字,含年份」;P-b 反向实验的红名单是 4 条不是 5 条(全套 4 failed / 379 passed,无连带);P-c 「数字只要站在某个词之后就是量不是名字」已就地加限定词(且不在紧挨主语那个位置);P-d 8799 守卫仍只在开跑时探一次,现在指纹补上了**旧构建**答题这一口——同树残留的服务占住端口时指纹仍给 200(评审实测:同树同 `ELY_EYE_AUTH_TOKEN` 的服务把 5 条 atom 落进它自己的 home,直到挂载卡带才失败),那种情形仍只有 busy-port 守卫能挡。该轮同时实测确认:门禁全绿且 mypy 两边逐字相同、tokenize 0 块、最大文件 `bench/systems.py` 498 行;D41 的新测试文案与实测一致;P4 的 stamp 判断站得住(`bench/leg_report.py` 两处 blob 全同 `40596d29…`)。判定:p2-3.1/3.2/3.3 与两条验收都能 `[x]`,p2-3.4 因 D43/D44 的记录级缺陷暂缓。
- **评审第 5 轮 PASS**(同一独立 pi,7 项全跑,本轮 diff 仅 3 个文件 +31/−5、机制 blob 全同:`trust.py d0b1a090`、`memory.py 13b0ebbf`、`retrieval.py 305d6e3a`、`compiler.py 5d341ca5`、`api_memory.py 22a03666`):D42/D43/D44 全部按实测改掉并被独立复现——D42 的判别子它自己起两边服务量过(`4c6af87` 的 `/api/memory/working` 404、HEAD 200);D43 三个主语实测 `3` / `room3` / `2026`;D44 四对逐对 0 条且贴出了页面(两条都留着),新测试的两条断言它逐条读过(第二条真的检查了「都在页上」)。**「不用未改名主语兜底比对」这个判断它用实验支持了**:把 `quantity_conflict` 改成主语不等时退回比值键,`Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 立刻变回 `('holds','22','24')`,即第 2 轮 D37 的误报复活。反向实验(去掉 `_numbers_follow` 条件)红名单仍是 4 条、全套 4 failed / 379 passed,**新加那条不变红**(无条件改名下它两条断言仍成立)。20 对冲突 + D40 五对 + `Room 3 is big` 边界逐字复现。门禁全部重跑且与本报告逐格一致(384 passed、六文件 83、conflicts 27、三文件 35、mypy 与 `9cb46c1` 文件集合与去行号文本逐字相同、tokenize 148 个 `.py` 0 块、最大文件 `bench/systems.py` 498、go `-count=1` 全套 + gofmt 空、`index-CSDHCFNo.js 221.48 kB`、smoke PASS)。两条精度说明(不算缺陷)已采纳:P-e 把 D44 成因从「一侧改名」改成「改名后主语键各自吃进自己的数字,两侧都改名也永不相等」(我另测 `温度 22 度 湿度 40` + `温度 24 度 湿度 40` → 0 条证实对称情形);P-f 把「指纹补上 8799 那一口」限定为旧构建那一口。判定:p2-3.1/3.2/3.3/3.4 与两条验收全部 `[x]`。 - **评审第 5 轮 PASS**(同一独立 pi,7 项全跑,本轮 diff 仅 3 个文件 +31/−5、机制 blob 全同:`trust.py d0b1a090`、`memory.py 13b0ebbf`、`retrieval.py 305d6e3a`、`compiler.py 5d341ca5`、`api_memory.py 22a03666`):D42/D43/D44 全部按实测改掉并被独立复现——D42 的判别子它自己起两边服务量过(`4c6af87` 的 `/api/memory/working` 404、HEAD 200);D43 三个主语实测 `3` / `room3` / `2026`;D44 四对逐对 0 条且贴出了页面(两条都留着),新测试的两条断言它逐条读过(第二条真的检查了「都在页上」)。**「不用未改名主语兜底比对」这个判断它用实验支持了**:把 `quantity_conflict` 改成主语不等时退回比值键,`Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 立刻变回 `('holds','22','24')`,即第 2 轮 D37 的误报复活。反向实验(去掉 `_numbers_follow` 条件)红名单仍是 4 条、全套 4 failed / 379 passed,**新加那条不变红**(无条件改名下它两条断言仍成立)。20 对冲突 + D40 五对 + `Room 3 is big` 边界逐字复现。门禁全部重跑且与本报告逐格一致(384 passed、六文件 83、conflicts 27、三文件 35、mypy 与 `9cb46c1` 文件集合与去行号文本逐字相同、tokenize 148 个 `.py` 0 块、最大文件 `bench/systems.py` 498、go `-count=1` 全套 + gofmt 空、`index-CSDHCFNo.js 221.48 kB`、smoke PASS)。两条精度说明(不算缺陷)已采纳:P-e 把 D44 成因从「一侧改名」改成「改名后主语键各自吃进自己的数字,两侧都改名也永不相等」(我另测 `温度 22 度 湿度 40` + `温度 24 度 湿度 40` → 0 条证实对称情形);P-f 把「指纹补上 8799 那一口」限定为旧构建那一口。判定:p2-3.1/3.2/3.3/3.4 与两条验收全部 `[x]`。
- 钥匙测试场景 3 暂露的真缺口(终验时补):`v_atoms_current` 只答「现在开着」的记忆,所以被 supersede 的旧事实**无论怎么设 `since`/`until` 都答不出来**——那两个字段框的是 event time,不是「从哪个时刻答题」。补成能力:`SearchFilters.as_of`(`schemas.py`),`store_search.py` 把视图换成 `atoms` + 两分支谓词(不带 as_of 时逐字重复视图条件,带时 `valid_from <= as_of < valid_to`、`event_time <= as_of`、`retention_until > as_of`,且**不看 `archived_at`**,因为归档是存储层不是关于世界的断言),`vector_index.py:_allowed()` 同一套窗口,`retrieval.py:119` 透传,CLI `search --as-of`。 - 钥匙测试场景 3 暂露的真缺口(终验时补):`v_atoms_current` 只答「现在开着」的记忆,所以被 supersede 的旧事实**无论怎么设 `since`/`until` 都答不出来**——那两个字段框的是 event time,不是「从哪个时刻答题」。补成能力:`SearchFilters.as_of`(`schemas.py`),`store_search.py` 把视图换成 `atoms` + 两分支谓词(不带 as_of 时逐字重复视图条件,带时 `valid_from <= as_of < valid_to`、`event_time <= as_of`、`retention_until > as_of`,且**不看 `archived_at`**,因为归档是存储层不是关于世界的断言),`vector_index.py:_allowed()` 同一套窗口,`retrieval.py:119` 透传,CLI `search --as-of`。
- 评审第 2 轮实测把这条能力拆穿了一半,三处已按机制修掉(不是加披露):**D9** 那个名字不副实的 dense 测试根本没开 dense(`config.py:36 dense_embeddings_enabled=False`),它把 `VectorIndex.search` 包一层计数,跑完 `test_as_of_search.py` 与 `test_search_api.py` 记下 **0 次调用**;把 `_allowed` 的 as_of 分支换成现行分支,`test_as_of_search.py` 仍 5 passed、key_test 仍 exit 0;删掉 `retrieval.py:119` 也一样。现在那条测试自己开 dense(假 embedding 模型),既直接调 `index.search(..., as_of=…)`,也调 `retrieval._dense_search(...)` 透传 criteria,两条都钉住。**D10** 真开着 dense 时两条腿不一致:词腿在 `as_of` 下回「钥匙放在玄关抽屉里」,`retrieval._dense_search` 回 **空**——因为 `_current`/`_stale`/`_expected`(`vector_index.py:397`、`:451`、`:474`)都读 `v_atoms_current`,退役的记忆**连向量都没有**,dense 的 as_of 分支只能排除、召不回旧事实。改法:一个还存在的记忆就保留向量(三处改读 `atoms`),谁能看由 `_allowed` 决定;`_expected` 的计数同步,否则 `pending` 会永远不为 0。**D11** 排期到未来的 supersede 不对称:旧行 `valid_to=2099`、新行 `valid_from=2099`,不带 as_of 的搜索回 **0 命中**(现行读法只认 `valid_to IS NULL`,把「明年才失效」的记忆当成已经不真),而 dense 腿回 1 行——两边谓词不一致。改法:「现在开着」在三处统一成 `valid_to IS NULL OR valid_to > now`(视图 `migrations.py:191`、`store_search.py:71`,`vector_index.py:268` 早就是这样),schema 因此从 v11 升到 **v12**,并把 `DROP VIEW` 的闸门从 `version < 10` 放宽到 `version < 12`(`CREATE VIEW IF NOT EXISTS` 会留着旧视图的旧身体,升级过的库必须被提醒重看一遍)。验收:`pytest backend/tests/test_as_of_search.py` → **6 passed**(旧记忆答它当真的那阵、下周的记忆不答今天的问、归档前的时段仍答、as_of 不借别人的记忆、dense 腿与文字腿读同一个窗口、明年才退役的记忆今天仍答)。五个变异各自只红一条测试(这是评审要的判别子):M1 dense as_of 分支拒绝任何退役行 → dense 测试红;M2 删 `retrieval.py:119` → dense 测试红;M3 三处改回读 `v_atoms_current` → dense 测试红;M4 `store_search.py` 现行分支退回 `valid_to IS NULL` → 排期那条红;M5 视图退回 `WHERE valid_to IS NULL` → 排期那条红。变异脚本必须备份它碰过的每个文件,别在树里还有未提交改动时用 `git checkout --` 还原(我这样丢过一次自己的 store_search 修复,症状是「还原后仍红」)。 - 评审第 2 轮实测把这条能力拆穿了一半,三处已按机制修掉(不是加披露):**D9** 那个名字不副实的 dense 测试根本没开 dense(`config.py:36 dense_embeddings_enabled=False`),它把 `VectorIndex.search` 包一层计数,跑完 `test_as_of_search.py` 与 `test_search_api.py` 记下 **0 次调用**;把 `_allowed` 的 as_of 分支换成现行分支,`test_as_of_search.py` 仍 5 passed、key_test 仍 exit 0;删掉 `retrieval.py:119` 也一样。现在那条测试自己开 dense(假 embedding 模型),既直接调 `index.search(..., as_of=…)`,也调 `retrieval._dense_search(...)` 透传 criteria,两条都钉住。**D10** 真开着 dense 时两条腿不一致:词腿在 `as_of` 下回「钥匙放在玄关抽屉里」,`retrieval._dense_search` 回 **空**——因为 `_current`/`_stale`/`_expected`(`vector_index.py:397`、`:451`、`:474`)都读 `v_atoms_current`,退役的记忆**连向量都没有**,dense 的 as_of 分支只能排除、召不回旧事实。改法:一个还存在的记忆就保留向量(三处改读 `atoms`),谁能看由 `_allowed` 决定;`_expected` 的计数同步,否则 `pending` 会永远不为 0。**D11** 排期到未来的 supersede 不对称:旧行 `valid_to=2099`、新行 `valid_from=2099`,不带 as_of 的搜索回 **0 命中**(现行读法只认 `valid_to IS NULL`,把「明年才失效」的记忆当成已经不真),而 dense 腿回 1 行——两边谓词不一致。改法:「现在开着」在三处统一成 `valid_to IS NULL OR valid_to > now`(视图 `migrations.py:191`、`store_search.py:71`,`vector_index.py:268` 早就是这样),schema 因此从 v11 升到 **v12**,并把 `DROP VIEW` 的闸门从 `version < 10` 放宽到 `version < 12`(`CREATE VIEW IF NOT EXISTS` 会留着旧视图的旧身体,升级过的库必须被提醒重看一遍)。验收:`pytest backend/tests/test_as_of_search.py` → **6 passed**(旧记忆答它当真的那阵、下周的记忆不答今天的问、归档前的时段仍答、as_of 不借别人的记忆、dense 腿与文字腿读同一个窗口、明年才退役的记忆今天仍答)。变异实测(评审第 4 轮自己量的,不是我预测的):M1 dense as_of 分支拒绝任何退役行 → **红 2 条**(dense 测试 + `test_a_superseded_memory_keeps_its_vector_but_leaves_the_current_answer_alone`);M2 删 `retrieval.py:119` → 红 1 条;M3 三处一起改回读 `v_atoms_current` → 红 2 条,拆开看 `_current` 红 2、`_expected` 红 1、`_stale` 当时红 **0**;M4 `store_search.py` 现行分支退回 `valid_to IS NULL` → 红 1 条;M5 视图退回 `WHERE valid_to IS NULL` → 红 1 条。`_stale` 那个 0 红的洞本轮补了(见 p2-4 第 4 轮那条)。变异脚本必须备份它碰过的每个文件,别在树里还有未提交改动时用 `git checkout --` 还原(我这样丢过一次自己的 store_search 修复,症状是「还原后仍红」)。
- 顺带:`cli.py` 495→432 行(写路径拆进 `cli_memory.py`),`bench/leg_check.py` 拆出 `bench/leg_report.py`;typer 0.27.2 用**关键字**调用回调,`*atom_ids: str` 形式的命令一调就 `TypeError`,改成 `list[str]`——这才是之前 `test_maintenance` 那条 TypeError 的真因,命令重名只是表象。报告 stamp 早于 harness 的缺陷也修了(`a52c104`)。 - 顺带:`cli.py` 495→432 行(写路径拆进 `cli_memory.py`),`bench/leg_check.py` 拆出 `bench/leg_report.py`;typer 0.27.2 用**关键字**调用回调,`*atom_ids: str` 形式的命令一调就 `TypeError`,改成 `list[str]`——这才是之前 `test_maintenance` 那条 TypeError 的真因,命令重名只是表象。报告 stamp 早于 harness 的缺陷也修了(`a52c104`)。
- [x] **p2-4** 共享记忆 - [x] **p2-4** 共享记忆
- [x] p2-4.1 多用户/多 agent 作用域 + ACL(schema v11:holder token + 三态眼睛) - [x] p2-4.1 多用户/多 agent 作用域 + ACL(schema v11:holder token + 三态眼睛)
@@ -442,10 +442,13 @@
- **评审第 2 轮 FAIL**(同一独立 pi,`df812f3`,7 项全跑):D1–D8 全部实测确认已修。as_of 的行为面它自己写脚本测通:退役的记忆在 `as_of` 下应答、下周的事今天不应答、归档不挡 as_of、不跨 holder 借看。`scripts/key_test.sh` 在 `df812f3` 上 exit 0(6 PASS + 1 NOT MEASURED),把 `store_search.py` 换回父提交就红在场景 3。新缺陷六条,三条行为三条记录:**D9** 那条 dense 测试名字不副实、as_of 的向量侧零覆盖(包一层计数实测 0 次调用;改 `_allowed` 的 as_of 分支、删 `retrieval.py:119` 都全绿);**D10** 真开 dense 时两条腿不一致,词腿回旧事实、dense 腿回空,因为退役记忆根本没向量(`_expected`/追赶都读 `v_atoms_current`),commit message 里「cannot disagree」写过头;**D11** 排期到未来的 supersede 不披露且不对称:不带 as_of 的搜索 0 命中、`GET /api/atoms` total 0、dense 腿 1 行;**D12** `:430` 那个旧行号括号仍是错的;**D13** p2-5 验收 grep 实测 **2 命中**不是 0(它扫到了 CHECKLIST 自己引用的被删字符串);**D14** `:455` 写「10 项闭合、2 项部分闭合」与 Gaps §0.5 自己那张表对不上(实为 8 / 3 / 1)。六条已全部按机制修完,见上面 p2-3 那条 as_of 与下面本轮门禁格。**读错评审答案的教训(真实代价)**:我第一次读的是它 session JSONL 里最后一个文本块的 PASS 表格,据此拆出一份错的 D-list(compare_digest / 历史行没钉提交 / 401 vs 406 / 缓存 key 后果 / video harness 写死报告),`cf2de31` 就是按那份错的清单修的——那些修法本身都成立(它们是评审的精度说明与上一轮残留),但不是本轮的 D9–D14,而且第 3 轮 prompt 已按错清单发出去了(作废重发)。以后读 verdict:先按 index 数文本块、只读它自己的「缺陷清单」那一节,不读 PASS 表。另采纳它的四条精度说明(不算缺陷):本轮范围是 3 个 commit 不是 2;naive `--as-of` 被接受(与 `--since` 同策略,awareness 校验只在 `schemas.py:128-145` 的 `EvidenceAtom` 上,`SearchFilters` 没有);smoke 的端口守卫只在启动前查(它曾拿到一个正在死的 server:「FAIL: the session did not reload in the order it was recorded: []」);`browse.py` 那条子句只被 pytest 卡住(删掉它 key_test 与 smoke 都绿,因为场景 5 测的是隔离不是 ACL)。 - **评审第 2 轮 FAIL**(同一独立 pi,`df812f3`,7 项全跑):D1–D8 全部实测确认已修。as_of 的行为面它自己写脚本测通:退役的记忆在 `as_of` 下应答、下周的事今天不应答、归档不挡 as_of、不跨 holder 借看。`scripts/key_test.sh` 在 `df812f3` 上 exit 0(6 PASS + 1 NOT MEASURED),把 `store_search.py` 换回父提交就红在场景 3。新缺陷六条,三条行为三条记录:**D9** 那条 dense 测试名字不副实、as_of 的向量侧零覆盖(包一层计数实测 0 次调用;改 `_allowed` 的 as_of 分支、删 `retrieval.py:119` 都全绿);**D10** 真开 dense 时两条腿不一致,词腿回旧事实、dense 腿回空,因为退役记忆根本没向量(`_expected`/追赶都读 `v_atoms_current`),commit message 里「cannot disagree」写过头;**D11** 排期到未来的 supersede 不披露且不对称:不带 as_of 的搜索 0 命中、`GET /api/atoms` total 0、dense 腿 1 行;**D12** `:430` 那个旧行号括号仍是错的;**D13** p2-5 验收 grep 实测 **2 命中**不是 0(它扫到了 CHECKLIST 自己引用的被删字符串);**D14** `:455` 写「10 项闭合、2 项部分闭合」与 Gaps §0.5 自己那张表对不上(实为 8 / 3 / 1)。六条已全部按机制修完,见上面 p2-3 那条 as_of 与下面本轮门禁格。**读错评审答案的教训(真实代价)**:我第一次读的是它 session JSONL 里最后一个文本块的 PASS 表格,据此拆出一份错的 D-list(compare_digest / 历史行没钉提交 / 401 vs 406 / 缓存 key 后果 / video harness 写死报告),`cf2de31` 就是按那份错的清单修的——那些修法本身都成立(它们是评审的精度说明与上一轮残留),但不是本轮的 D9–D14,而且第 3 轮 prompt 已按错清单发出去了(作废重发)。以后读 verdict:先按 index 数文本块、只读它自己的「缺陷清单」那一节,不读 PASS 表。另采纳它的四条精度说明(不算缺陷):本轮范围是 3 个 commit 不是 2;naive `--as-of` 被接受(与 `--since` 同策略,awareness 校验只在 `schemas.py:128-145` 的 `EvidenceAtom` 上,`SearchFilters` 没有);smoke 的端口守卫只在启动前查(它曾拿到一个正在死的 server:「FAIL: the session did not reload in the order it was recorded: []」);`browse.py` 那条子句只被 pytest 卡住(删掉它 key_test 与 smoke 都绿,因为场景 5 测的是隔离不是 ACL)。
- **第 3 轮作废重发**:`cf2de31` 那一轮是按我读错的 D-list 发的(见上面那条教训),它验的仍是记录面那五条;本轮真正的 D9–D14 已在 `eab6bd7` 之后修完,重发的 prompt 把三条行为缺陷的复现方式写死(dense 腿零覆盖的包层计数、`_expected` 的向量计数、排期 supersede 的三种读法)。 - **第 3 轮作废重发**:`cf2de31` 那一轮是按我读错的 D-list 发的(见上面那条教训),它验的仍是记录面那五条;本轮真正的 D9–D14 已在 `eab6bd7` 之后修完,重发的 prompt 把三条行为缺陷的复现方式写死(dense 腿零覆盖的包层计数、`_expected` 的向量计数、排期 supersede 的三种读法)。
- **评审第 3 轮 FAIL**(`cf2de31`,D15–D22,全部实测):D15 = 我写「dense 腿也验了」时它其实是零覆盖——`test_as_of_search.py:105` 没开 `dense_embeddings_enabled`、没建 `VectorIndex`,在 `VectorIndex.search` 里加 `print("[CALL]")` 对该文件 **0 次**(`test_vector_index.py` 17 次),真开 dense 后 words 腿 `as_of=上周+1` 答抽屉、`index.search(as_of=…)` 返空,`expected_vectors` 只算出 1、`vec_atoms` 只有一行;把 `_allowed` 里 6 个 `as_of` 参数全绑成 `None` 仍 `5 passed`。⇒ 第 2 轮的 D9/D10 既没修也没记,却被写成已验。**已由 `92237c8` 修**(D9 测试真开 dense 并断言两条腿读同一窗口、D10 向量改读 `atoms`、D11 币种统一 + schema v12,五条变异各打红一条)。D16/D17 = 我上一行把功劳写错了对象:「在 `209f2d0` 重现旧 key 无身份成分」是**第 3 轮**做的,且实测比这句话多一层——`209f2d0:compiler.py:43` 根本不传 filters,那里没有「陌生人那份」可串,`compiler.py:33 if compiled.hits:` 说明空 pack 从不被端出;`as_holder or ""` 这个串**代码里从来没有**,只在 CHECKLIST 里出现过(`git log -S` 列出 `83dfc14`/`df812f3`/`cf2de31` 三个提交,都是改 CHECKLIST 的)。D18 = D-list 被换掉(已由 `92237c8` 恢复)。D19 = 「当前 411」是我自己工作树的数,被审的 `cf2de31` 是 410。D20 = 我那句「视频 harness 会拒写冻结报告」三处不实:`VIDEO_AUDIO.md`/`video_audio.json` 当时**未入库**(worktree 里根本没有可弄脏的冻结件)、worktree 跑不到写那步(死在 `no store to copy` / `123 store sources are not corpus files`)、守卫按 cwd 相对比较所以**绝对路径指向冻结件本身也能过**;且该 harness 是 `cf2de31` 自己引入的,「已修」修的是同一提交刚造的东西。D21 = `--limit 1 --probes 1`(含 `--probes 5`)死在 `aggregate needs at least one answerable query`、exit 1、零产物——冒烟旋钮在小数值时不是「少测」而是崩。D22 = 第 2 轮的 D13/D14 未动(已由 `92237c8` 修)。 - **评审第 3 轮 FAIL**(`cf2de31`,D15–D22,全部实测):D15 = 我写「dense 腿也验了」时它其实是零覆盖——`test_as_of_search.py:105` 没开 `dense_embeddings_enabled`、没建 `VectorIndex`,在 `VectorIndex.search` 里加 `print("[CALL]")` 对该文件 **0 次**(`test_vector_index.py` 17 次),真开 dense 后 words 腿 `as_of=上周+1` 答抽屉、`index.search(as_of=…)` 返空,`expected_vectors` 只算出 1、`vec_atoms` 只有一行;把 `_allowed` 里 6 个 `as_of` 参数全绑成 `None` 仍 `5 passed`。⇒ 第 2 轮的 D9/D10 既没修也没记,却被写成已验。**已由 `92237c8` 修**(D9 测试真开 dense 并断言两条腿读同一窗口、D10 向量改读 `atoms`、D11 币种统一 + schema v12,五条变异各打红一条)。D16/D17 = 我上一行把功劳写错了对象:「在 `209f2d0` 重现旧 key 无身份成分」是**第 3 轮**做的,且实测比这句话多一层——`209f2d0:compiler.py:43` 根本不传 filters,那里没有「陌生人那份」可串,`compiler.py:33 if compiled.hits:` 说明空 pack 从不被端出;`as_holder or ""` 这个串**代码里从来没有**,只在 CHECKLIST 里出现过(`git log -S` 列出 `83dfc14`/`df812f3`/`cf2de31` 三个提交,都是改 CHECKLIST 的)。D18 = D-list 被换掉(已由 `92237c8` 恢复)。D19 = 「当前 411」是我自己工作树的数,被审的 `cf2de31` 是 410。D20 = 我那句「视频 harness 会拒写冻结报告」三处不实:`VIDEO_AUDIO.md`/`video_audio.json` 当时**未入库**(worktree 里根本没有可弄脏的冻结件)、worktree 跑不到写那步(死在 `no store to copy` / `123 store sources are not corpus files`)、守卫按 cwd 相对比较所以**绝对路径指向冻结件本身也能过**;且该 harness 是 `cf2de31` 自己引入的,「已修」修的是同一提交刚造的东西。D21 = `--limit 1 --probes 1`(含 `--probes 5`)死在 `aggregate needs at least one answerable query`、exit 1、零产物——冒烟旋钮在小数值时不是「少测」而是崩。D22 = 第 2 轮的 D13/D14 未动(已由 `92237c8` 修)。
- 本轮(D15–D22 收尾)harness 三修:`refuse_frozen_overwrite` 改成比较 `resolve()` 后的目录(绝对路径指向冻结件也拒,D20);新增 `probe_sample(probes, limit)`,`--probes 0` 关掉该臂;当 `--limit` 把样本收到只剩视频时小 count 会整块跳过该模态(评审量的 `--limit 1 --probes 1` 与 `--probes 5` 就是),此时在**二十分钟 ingest 之前**就 `SystemExit` 点名「picked no answerable video query…Pass --probes 0 to skip the probe arm, or the shipped 300」(D21:分层样本按全库跨步,小 count 会整块跳过本模态);`rank_arm` 空样本返 `None`,`_required` 保证视频臂不会静默变成一行零,`_score_of`/markdown 打印 `skipped`。新增 3 条测试(绝对路径守卫、`probe_sample` 两态、`_required`/`_score_of`)。 - 本轮(D15–D22 收尾)harness 三修:`refuse_frozen_overwrite` 改成比较 `resolve()` 后的目录(绝对路径指向冻结件也拒,D20);新增 `probe_sample(probes, limit)`,`--probes 0` 关掉该臂;当 `--limit` 把样本收到只剩视频时小 count 会整块跳过该模态(评审量的 `--limit 1 --probes 1` 与 `--probes 5` 就是),此时在**二十分钟 ingest 之前**就 `SystemExit` 点名「picked no answerable video query…Pass --probes 0 to skip the probe arm, or the shipped 300」(D21:分层样本按全库跨步,小 count 会整块跳过本模态);`rank_arm` 空样本返 `None`,`_required` 保证视频臂不会静默变成一行零,`_score_of`/markdown 打印 `skipped`。本轮新增 7 条测试(评审第 4 轮 D25 纠正了我写的「3 条」):`bench/tests/test_video_audio_check.py` 4 → 8(绝对路径守卫、`probe_sample` 两态、`_required`/`_score_of`、size cap 等),新建 `bench/tests/test_scratch.py` 3 条(`HELD` 认领:活人拒、死人接管、同进程可重复),套件 411 → 418。
- 本轮另修的两处产品事实(都是 D10/D11 带出来的):一个还存在的记忆就保留它的向量(`vector_index.py:397`/`:451`/`:474` 改读 `atoms`),谁能看由 `_allowed` 决定;「现在开着」的谓词在视图与两条搜索腿里统一成 `valid_to IS NULL OR valid_to > now`,schema 以 **v12** 记录这次语义改变,升级时 `DROP VIEW`(`CREATE VIEW IF NOT EXISTS` 不会换身体)。`db.close_atoms` 的 `WHERE valid_to IS NULL` 不是现行读法而是写闸门(别去闭一条已经闭上的),故意保留。 - 本轮另修的两处产品事实(都是 D10/D11 带出来的):一个还存在的记忆就保留它的向量(`vector_index.py:397`/`:451`/`:474` 改读 `atoms`),谁能看由 `_allowed` 决定;「现在开着」的谓词在视图与两条搜索腿里统一成 `valid_to IS NULL OR valid_to > now`,schema 以 **v12** 记录这次语义改变,升级时 `DROP VIEW`(`CREATE VIEW IF NOT EXISTS` 不会换身体)。`db.close_atoms` 的 `WHERE valid_to IS NULL` 不是现行读法而是写闸门(别去闭一条已经闭上的),故意保留。`retrieval.py:107` 原先写「索引会在 `search` 里追上」是**错的**(评审第 4 轮 P-b):`_stale` 只被 `sync()`(`:104`) 与 `stats()`(`:348`) 用,`search` 里没有追赶;后果是 v11 的库升到 v12 后 `pending=2`、dense 腿两种读法都返空,必须跑 `ely-eye build-dense-index`。本轮把那句 docstring 改成实测行为(写的时候索引;升级过的库是例外,`ely-eye doctor` 会点名 `the dense index holds X of the Y memories with words; run \`ely-eye build-dense-index\``,实测谓词在 `senses.py:44` + `maintenance.py:_check_indexes`)。
- 本轮门禁(D9–D14 修完,数字钉在本轮的 commit 上):`pytest -q` **411 passed**(+1 排期 supersede 测试;`test_superseding_moves_the_vector_rather_than_keeping_two` 改成新契约 `test_a_superseded_memory_keeps_its_vector_but_leaves_the_current_answer_alone`:vectors **2**、现行搜索只回替换者、`as_of` 下回旧者);`ruff check backend/src/ely_eye backend/tests bench` 干净;`bench/filesize_check` 干净;`bench.audit --product` → `audited 33 harness files, 2 reports, 6 set files` / `clean`;`./node_modules/.bin/pyright` 对 6 个改动文件 → **0 errors**;mypy 冷缓存对同 6 个文件 `Success: no issues found`;五个变异 M1–M5 各自只红一条测试(见 p2-3 那条 as_of)。 - 本轮门禁(D9–D14 修完,数字钉在本轮的 commit 上):`pytest -q` **411 passed**(+1 排期 supersede 测试;`test_superseding_moves_the_vector_rather_than_keeping_two` 改成新契约 `test_a_superseded_memory_keeps_its_vector_but_leaves_the_current_answer_alone`:vectors **2**、现行搜索只回替换者、`as_of` 下回旧者);`ruff check backend/src/ely_eye backend/tests bench` 干净;`bench/filesize_check` 干净;`bench.audit --product` → `audited 33 harness files, 2 reports, 6 set files` / `clean`;`./node_modules/.bin/pyright` 对 `92237c8` 改动的 **5 个 `.py`**(该提交共改 7 个文件,评审第 4 轮 D24 纠正了我写的「6 个改动文件」)→ **0 errors**;mypy 冷缓存对同 5 个文件 `Success: no issues found`;变异 M1–M5 的红条数见上面 p2-3 那条(不是「各红一条」)。
- 门禁(`83dfc14` 那轮量的 401;`eab6bd7` 加 5 条 as_of 测试后 406,`cf2de31` 加 4 条视频 harness 测试后 **410**(评审第 3 轮 D19:我原先写「当前 411」是我工作树里的数,被审 commit 是 410),`92237c8` 加 1 条排期 supersede 测试后 **411**):`pytest -q` **401 passed**;`ruff check` 对 22 个改动文件干净;`bench/filesize_check` 干净;`bench.audit --product` → `audited 32 harness files, 2 reports, 6 set files` / `clean`;mypy 冷缓存 **24 errors in 9 files (checked 112 source files)**,与 `9cb46c1` 基线(24/9)**净新增 0**;`./node_modules/.bin/pyright` 对 17 个后端源文件 + 5 个测试文件 → **0 errors**;`go build` / `go vet` / `go test`(apiclient ok、mcpserver ok)+ `gofmt -l` 空;`npm run build` → `dist/assets/index-CSDHCFNo.js 221.48 kB`(bundle 哈希与 `246dae9` 相同,与「本轮没动前端」一致)+ eslint exit 0;`bash scripts/mcp_write_smoke.sh` 3.3s 两行 PASS。 - 门禁(`83dfc14` 那轮量的 401;`eab6bd7` 加 5 条 as_of 测试后 406,`cf2de31` 加 4 条视频 harness 测试后 **410**(评审第 3 轮 D19:我原先写「当前 411」是我工作树里的数,被审 commit 是 410),`92237c8` 加 1 条排期 supersede 测试后 **411**):`pytest -q` **401 passed**;`ruff check` 对 22 个改动文件干净;`bench/filesize_check` 干净;`bench.audit --product` → `audited 32 harness files, 2 reports, 6 set files` / `clean`;mypy 冷缓存 **24 errors in 9 files (checked 112 source files)**,与 `9cb46c1` 基线(24/9)**净新增 0**;`./node_modules/.bin/pyright` 对 17 个后端源文件 + 5 个测试文件 → **0 errors**;`go build` / `go vet` / `go test`(apiclient ok、mcpserver ok)+ `gofmt -l` 空;`npm run build` → `dist/assets/index-CSDHCFNo.js 221.48 kB`(bundle 哈希与 `246dae9` 相同,与「本轮没动前端」一致)+ eslint exit 0;`bash scripts/mcp_write_smoke.sh` 3.3s 两行 PASS。
- **评审第 4 轮 FAIL**(同一独立 pi,8 项全跑,diff `cf2de31..efbca0b`):功能面它自己复验通过——门禁全绿(`pytest -q` 418、mypy 冷缓存 24/9 与 `9cb46c1` worktree 逐条同消息 ⇒ 净 0、pyright 0、go 全套 + `gofmt -l` 空、`npm run build` 221.48 kB + eslint 0);`VectorIndex.search` 插桩在 `test_as_of_search.py` 打出 6 次 `as_of`(第 3 轮是 0 次);排期 supersede 两腿同向;认领机制四条对照全测通;D9/D11/D17/D18/D19/D21/D22 判为完全修好。七条新缺陷全在记录与判别子上:**D23** 「五个变异各红一条」不实(M1 红 2、M3 三处一起红 2、`_stale` 单独改红 0);**D24** pyright/mypy 的文件集写错(`92237c8` 改 7 个文件、其中 5 个 `.py`);**D25** 「新增 3 条测试」少算 4 条;**D26** 两处接线没有判别测试(`_tracked` 恒假 → 418 全绿,两条守卫测试都 monkeypatch 它;删掉 `copy_store` 里的 `claim(scratch)` → 418 全绿,scratch 测试只直接调 `claim()`);**D27** 本轮自己在 `bench/tests/test_video_audio_check.py:53-54` 造了一处整行 `#` 两行块,而记录说全仓 0;**D28** v10/v11→v12 的 `DROP VIEW` 闸门无测试(`version < 12` 改成 `version < 10` 全绿,手工升级的库里视图 0 行、`browse` total=0 而 search 有 1);**D29** 「绝对路径也拒」只在同一 checkout 成立(worktree 里指主仓的冻结件被放行)。四条精度说明:P-b 见上面 `retrieval.py` 那条;P-c 提交信息「Six harnesses share that copy path」不准,六个默认 scratch 各不相同、共用的是代码路径不是目录;P-e `_alive` 对垃圾内容 fail-open(`not-a-pid` → 接管,无 `/proc` 同样放行,docstring 自陈);P-g 「整行」口径已在 p1-6.4 写明,允许行尾注释的算法在基线上就有 2 块。
- 本轮按机制修,不按披露修:`_tracked` 改问**拥有那个路径的仓库**(`git -C <目录> ls-files --error-unmatch <文件>`),守卫去掉目录比较——**任何被 git 跟踪的文件都是冻结证据**,worktree 指主仓的 `LEGS.md` 也拒(D29);补四条判别测试:`test_a_committed_report_is_frozen_without_any_stub`(不 stub `_tracked`,用真 git 索引)、`test_copy_store_claims_the_copy_it_makes`(活人占着 scratch 时 `copy_store` 拒,且断言 `data/` 还没被创建 ⇒ 复制一个字节之前就拒)、`test_a_closed_memory_owes_the_index_a_vector_even_though_it_answers_nothing_now`(`_stale` 改读视图 → 正好它红)、`test_an_eleventh_version_database_learns_that_a_future_retirement_is_still_current`(先断言 v11 视图体下确实看不见,再升级断言看得见)。本轮四个新变异各红一条:M5 `version < 12`→`version < 10` → 只红 v11 升级那条;M6 `_stale` 改读 `v_atoms_current` → 只红那条向量测试;M7 `copy_store` 去掉 `claim()` → 只红认领那条;M8 `_tracked` 恒假 → 只红不 stub 那条。
- 本轮门禁(钉在本轮 commit):`pytest -q` **422 passed**;`ruff check backend/src/ely_eye backend/tests bench` 干净;`bench.filesize_check` 干净;`bench.audit --product` → `audited 33 harness files, 2 reports, 6 set files` / `clean`;mypy 冷缓存对 9 个触及文件 `Success: no issues found`,全仓 **24 errors / 9 files**(= `9cb46c1` 基线,净 0);`./node_modules/.bin/pyright` 对同 9 个文件 **0 errors**;全仓整行 `#` 连续 ≥2 的块 **0**(157 个 tracked `.py`,本轮把那处收回一行,D27)。本轮未动 Go 与前端,沿用 `efbca0b` 那轮量的 go 全套 + `gofmt -l` 空 与 `npm run build` 221.48 kB + eslint 0。
- [x] **p2-5** 文档单一真相 - [x] **p2-5** 文档单一真相
- [x] p2-5.1 `AGENTS.md` 重写并停止 gitignore - [x] p2-5.1 `AGENTS.md` 重写并停止 gitignore
- `.gitignore` 删了 `AGENTS.md` 那一行(`CLAUDE.md` 仍忽略,它不是本仓库的真相源),`git check-ignore AGENTS.md` 现在 exit 1。新文件 5.5KB,写的不是风格偏好而是**这个仓库付过学费的约束**:门禁命令清单(含 mypy 基线是 24 errors in 9 files、必须在分支点的 worktree 里量而不是猜);一个 writer 一条规则(Go 不写 SQLite);fail fast 必须点名(引真实报错 `nothing to forget: no atom or source was given`);≤500 行、无死代码、注释单行、docstring 是契约;「缺席是一种状态」——`v_atoms_current` 隐藏而不删除。 - `.gitignore` 删了 `AGENTS.md` 那一行(`CLAUDE.md` 仍忽略,它不是本仓库的真相源),`git check-ignore AGENTS.md` 现在 exit 1。新文件 5.5KB,写的不是风格偏好而是**这个仓库付过学费的约束**:门禁命令清单(含 mypy 基线是 24 errors in 9 files、必须在分支点的 worktree 里量而不是猜);一个 writer 一条规则(Go 不写 SQLite);fail fast 必须点名(引真实报错 `nothing to forget: no atom or source was given`);≤500 行、无死代码、注释单行、docstring 是契约;「缺席是一种状态」——`v_atoms_current` 隐藏而不删除。
@@ -472,7 +475,7 @@
- 为什么要有这一条:验收线 `recall@10 ≥ 0.90`(m00217 定的)现在 0.863,而缺口不是均匀的。按全库可答题数加权(code 1237/2928=0.4225、pdf 766=0.2616、ui_screenshot 320=0.1093、image 240=0.0820、video 215=0.0734、conversation 150=0.0512)用当前每模态实测重算:0.945×0.4225 + 0.885×0.2616 + 0.818×0.1093 + 0.720×0.0820 + 0.493×0.0734 + 0.965×0.0512 = **0.865**,距 0.90 差 **0.035**。 - 为什么要有这一条:验收线 `recall@10 ≥ 0.90`(m00217 定的)现在 0.863,而缺口不是均匀的。按全库可答题数加权(code 1237/2928=0.4225、pdf 766=0.2616、ui_screenshot 320=0.1093、image 240=0.0820、video 215=0.0734、conversation 150=0.0512)用当前每模态实测重算:0.945×0.4225 + 0.885×0.2616 + 0.818×0.1093 + 0.720×0.0820 + 0.493×0.0734 + 0.965×0.0512 = **0.865**,距 0.90 差 **0.035**。
- 谁能补上这 0.035:ui_screenshot 0.818→0.95 只值 +0.0144,video 0.493→0.75(即音频就抬 +0.256)值 +0.0189,image 0.720→0.85 值 +0.0107——**三条加起来 +0.044 才过线,任何单条都不够**;code/pdf/conversation 已经没多少可抬。所以这不是“再调一下排序”能解决的,是图片与视频的内容表示问题。 - 谁能补上这 0.035:ui_screenshot 0.818→0.95 只值 +0.0144,video 0.493→0.75(即音频就抬 +0.256)值 +0.0189,image 0.720→0.85 值 +0.0107——**三条加起来 +0.044 才过线,任何单条都不够**;code/pdf/conversation 已经没多少可抬。所以这不是“再调一下排序”能解决的,是图片与视频的内容表示问题。
- [ ] p2-6.1 音频进视频:`bench/video_audio_check` 在 129 个视频上重录一遍(同库副本、同题、同 words-only 路径,只差音频),报告 `bench/reports/VIDEO_AUDIO.md`。验收 = 两行对照 + 当时的机器负载 + 音频 atom 数;跑前已量到 before 行复现 LEGS.md 的 shipped 0.863(同一产品路径的交叉校验)。 - [ ] p2-6.1 音频进视频:`bench/video_audio_check` 在 129 个视频上重录一遍(同库副本、同题、同 words-only 路径,只差音频),报告 `bench/reports/VIDEO_AUDIO.md`。验收 = 两行对照 + 当时的机器负载 + 音频 atom 数;跑前已量到 before 行复现 LEGS.md 的 shipped 0.863(同一产品路径的交叉校验)。
- [ ] p2-6.2 给截图一个局部的图片腿:整屏的 CLIP 向量不含「搜索图标」这种局部信息(VISUAL.md 实测 picture 腿把 ui_screenshot 从 0.583 拉到 0.528,因为它把整屏编成一个向量)。试法:一张屏切成重叠小块,**每块一个 atom**(`layout` 带 bbox,source 不变所以 source 级 recall 可比),CLIP 只编块。验收 = `bench/visual_recall.py` 加一臂,与现有 arms 并排(image n=240、ui_screenshot n=320);不涨就删掉该臂并把“不涨”写进 VISUAL.md,不留在默认路径里。 - [x] p2-6.2 给截图一个局部的图片腿:**假设被实测推翻,默认路径不动**。池实验(`/tmp/region_clip.py`,日志 `bench/runs/region_clip.log`):275 张 gold 截图、320 条查询、chance@10 = 0.036,三臂只比排名——屏上文字(空白分词的 BM25,比现行 CJK 词腿弱)0.584(text 135/169 = 0.799、icon 52/151 = 0.344);整屏 CLIP 0.269(text 0.272、icon 0.265);OCR 框裁块 CLIP(每屏取 ~36 块的最高分)0.475(text 0.663、icon 40/151 = 0.265)。读法:裁块**确实**好过整屏(0.475 vs 0.269,VISUAL.md 那条「picture 腿把 ui_screenshot 从 0.583 拉到 0.528」的整屏结论没有被推翻),但两条图片腿都打不过屏上的文字,而在真正需要图片的 icon 半边,裁块与整屏同为 0.265、文字反而最高(0.344)——**裁块没有补上 icon 的缺口**,所以不加 `bench/visual_recall.py` 的新臂、不进默认路径,按 p2-6.2 自己写的「不涨就删掉该臂并把不涨写进去」处理。口径交代:该实验在 275 张池内排名,不是 5 万源的现行排序;文字臂是空白分词,故它是被低估的基线,图片臂输给的是一个更弱的对手。
- [ ] p2-6.3 若仍不达 0.90:把「不达」与原因写进 README/Gaps(已有的 VLM 区域描述、widget tree 两条外部能力,本轮按 m00217 未下 8B VLM),不用改验收线口径硬凑。 - [ ] p2-6.3 若仍不达 0.90:把「不达」与原因写进 README/Gaps(已有的 VLM 区域描述、widget tree 两条外部能力,本轮按 m00217 未下 8B VLM),不用改验收线口径硬凑。
- [ ] **final-accept** Gaps.md §6「钥匙测试」7 条场景逐条跑通 - [ ] **final-accept** Gaps.md §6「钥匙测试」7 条场景逐条跑通
- 命令:`bash scripts/key_test.sh`(自建临时 home、假模型快照、起 API、Go MCP 走 stdio、CLI 挂卡带;一张表,任何 FAIL 退出码非 0)。实测:**6 PASS + 1 NOT MEASURED**,exit 0。 - 命令:`bash scripts/key_test.sh`(自建临时 home、假模型快照、起 API、Go MCP 走 stdio、CLI 挂卡带;一张表,任何 FAIL 退出码非 0)。实测:**6 PASS + 1 NOT MEASURED**,exit 0。
+3 -1
View File
@@ -104,7 +104,9 @@ class RetrievalService:
Every filter is applied inside the engine, in the same statement as the nearest-neighbour Every filter is applied inside the engine, in the same statement as the nearest-neighbour
walk, so a scoped search cannot return a memory the caller was never allowed to see. The walk, so a scoped search cannot return a memory the caller was never allowed to see. The
index catches up inside `search`, so nothing here has to rebuild it first. writer indexes a memory in the call that saves it, so a search never waits for a rebuild;
a store upgraded from an older schema is the exception, and `ely-eye doctor` names it until
`ely-eye build-dense-index` runs.
""" """
query_vector = self.embeddings.embed_query(query) query_vector = self.embeddings.embed_query(query)
return self.vectors.search( return self.vectors.search(
+31
View File
@@ -311,3 +311,34 @@ def test_a_ninth_version_database_gains_the_tiers_in_place(tmp_path: Path) -> No
assert hidden == 0, ( assert hidden == 0, (
"the old view body survived the upgrade, so an archived memory would still answer" "the old view body survived the upgrade, so an archived memory would still answer"
) )
def test_an_eleventh_version_database_learns_that_a_future_retirement_is_still_current(tmp_path: Path) -> None:
"""v11's view hid a memory whose retirement is scheduled for next year; the upgrade must rewrite the body."""
clock = datetime.now(UTC)
database = _database(tmp_path)
_seed_source(database)
database.upsert_atoms([_atom("key", ingested_at=clock, valid_from=clock, valid_to=clock + timedelta(days=365))])
with database.connect() as connection:
# The body v11 shipped, written out here so the upgrade is judged against the thing it replaced.
connection.executescript(
"""
DROP VIEW IF EXISTS v_atoms_current;
CREATE VIEW v_atoms_current AS
SELECT * FROM atoms
WHERE valid_to IS NULL
AND (valid_from IS NULL OR valid_from <= strftime('%Y-%m-%dT%H:%M:%f+00:00','now'))
AND (retention_until IS NULL OR retention_until >= strftime('%Y-%m-%dT%H:%M:%f+00:00','now'));
PRAGMA user_version = 11;
"""
)
hidden = connection.execute("SELECT count(*) FROM v_atoms_current").fetchone()[0]
assert hidden == 0, "the fixture must really look like a v11 store before it is upgraded"
upgraded = _database(tmp_path)
with upgraded.connect() as connection:
assert int(connection.execute("PRAGMA user_version").fetchone()[0]) == SCHEMA_VERSION
visible = [str(row[0]) for row in connection.execute("SELECT atom_id FROM v_atoms_current")]
assert visible == ["key"], (
"the old view body survived, so a memory retiring next year would answer nothing today"
)
+16
View File
@@ -244,6 +244,22 @@ def test_a_superseded_memory_keeps_its_vector_but_leaves_the_current_answer_alon
assert list(then) == [old.atom_id], "asked before the retirement, the old memory is still findable" assert list(then) == [old.atom_id], "asked before the retirement, the old memory is still findable"
def test_a_closed_memory_owes_the_index_a_vector_even_though_it_answers_nothing_now(
tmp_path: Path, fake_embeddings: Callable[[], Any]
) -> None:
"""`sync` walks the atoms table, not the current view, or an as-of question would find nothing."""
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
old = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
memory.supersede(old.atom_id, text=PASSPORT, holder_id="wife")
index.drop_atoms([old.atom_id])
assert index.stats()["pending"] == 1, "the closed memory answers nothing now, but the index still owes it a vector"
assert index.sync()["encoded"] == 1, "sync re-encodes the closed memory rather than skipping it"
assert index.stats()["vectors"] == 2
def test_a_store_that_never_built_its_index_says_so_instead_of_stalling_a_search( def test_a_store_that_never_built_its_index_says_so_instead_of_stalling_a_search(
tmp_path: Path, fake_embeddings: Callable[[], Any] tmp_path: Path, fake_embeddings: Callable[[], Any]
) -> None: ) -> None:
+15
View File
@@ -29,3 +29,18 @@ def test_one_run_may_copy_more_than_once(tmp_path: Path) -> None:
scratch.claim(tmp_path) scratch.claim(tmp_path)
scratch.claim(tmp_path) scratch.claim(tmp_path)
assert (tmp_path / scratch.HELD).read_text(encoding="utf-8") == str(os.getpid()) assert (tmp_path / scratch.HELD).read_text(encoding="utf-8") == str(os.getpid())
def test_copy_store_claims_the_copy_it_makes(tmp_path: Path) -> None:
"""The claim lives inside `copy_store`, so a harness cannot step on a live run by forgetting to call `claim`."""
if not Path("/proc/1").exists():
pytest.skip("liveness is read from /proc")
source = tmp_path / "source"
(source / "data").mkdir(parents=True)
(source / "data" / "ely_eye.sqlite").write_text("store", encoding="utf-8")
busy = tmp_path / "scratch"
busy.mkdir()
(busy / scratch.HELD).write_text("1", encoding="utf-8")
with pytest.raises(SystemExit, match="is held by pid 1"):
scratch.copy_store(source, busy)
assert not (busy / "data").exists(), "the refusal must come before a byte is copied"
+16 -8
View File
@@ -11,8 +11,7 @@ from bench import video_audio_check
def test_a_frozen_report_is_not_overwritten_by_a_harness( def test_a_frozen_report_is_not_overwritten_by_a_harness(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""A reviewer runs harnesses in a worktree of its own; the committed evidence stays put.""" """A reviewer runs harnesses in a worktree of its own; the committed evidence stays put, in any directory."""
monkeypatch.setattr(video_audio_check, "PAYLOAD", tmp_path / "video_audio.json")
monkeypatch.setattr(video_audio_check, "_tracked", lambda path: True) monkeypatch.setattr(video_audio_check, "_tracked", lambda path: True)
frozen = tmp_path / "VIDEO_AUDIO.md" frozen = tmp_path / "VIDEO_AUDIO.md"
frozen.write_text("committed evidence", encoding="utf-8") frozen.write_text("committed evidence", encoding="utf-8")
@@ -21,14 +20,14 @@ def test_a_frozen_report_is_not_overwritten_by_a_harness(
elsewhere = tmp_path / "scratch" / "VIDEO_AUDIO.md" elsewhere = tmp_path / "scratch" / "VIDEO_AUDIO.md"
elsewhere.parent.mkdir() elsewhere.parent.mkdir()
elsewhere.write_text("scratch", encoding="utf-8") elsewhere.write_text("scratch", encoding="utf-8")
video_audio_check.refuse_frozen_overwrite(elsewhere) with pytest.raises(SystemExit, match="is a frozen report"):
video_audio_check.refuse_frozen_overwrite(elsewhere)
def test_a_harness_may_write_its_own_untracked_output( def test_a_harness_may_write_its_own_untracked_output(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""The default destination is the reports directory; refusing it would stop the shipped run.""" """The default destination is the reports directory; refusing it would stop the shipped run."""
monkeypatch.setattr(video_audio_check, "PAYLOAD", tmp_path / "video_audio.json")
monkeypatch.setattr(video_audio_check, "_tracked", lambda path: False) monkeypatch.setattr(video_audio_check, "_tracked", lambda path: False)
fresh = tmp_path / "VIDEO_AUDIO.md" fresh = tmp_path / "VIDEO_AUDIO.md"
fresh.write_text("not yet evidence", encoding="utf-8") fresh.write_text("not yet evidence", encoding="utf-8")
@@ -38,8 +37,7 @@ def test_a_harness_may_write_its_own_untracked_output(
def test_an_absolute_path_to_the_frozen_directory_is_refused_too( def test_an_absolute_path_to_the_frozen_directory_is_refused_too(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""The guard compares where the file really lands, not how the path was spelled.""" """The guard asks about the file, not the spelling of its path."""
monkeypatch.setattr(video_audio_check, "PAYLOAD", tmp_path / "video_audio.json")
monkeypatch.setattr(video_audio_check, "_tracked", lambda path: True) monkeypatch.setattr(video_audio_check, "_tracked", lambda path: True)
frozen = tmp_path / "VIDEO_AUDIO.md" frozen = tmp_path / "VIDEO_AUDIO.md"
frozen.write_text("committed evidence", encoding="utf-8") frozen.write_text("committed evidence", encoding="utf-8")
@@ -47,11 +45,21 @@ def test_an_absolute_path_to_the_frozen_directory_is_refused_too(
video_audio_check.refuse_frozen_overwrite(Path(frozen.resolve())) video_audio_check.refuse_frozen_overwrite(Path(frozen.resolve()))
def test_a_committed_report_is_frozen_without_any_stub(tmp_path: Path) -> None:
"""A stubbed `_tracked` proves the branch works, not that the guard does: this test uses the real git index."""
committed = Path("bench/reports/LEGS.md")
assert committed.exists(), "the acceptance needs a tracked report to refuse"
with pytest.raises(SystemExit, match="is a frozen report"):
video_audio_check.refuse_frozen_overwrite(committed)
elsewhere = tmp_path / "LEGS.md"
elsewhere.write_text("mine", encoding="utf-8")
video_audio_check.refuse_frozen_overwrite(elsewhere)
def test_the_probe_arm_switches_off_rather_than_ranking_nothing() -> None: def test_the_probe_arm_switches_off_rather_than_ranking_nothing() -> None:
"""--probes 0 means "skip that arm"; a small count with the smoke knob means "you measured nothing".""" """--probes 0 means "skip that arm"; a small count with the smoke knob means "you measured nothing"."""
assert video_audio_check.probe_sample(0, 0) == [] assert video_audio_check.probe_sample(0, 0) == []
# The shipped sample keeps every modality; the smoke knob narrows it to video, where a # The shipped sample keeps every modality; the smoke knob narrows it to video, where a small count can miss one entirely.
# small count can miss the modality entirely (the reviewer's --limit 1 --probes 1 crash).
assert any(query.modality == video_audio_check.VIDEO_MODALITY for query in video_audio_check.probe_sample(300, 0)) assert any(query.modality == video_audio_check.VIDEO_MODALITY for query in video_audio_check.probe_sample(300, 0))
for probes in (1, 5): for probes in (1, 5):
with pytest.raises(SystemExit, match=f"--probes {probes} picked no answerable video query"): with pytest.raises(SystemExit, match=f"--probes {probes} picked no answerable video query"):
+10 -5
View File
@@ -272,10 +272,17 @@ def write_markdown(report: dict[str, Any], destination: Path) -> None:
def _tracked(path: Path) -> bool: def _tracked(path: Path) -> bool:
"""Only committed evidence is frozen; a harness's own untracked output is fair game.""" """Only committed evidence is frozen; a harness's own untracked output is fair game.
The question goes to the repository that owns the path, so a reviewer working in a worktree
cannot point at another checkout's committed report and be told it is untracked.
"""
owner = path.resolve().parent
return ( return (
subprocess.run( subprocess.run(
["git", "ls-files", "--error-unmatch", str(path)], capture_output=True, check=False ["git", "-C", str(owner), "ls-files", "--error-unmatch", path.resolve().name],
capture_output=True,
check=False,
).returncode ).returncode
== 0 == 0
) )
@@ -283,9 +290,7 @@ def _tracked(path: Path) -> bool:
def refuse_frozen_overwrite(destination: Path) -> None: def refuse_frozen_overwrite(destination: Path) -> None:
"""A reviewer running this in a worktree of its own must not overwrite the committed evidence.""" """A reviewer running this in a worktree of its own must not overwrite the committed evidence."""
if destination.exists() and destination.resolve().parent == PAYLOAD.resolve().parent and _tracked( if destination.exists() and _tracked(destination):
destination
):
raise SystemExit( raise SystemExit(
f"{destination} is a frozen report; point --report and --payload somewhere else, or " f"{destination} is a frozen report; point --report and --payload somewhere else, or "
"delete the frozen copy on purpose rather than under a harness" "delete the frozen copy on purpose rather than under a harness"