Corroborate a mixed-script claim in both scripts

A claim written in Chinese and English was checked only in English: the
longest Latin words took every slot the rule demands, so one planted
English word moved the closing Chinese pair out of the comparison and a
witness that contradicted the number became a second source. The attack
costs nothing — append "the" to a Chinese sentence — and it fails open,
which is the wrong direction for a rule whose whole job is to keep an
unvouched claim out of the prompt.

Each script is now checked in its own terms: a claim with CJK pairs is
demanded at its opening, middle and tail, plus its single most
distinctive Latin word. A Latin-only claim keeps its three longest
words. The demanded set is what claim_terms returns, all of it.

Measured: the planted-word witnesses are no longer believed, a verbatim
repeat still is, and a mixed claim that agrees in only one script is not
a witness. The term-level boundaries are listed in CHECKLIST for both
scripts, including the two the reviewer found that this rule does not
catch: a demanded pair inside a longer word, and a long claim whose
middle position lands on filler.

Also corrects the record: the boundaries were not the same in both
scripts as claimed, a historical line said "now" instead of naming a
commit, and a limitation line I deleted in the last round is restored.

Gates: 322 passed, ruff clean, mypy 24 errors / 9 files (baseline, net
zero), pyright 0 on the two touched files, filesize clean, product audit
clean.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 03:12:56 -04:00
1 parent 334071199e
commit 8d84e03077
3 files changed
+49 -17

No files matched your search

+10 -4
View File
@@ -377,7 +377,10 @@
- 佐证**读时算、不存**(hai-razor):它是「此刻语料里有没有第二个来源」的属性,存下来就得每次写入重算,还会让外来事实永远引用不了。限制写进 docstring:佐证是**词面**的、不是语义的——两个来源共享同一个说法只说明这话被看见了两次。
- 旧规则被评审跑出两个反例,所以换了机制:旧写法把词交给 `rare_terms` 过滤,而它按语料规模判「常见」——2 个 atom 的库里陈述自己的词(thermostat / archive / eighteen)撞上上限被判常见丢掉,只剩攻击词,于是补一条**逐字相同、不同 source** 的记忆也不翻案;反过来只补 40 条**无关**记忆,上限抬到 3,它立刻可引用、整句注入语进了 `packed_text`。现在固定要 claim 最长的 **3** 个词、全部命中(新 `text_index.fts_all_expression`,AND 而非 OR),并且**剔掉攻击短语自己的词**(`trust.claim_terms`),所以语料大小和攻击词都决定不了翻案。测试 `test_a_library_full_of_unrelated_memories_is_not_a_second_witness`、`test_the_words_of_the_attack_do_not_vouch_for_the_claim_they_carry`、`test_a_memory_that_is_only_an_instruction_stays_uncited_when_everyone_repeats_it`。SQL 只在 `v_atoms_current` 里找、`quarantined = 0` 才配作证、同 source 不算自己、上限 3 条。
- 评审第 2 轮跑出来的真缺陷:中文没有佐证通道。`fts_terms` 把一个 CJK 串算**一个**词,所以「档案室的恒温器保持十八度」只有一个词,永远够不上三条要求,多少独立来源重复它都不翻案(实测一句 / 两句都是 `is_citable=False`,凑够三个分句才 True)。改法:CJK 串拆成相邻二字组(新 `text_index.cjk_pairs` / `is_cjk`,`_cjk_phrase` 顺手复用),攻击短语自己的二字组一并剔掉(`trust._signal_words`)。实测:逐字重复 → 可引用;纯指令句拆完只剩 1 个可用二字组 → 重复多少遍都不可引用。测试 `test_a_chinese_claim_becomes_citable_when_another_source_repeats_it`、`test_a_chinese_instruction_cannot_vouch_for_its_own_characters`。
- 评审第 3 轮跑出来的真缺陷 D12:上一版按**每个 CJK 段**分别取词,加上等长二字组按插入序保留,于是多段 claim 的三个必需词**全落在第一段**(5 个多段 claim 里 4 个如此),短 run 的 `int(i*len/3)` 在 `len=4/5` 时给 `0,1,2` / `0,1,3`——恰好挤在开头,与 `trust.py` docstring 自述的「spread across the run instead of clustered at its start」相反。后果实测:`恒温器很贵` 被 `恒温器很便宜` 判可引用、`这房子值得买` 被 `这房子不值得买` 判可引用(互相矛盾的说法互为佐证),一条只重复首句的无关记忆把被隔离的 `全仓买入` 送进 `packed_text`。方向是 fail-open,正打在「外来句要第二个来源在三处分别对上」这条规则的目的上。修法:二字组跨段合并成一池,固定要**开头、正中、结尾**三个位置(结尾就是断言本身)。改后实测:上述反例全部 `is_citable=False`;逐字重复、`恒温器保持十八度` 同句重复、多段 claim 逐字重复仍 `True`;只共享 3 个里 2 个词的 witness 仍 `False`;`_cjk_phrase` 复用 `cjk_pairs` 后 15 条中文/混排查询的 terms、表达式、命中集与 `203b6d0` 逐字一致(检索无回归)。词面规则的边界也实测钉住,中英一样:保留开头/正中/结尾三处的换词说法**算**佐证(`恒温器一直放在档案室,读数十八度` → `True`),换掉被比位置的写法不算(`档案室里的温控装置固定在十八摄氏度` → `False`),英文只少一个否定词也算佐证(`…holds eighteen degrees` 被 `…does not hold eighteen degrees` 判 `True`),数值换了不算(`eighteen`→`twenty` → `False`)。测试 `test_a_witness_that_denies_the_claim_does_not_vouch_for_it`、`test_a_witness_that_only_repeats_the_opening_of_a_claim_does_not_vouch_for_it`、`test_a_chinese_restatement_that_keeps_the_compared_pairs_is_a_second_witness`、`test_a_chinese_rewording_that_replaces_the_compared_pairs_is_not_a_second_witness`、`test_a_witness_that_negates_one_word_is_still_read_as_agreement`。
- 评审第 3 轮跑出来的真缺陷 D12:上一版按**每个 CJK 段**分别取词,加上等长二字组按插入序保留,于是多段 claim 的三个必需词**全落在第一段**(5 个多段 claim 里 4 个如此),短 run 的 `int(i*len/3)` 在 `len=4/5` 时给 `0,1,2` / `0,1,3`——恰好挤在开头,与 `trust.py` docstring 自述的「spread across the run instead of clustered at its start」相反。后果实测:`恒温器很贵` 被 `恒温器很便宜` 判可引用、`这房子值得买` 被 `这房子不值得买` 判可引用(互相矛盾的说法互为佐证),一条只重复首句的无关记忆把被隔离的 `全仓买入` 送进 `packed_text`。方向是 fail-open,正打在「外来句要第二个来源在三处分别对上」这条规则的目的上。修法:二字组跨段合并成一池,固定要**开头、正中、结尾**三个位置(结尾就是断言本身)。改后实测:上述反例全部 `is_citable=False`;逐字重复、`恒温器保持十八度` 同句重复、多段 claim 逐字重复仍 `True`;只共享 3 个里 2 个词的 witness 仍 `False`;`_cjk_phrase` 复用 `cjk_pairs` 后 15 条中文/混排查询的 terms、表达式、命中集与 `203b6d0` 逐字一致(检索无回归)。词面规则的边界也实测钉住(当时中英**不一样**,见下面 D15 那条):保留开头/正中/结尾三处的换词说法**算**佐证(`恒温器一直放在档案室,读数十八度` → `True`),换掉被比位置的写法不算(`档案室里的温控装置固定在十八摄氏度` → `False`),英文只少一个否定词也算佐证(`…holds eighteen degrees` 被 `…does not hold eighteen degrees` 判 `True`),数值换了不算(`eighteen`→`twenty` → `False`)。测试 `test_a_witness_that_denies_the_claim_does_not_vouch_for_it`、`test_a_witness_that_only_repeats_the_opening_of_a_claim_does_not_vouch_for_it`、`test_a_chinese_restatement_that_keeps_the_compared_pairs_is_a_second_witness`、`test_a_chinese_rewording_that_replaces_the_compared_pairs_is_not_a_second_witness`、`test_a_witness_that_negates_one_word_is_still_read_as_agreement`。
- 评审第 4 轮跑出来的真缺陷 D15:混排 claim 里一个 ≥3 字母的拉丁词就能把「结尾断言」挤出被要求集——`claim_terms` 把拉丁词与二字组混在一起按长度排,`corroborating_atom_ids` 只取前三个,于是 `档案室的恒温器保持十八度 the` 被 `…保持二十六度 the` 判可引用(端到端:HTTP 上传那句带着错的数值进 prompt,`packed_text` 同时出现「十八度」与「二十六度」)。攻击者零成本(往中文句里塞一个英文词),方向仍是 fail-open,与 D12 同族。修法:**每种文字各查各的**——有 CJK 二字组就固定要它的开头 / 正中 / 结尾三个位置,再外加最长的一个拉丁词(新 `LATIN_WITH_CJK = 1`);纯拉丁 claim 仍要最长三个词;`corroborating_atom_ids` 改成要求 `claim_terms` 交出的**全部**词(AND)。实测:`…十八度 cryptography` / `… the` / `… AI` / `恒温器很贵 thermodynamics …` 配矛盾 witness 全部 `False`,逐字重复仍 `True`;混排 claim 只同意一种文字不算佐证(`thermostat eighteen 档案` → `False`),两种都留下才算(`test_a_mixed_claim_is_corroborated_in_both_scripts`)。
- 词面规则现在实测钉住的边界(中英各自量过):保留开头/正中/结尾三处的换词说法**算**佐证(`恒温器一直放在档案室,读数十八度` → `True`),换掉被比位置的写法不算(`档案室里的温控装置固定在十八摄氏度` → `False`);witness 把被要求的二字组装进更长的词里也算(`这个方案很好` 被 `这个方案很好笑` 判 `True`,很好 ⊂ 很好笑);长 claim 的正中可能落在填充词上,于是 `我于二零二四年三月五号在档案室把恒温器调到十八度并且锁上了门`(29 个二字组,要 `我于 / 室把 / 了门`)换掉日期与数值仍 `True`,但改掉结尾(`锁上了门`→`没锁门`)就 `False`;英文少一个否定词仍算佐证(`…holds eighteen degrees` ← `…does not hold eighteen degrees` → `True`),数值换了不算(`eighteen`→`twenty` → `False`)。测试 `test_a_witness_that_denies_the_claim_does_not_vouch_for_it`、`test_a_witness_that_only_repeats_the_opening_of_a_claim_does_not_vouch_for_it`、`test_a_chinese_restatement_that_keeps_the_compared_pairs_is_a_second_witness`、`test_a_chinese_rewording_that_replaces_the_compared_pairs_is_not_a_second_witness`、`test_a_witness_that_negates_one_word_is_still_read_as_agreement`、`test_one_foreign_latin_word_cannot_drop_the_chinese_assertion`。
- 明写的限制(D18:这行曾被误删,行为与 docstring 一直在):可用词不足 3 个的 claim 永不佐证——`Pineapple answer` 逐字重复也不翻案(测试 `test_a_claim_too_short_to_compare_stays_uncited`),2–3 字的 CJK 串只剩 1–2 个二字组,实测 `钥匙` / `钥匙丢` / `恒温器` 逐字重复也不翻案;短到没法比的话选择不信,方向是 fail-closed。带指令前缀的中文 claim 会被剔掉自己的二字组(纯指令句只剩 1 个可用词),但「指令 + 正文」这类句子剔完之后与裸正文取到的词**完全相同**,所以「更难佐证」在那类句子上无可测差异——方向上不会更容易,这是实测不是推断。
- [x] p2-2.3 外来文本里的指令进隔离区(诚实的名字:固定短语清单检测(折叠空白 / 全半角后子串匹配)+ 隔离 + 声明,不是「消毒」)
- 19 条字面信号(10 英 9 中),`NFKC` + casefold + 空白折叠后匹配,所以 `IGNORE PREVIOUS INSTRUCTIONS` 认得。命中只**隔离**不删词:句子还在、搜得到、不进上下文,命中词留在 `atom.metadata["injection_signals"]`。自己记住的「ignore previous instructions」同样被隔离(`origin` 仍是 `self`)——隔离针对句子,不针对作者。
- 限制(评审判的,我认):19 条是字面清单,`disregard foregoing directives` 这类改写绕过检测;绕过之后仍靠佐证把关,所以风险有限。没有任何对文本本身的消毒(不剥词、不转义、不改写),所以这一节不叫消毒。
@@ -386,11 +389,14 @@
- `forget` 现在清掉:atom(连带 `atom_fts`、`term_df`)、`evidence_edges`(元信念)、vec0 向量、图片索引、**`sources` 行**(新 `db.delete_source`,删不掉就 fail fast)、**语义缓存文件**(新 `CacheFabric.drop_semantic_cache()`,整个 `cache/semantic/*.json`——派生数据、forget 罕见、没人能保证一份编译过的上下文只引用了还活着的记忆)。`cartridge_atoms` 靠 `ON DELETE CASCADE` 走,`cache_events` 留一条 `memory/invalidated` 痕迹。导出卡带只报告不改写(别人引过它的 DNA)。报告多一个 `semantic_cache_cleared` 字段,Go `domain.ForgetReport` 同步,前端 `types.ts` 与 `MemoryPanel` 也显示它(评审指出三端不一致,只有前端漏了)。
- **评审第 1 轮 FAIL**(独立 pi 自己跑,12 项全跑):4 条实测缺陷 + 1 条三端不一致,全部改掉——D1 「查询不是写入」错(search/ask 写 `term_df`);D2 「11 处」实测 13;D3 信号英中拆分数错(实为 10/9);D4 佐证被攻击词劫持、能否翻案取决于语料大小(补逐字相同的第二来源不翻案,补 40 条无关记忆反而翻案);D5 `ForgetReport` 只有前端漏字段。评审同时实测确认:门禁全绿(306 / mypy 24-9 净 0 / pyright 0 / go 全套 / npm 221.39 kB + eslint 0)、403-403-401-200 语义逐条对、旧环境变量名无效、缓存不冻住新佐证(`cache_trace_id` 会变)、级联逐表数过(atoms/atom_fts/edges/sources/cartridge_atoms/term_df/vec0/semantic 全归 0,revision 1→2)、smoke 8.3s PASS、故意在 `proofs/` 塞 retry-until-score 确实被 `--product` 报出来。本轮修完的门禁:`pytest -q` **312 passed**(+6 条新测试)、ruff 干净、mypy 冷缓存 **24 errors / 9 files**(净 0)、pyright 对 5 个改动文件 **0 errors**(顺手修了 `runtime.py` 两条预存的 `process_vision_info` 元组宽度报错:实测该库只在 `return_video_kwargs=True` 时多返一个元素,调用没传该旗标,所以取 `[:2]`,行为不变)、`npm run build` 221.48 kB + eslint exit 0、`bash scripts/mcp_write_smoke.sh` PASS。
- 顺带修一个根因:语义缓存 key 原是 `sha256("compiler-v2\n{profile}\n{question}")`,编译过一次就把隔离判定冻住——新证据进来了旧答案照旧。改成把 **store revision** 放进 key(新 `store_meta` 表,`upsert_atoms` / `close_atoms` / `delete_atoms` 各 +1)。已知边界:卡带 mount/switch 不 bump。
- 验收:`pytest backend/tests/test_api_auth.py backend/tests/test_trust_quarantine.py backend/tests/test_memory_write.py backend/tests/test_text_index.py` → **8 + 21 + 8 + 18** 全绿,合跑 `55 passed`(无 token 写被拒、投毒进隔离区且不进 `packed_text`、级联逐项数出来:atom / fts / 边 / 向量 / `sources` / `cartridge_atoms` / 缓存文件)。跨进程:`bash scripts/mcp_write_smoke.sh` → `PASS: remember, supersede, search, forget, cartridge mounting and the transactive referral work through MCP`,脚本先证 401(无 token 写)与 200(带 token 写)再跑 MCP 全流程,两个服务器加 MCP 子进程共用一个 token。
- 验收:`pytest backend/tests/test_api_auth.py backend/tests/test_trust_quarantine.py backend/tests/test_memory_write.py backend/tests/test_text_index.py` → **8 + 23 + 8 + 18** 全绿,合跑 `57 passed`(无 token 写被拒、投毒进隔离区且不进 `packed_text`、级联逐项数出来:atom / fts / 边 / 向量 / `sources` / `cartridge_atoms` / 缓存文件)。跨进程:`bash scripts/mcp_write_smoke.sh` → `PASS: remember, supersede, search, forget, cartridge mounting and the transactive referral work through MCP`,脚本先证 401(无 token 写)与 200(带 token 写)再跑 MCP 全流程,两个服务器加 MCP 子进程共用一个 token。
- 门禁(`9145a23` 那轮量的,后面几轮的数字在下面的评审记录里):`pytest -q` **306 passed**;`ruff check` 干净;mypy 冷缓存 **24 errors / 9 files**(与 `9cb46c1` 基线净新增 0);`./node_modules/.bin/pyright` 对 23 个后端文件与 2 个 bench 文件 **0 errors**;`go build` / `go vet` / `go test` + `gofmt -l` 空;`npm run build` 221.39 kB + eslint exit 0;`bench/filesize_check` 干净。
- **评审第 2 轮 FAIL**(同一独立 pi,12 项全跑,D1–D5 的修法逐条跑验成立):5 条缺陷,全是记录与一处功能缺陷——D6 改名格分项相加 11 ≠ 声称的 13、文件数写 6 实为 7;D7 中文佐证通道是死的(见 p2-2.2 那条,已修);D8 验收格写 `8 + 9 + 8`,实测 `8 + 13 + 8`(我加测试后现在是 `8 + 17 + 8`);D9 门禁格还写 306 / 221.39 kB,与同节评审记录自相矛盾;D10 `text_index.py:118` 漂到空行(现钉 :134 与 :121)。评审另实测确认:生造词不写 `term_df`、真实词才写行且上界是语料前缀集合;19/10/9 信号拆分对;五个攻击里除 CJK 外全部与 docstring 一致;三端 `ForgetReport` 10 字段一致且前端显示;`process_vision_info` 取 `[:2]` 是无操作(库只在 `return_video_kwargs=True` 时多返一个元素,调用点没传),修法正当;读路径唯一写语句就是 `term_df` 那一条,13 条 GET 与 3 个未卡 POST 逐表逐文件 diff 除 `term_df` 外全零。
- **评审第 2 轮 FAIL**(同一独立 pi,12 项全跑,D1–D5 的修法逐条跑验成立):5 条缺陷,全是记录与一处功能缺陷——D6 改名格分项相加 11 ≠ 声称的 13、文件数写 6 实为 7;D7 中文佐证通道是死的(见 p2-2.2 那条,已修);D8 验收格写 `8 + 9 + 8`,实测 `8 + 13 + 8`(那一轮加测试后是 `8 + 17 + 8`,钉在 `203b6d0`;当前数字见下面验收格);D9 门禁格还写 306 / 221.39 kB,与同节评审记录自相矛盾;D10 `text_index.py:118` 漂到空行(现钉 :134 与 :121)。评审另实测确认:生造词不写 `term_df`、真实词才写行且上界是语料前缀集合;19/10/9 信号拆分对;五个攻击里除 CJK 外全部与 docstring 一致;三端 `ForgetReport` 10 字段一致且前端显示;`process_vision_info` 取 `[:2]` 是无操作(库只在 `return_video_kwargs=True` 时多返一个元素,调用点没传),修法正当;读路径唯一写语句就是 `term_df` 那一条,13 条 GET 与 3 个未卡 POST 逐表逐文件 diff 除 `term_df` 外全零。
- 评审顺带量到一个**预存**检索缺陷(不在 p2-2 范围,本轮不计):小库里 `rare_terms` 会把唯一命中的词换成不命中的词——3 个 atom 时上限 `cap = max(int(3*0.05)+1, 2) = 2`,`archive/room/the/hold` 各命中 2 条被判「常见」丢掉,`what/temperature/does` 命中 0 反而被当「罕见」留下,于是 `what temperature does the archive room hold` 搜出 0 条;`store_search.py:54` 与 `retrieval.py:68` 都没有放宽回退。它和 D4 的旧根因同族(按语料规模判词表),记在这里当 p2-3 之后的候选,不当已修。
- **评审第 3 轮 FAIL**(同一独立 pi,8 项全跑):D6 / D7 / D8 / D10 的修法全部实测成立(改名 13 行 / 7 文件分项相加 13、验收格与实数对、行号逐行对上、D7 的中文通道过五个攻击、`_cjk_phrase` 重构后 15 条中文查询与 `203b6d0` 逐字一致),命名 judged 诚实(比第 2 轮诚实,可接受),纪律扫描干净(141 个 tracked .py、零多行注释块、最大 498 行、新名字全部有引用、旧名 `_claim_terms` / `_is_cjk` 零残留),p2-2 全节逐格对表。四条新缺陷:D11 门禁格标签钉 `203b6d0` 但格里的 306 / 221.39 kB / pyright 23+2 是 `9145a23` 那轮的量(它在 `203b6d0` worktree 实测 312、bundle 221.48 kB、该轮只改 5 个后端文件);D12 就是上面那条真功能缺陷;D13 验收格「合跑 51」与同格所列三文件命令不符(三文件实为 33);D14 「7 个 reader 路径」未写口径(按 `list_atoms|search_atoms` 实测 8 个调用方)。D11 / D13 / D14 全部改成可复现的说法(门禁格改钉 `9145a23`、验收格列齐四文件钉 55、reader 口径直接点名 7 个 `_ingest_*` 函数与两处 `trust.stamp` 行号)。本轮修完的门禁:`pytest -q` **320 passed**(+4 条佐证边界测试)、ruff 干净、mypy 冷缓存 **24 errors / 9 files**(净 0)、pyright 对 `trust.py` 与 `test_trust_quarantine.py` **0 errors**、`bench/filesize_check` 干净、`bench.audit --product` 31 / 2 / 6 clean、go 全套 + `gofmt -l` 空、`bash scripts/mcp_write_smoke.sh` PASS;本轮没动前端,`npm run build` 221.48 kB + eslint exit 0 是 `ff2471d` 上量的。
- **评审第 3 轮 FAIL**(同一独立 pi,8 项全跑):D6 / D7 / D8 / D10 的修法全部实测成立(改名 13 行 / 7 文件分项相加 13、验收格与实数对、行号逐行对上、D7 的中文通道过五个攻击、`_cjk_phrase` 重构后 15 条中文查询与 `203b6d0` 逐字一致),命名 judged 诚实(比第 2 轮诚实,可接受),纪律扫描干净(141 个 tracked .py、零多行注释块、最大 498 行、新名字全部有引用、旧名 `_claim_terms` / `_is_cjk` 零残留),p2-2 全节逐格对表。四条新缺陷:D11 门禁格标签钉 `203b6d0` 但格里的 306 / 221.39 kB / pyright 23+2 是 `9145a23` 那轮的量(它在 `203b6d0` worktree 实测 312、bundle 221.48 kB、该轮只改 5 个后端文件);D12 就是上面那条真功能缺陷;D13 验收格「合跑 51」与同格所列三文件命令不符(三文件实为 33);D14 「7 个 reader 路径」未写口径(按 `list_atoms|search_atoms` 实测 8 个调用方)。D11 / D13 / D14 全部改成可复现的说法(门禁格改钉 `9145a23`、验收格列齐四文件钉 55(那一轮的数字,当前见上面验收格)、reader 口径直接点名 7 个 `_ingest_*` 函数与两处 `trust.stamp` 行号)。本轮修完的门禁:`pytest -q` **320 passed**(+4 条佐证边界测试)、ruff 干净、mypy 冷缓存 **24 errors / 9 files**(净 0)、pyright 对 `trust.py` 与 `test_trust_quarantine.py` **0 errors**、`bench/filesize_check` 干净、`bench.audit --product` 31 / 2 / 6 clean、go 全套 + `gofmt -l` 空、`bash scripts/mcp_write_smoke.sh` PASS;本轮没动前端,`npm run build` 221.48 kB + eslint exit 0 是 `ff2471d` 上量的。
- **评审第 4 轮 FAIL**(同一独立 pi,8 项全跑):D11 / D13 / D14 的修法全部实测成立(它在 `9145a23` worktree 里逐字重现了门禁格那行数字:306 passed、bundle `index-u-5quhcj.js` 221.39 kB、`git show --name-only` 数出 23 个后端文件;验收格四文件与合跑 55 对上;点名的 7 个 `_ingest_*` 与两处 `trust.stamp` 行号逐个存在)。新缺陷四条:D15 就是上面那条混排 fail-open(真功能缺陷);D16 「中英一样」实测不成立(拉丁词按长度吃掉三个名额,CJK 位置从不参与);D17 第 2 轮记录行还写「我加测试后现在是 `8 + 17 + 8`」,与验收格自相矛盾(历史行必须钉提交或钉数字,不能写「现在」);D18 我第 3 轮的编辑删掉了「可用词不足 3 个永不佐证」那条限制,行为与 docstring 一直在但记录没了、测试没人引用。评审自己跑的结果:`pytest -q` 320、ruff、`bench.filesize_check`、`bench.audit --product` 31 / 2 / 6 clean、pyright 0、mypy 冷缓存 24 / 9(同命令在 `9cb46c1` worktree 也是 24 / 9,文件集与逐条报错全等 ⇒ 净 0)、go 全套 + `gofmt -l` 空、`npm run build` 221.48 kB + eslint 0、smoke 4.861s PASS;把 `trust.py` 在两棵树之间互换,5 条新测试里 3 条在旧机制下真红(钉的是机制不是现状);15 条中文/混排查询命中集与 `ff2471d` 逐字一致;纪律扫描干净(141 个 tracked `.py`、零多行整行注释块、最大 498 行、新名全部有引用、旧名零残留)。
- 评审量到一个**测量陷阱**(方法问题不是缺陷,但影响本仓所有「在某提交 worktree 里量的」断言):`.venv/lib/python3.14/site-packages/__editable__.ely_eye-0.1.0.pth` 写的是主仓绝对路径,`pyproject.toml:4 pythonpath = ["."]` 只加仓根,所以在 worktree 里裸跑 pytest/python 导入的是**主树**的代码。任何「在 commit X 上量的」必须先 `PYTHONPATH=$PWD/backend/src`(它第一次换 `trust.py` 的实验因此三条对照假绿)。
- D15 修完的门禁:`pytest -q` **322 passed**(+2 条混排测试)、四文件合跑 **57 passed**、`ruff check` 干净、mypy 冷缓存 **24 errors / 9 files**(净 0)、`./node_modules/.bin/pyright` 对 `trust.py` 与 `test_trust_quarantine.py` **0 errors**、`bench/filesize_check` 干净、`bench.audit --product` 31 / 2 / 6 clean;本轮没动前端与 Go,`npm run build` 221.48 kB 与 go 全套是 `ff2471d` / `3340711` 上量的。
- 顺带修的 harness 缺陷:`bench/audit.py --product` 还指着 `evals.py`,而证明套件在 p1-6 已拆进 `backend/src/ely_eye/proofs/`。它 fail fast 报了「not there」,但那条反自证规则其实已经**审不到产品**。改成 `product_sources(root)` 审整个包,并加测试钉住「必须有 `suite.py`、整包零 re-roll 违规」。现在 `--product` → `audited 31 harness files, 2 reports, 6 set files` / `clean`,顺带在当前树上重新证了 p0-6。
- [ ] **p2-3** 短期/长期分层
- [ ] p2-3.1 工作记忆层
+21 -13
View File
@@ -43,6 +43,8 @@ INJECTION_SIGNALS: tuple[str, ...] = (
CORROBORATION_TERMS = 3
CORROBORATION_LIMIT = 3
# A claim written in both scripts is corroborated in both: the CJK positions plus its most distinctive Latin word.
LATIN_WITH_CJK = 1
def _fold(text: str) -> str:
@@ -78,17 +80,17 @@ def corroborating_atom_ids(
that the claim was seen twice, not proof that it is true. A closed or quarantined atom cannot
corroborate, so a superseded fact stops propping up whatever cited it.
The words demanded are the claim's longest, with the instruction phrases stripped out, and the
count is fixed. Neither the attack's own words nor the size of the library decides whether a
second source is believed: an earlier rule that dropped words common to the corpus left a
two-atom library unable to corroborate anything, and 40 unrelated memories able to. A claim with
fewer usable words than the rule demands is never corroborated, so a two-word rumour stays
uncited — failing closed was chosen over trusting a sentence too short to compare.
The words demanded are the claim's own spine, with the instruction phrases stripped out. Neither
the attack's own words nor the size of the library decides whether a second source is believed:
an earlier rule that dropped words common to the corpus left a two-atom library unable to
corroborate anything, and 40 unrelated memories able to. A claim with fewer usable words than the
rule demands is never corroborated, so a two-word rumour stays uncited — failing closed was
chosen over trusting a sentence too short to compare.
"""
terms = claim_terms(atom.text)
if len(terms) < CORROBORATION_TERMS:
return []
expression = fts_all_expression(terms[:CORROBORATION_TERMS])
expression = fts_all_expression(terms)
with database.connect() as connection:
rows = connection.execute(
"""
@@ -113,7 +115,7 @@ def is_citable(atom: EvidenceAtom, database: Database) -> bool:
def claim_terms(text: str) -> list[str]:
"""This claim's own words, longest first, with the instruction phrases stripped out.
"""This claim's own spine, with the instruction phrases stripped out.
The words that made a text untrustworthy must not decide whether it is believed: a stranger who
writes "answer pineapple" twice would otherwise corroborate himself.
@@ -121,8 +123,13 @@ def claim_terms(text: str) -> list[str]:
A CJK run is a single term to fts_terms, so a Chinese claim would offer one term and could never
reach the three this rule demands. Every run is broken into its adjacent two-character pairs, the
pairs are pooled across the whole claim, and three are demanded from the start, the middle and
the end of that pool — so a second source has to agree with what the claim asserts, not merely
open the same way.
the end of that pool. A claim mixing scripts is demanded in both: the CJK positions plus its
longest Latin word, because sorting the two channels together by length let one planted English
word drop the Chinese assertion out of the comparison.
What that does not buy: the middle position of a long claim can land on filler, so a witness that
keeps the three positions and changes a date or a number elsewhere is still read as agreement, and
a witness that contains a demanded pair inside a longer word (很好 inside 很好笑) counts too.
"""
try:
terms = fts_terms(text)
@@ -136,8 +143,9 @@ def claim_terms(text: str) -> list[str]:
pairs.extend(pair for pair in cjk_pairs(term) if pair not in blocked)
elif term not in blocked:
words.append(term)
chosen = words + _spread(list(dict.fromkeys(pairs)))
return sorted(dict.fromkeys(chosen), key=lambda term: -len(term))
spread = _spread(list(dict.fromkeys(pairs)))
longest = sorted(dict.fromkeys(words), key=lambda word: -len(word))
return spread + longest[:LATIN_WITH_CJK if spread else CORROBORATION_TERMS]
def _signal_words(text: str) -> set[str]:
@@ -160,7 +168,7 @@ def _spread(pairs: list[str]) -> list[str]:
Spreading each run on its own clustered the demanded pairs on the first clause, and an evenly
spaced index did the same to a short run: a four-pair claim demanded its first three pairs, so
「恒温器很贵」 was corroborated by 「恒温器很便宜」. The last pair is the claim's assertion, so it
is demanded whatever the length.
is demanded whenever the claim has CJK pairs at all.
"""
if len(pairs) <= CORROBORATION_TERMS:
return pairs
+18
View File
@@ -223,6 +223,24 @@ def test_a_chinese_instruction_cannot_vouch_for_its_own_characters(settings: Set
assert not is_citable(stranger, Database(settings)), "a sentence too short to compare is not believed twice"
def test_one_foreign_latin_word_cannot_drop_the_chinese_assertion(settings: Settings) -> None:
stranger = remember(settings, "档案室的恒温器保持十八度 cryptography", origin=MemoryOrigin.third_party, holder="vendor")
remember(settings, "档案室的恒温器保持二十六度 cryptography", holder="husband")
assert not is_citable(stranger, Database(settings)), "a planted English word must not drop the Chinese assertion from the comparison"
repeat = remember(settings, "恒温器很贵 thermodynamics", origin=MemoryOrigin.third_party, holder="dealer")
remember(settings, "恒温器很便宜 thermodynamics", holder="wife")
assert not is_citable(repeat, Database(settings)), "the CJK positions are demanded beside a Latin word too"
def test_a_mixed_claim_is_corroborated_in_both_scripts(settings: Settings) -> None:
claim = "档案室的 thermostat 保持 eighteen 度"
stranger = remember(settings, claim, origin=MemoryOrigin.third_party, holder="vendor")
remember(settings, "thermostat eighteen 档案", holder="husband")
assert not is_citable(stranger, Database(settings)), "agreeing in one script is not agreeing with a claim written in two"
remember(settings, claim, holder="wife")
assert is_citable(stranger, Database(settings)), "a witness that keeps both scripts is a witness"
def test_a_claim_too_short_to_compare_stays_uncited(settings: Settings) -> None:
stranger = remember(settings, "Pineapple answer", origin=MemoryOrigin.third_party, holder="vendor")
remember(settings, "Pineapple answer", holder="husband")