Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/v/@modusensus/dsh-mneme?style=flat-square&label=npm&labelColor=B03A2E&color=EFE7D8&logoColor=1C1C1C"><img src="https://img.shields.io/npm/v/@modusensus/dsh-mneme?style=flat-square&label=npm&labelColor=B03A2E&color=111111&logoColor=FFFFFF" alt="npm version"></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/d18m/@modusensus/dsh-mneme?style=flat-square&label=downloads&labelColor=B03A2E&color=EFE7D8&logoColor=1C1C1C"><img src="https://img.shields.io/npm/d18m/@modusensus/dsh-mneme?style=flat-square&label=downloads&labelColor=B03A2E&color=111111&logoColor=FFFFFF" alt="npm downloads"></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI&labelColor=B03A2E&color=EFE7D8"><img src="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI&labelColor=B03A2E&color=111111" alt="CI"></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/tests-3503%20total-EFE7D8?style=flat-square"><img src="https://img.shields.io/badge/tests-3503%20total-111111?style=flat-square" alt="tests"></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/tests-3513%20total-EFE7D8?style=flat-square"><img src="https://img.shields.io/badge/tests-3513%20total-111111?style=flat-square" alt="tests"></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/license-MIT-EFE7D8?style=flat-square&label=license&labelColor=B03A2E&logoColor=1C1C1C"><img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square&label=license&labelColor=B03A2E&logoColor=FFFFFF" alt="license"></picture>
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome"></a>
</p>
Expand Down
9 changes: 6 additions & 3 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [Unreleased]

## [0.15.1] - 2026-11-13

- **验证基线只记 HEAD:工作区里还没提交的改动,提交之后就永远判不到了(#648)**。`verifiedBaseline` 一直是 `git rev-parse HEAD`,而 claims 的行号取自**磁盘上的内容**——工作区脏的时候,这两者不是同一棵树。机理很具体:改了某文件第 30 行但没提交,这时跑 T1,claim 按新内容算出的行号落库、基线却记成当前 HEAD;等这个改动被提交,`diffSince(HEAD, 新HEAD)` 从改动之后起算,diff 里根本看不到这一段,claim 会被判成「没被动过」,而且**这个窗口不会自愈**——改动永远落在基线之前。修法是**把「工作区状态」单独记一份**,而不是给基线换一种表示:T1 富化时用 `git status --porcelain -z --untracked-files=all` 取脏文件清单(`src/wiki/t0.js` 新增 `gitDirtyFiles`,与 `gitHead` 并列),连同 `verified_baseline` 一起落库(`choro_pages.verified_dirty_files`,幂等迁移补列)与 evidence 旁证,页面 frontmatter 只写一个布尔 `verifiedDirty`(清单太长,不该淹掉给人读的页头)。判定侧在 `src/wiki/claims.js` 的 `summarizeClaims` 里分两层:清单里**被 claim 引用**的文件判新理由 `uncommitted`(行号来自任何提交都证明不了的树),其余文件照旧走精确 diff——**不做整页降级**,一个无关脏文件不该杀掉整页的漂移检测。刻意不写 `git write-tree` / stash 式 ref:那会留下悬空对象或额外 ref,默认 2 周后被 gc 掉,等于把可解释性押在对象存活上;文件清单是纯文本,随库走。`.chorography/**` 从清单里滤掉——那是工具自己的产地(这一轮写盘本身就会让它脏),而 claims 永远不会指向它。**老数据不猜**:`verified_dirty_files` 为 NULL 的页 = 「不知道当时工作区状态」,不判 `uncommitted`、不回填,面板照旧按提交基线比对;空数组才是明确的「当时干净」,两者严格分开。展示面同步:面板的复核理由多一档中文/英文文案,页头 provenance 对脏工作区验证过的页改成「对着 {sha} 验证(当时工作区有未提交改动)」,不再冒充干净提交。回归测试 +4(脏工作区富化记清单且 claim 判 `uncommitted`、提交之后仍不自愈;只有被引用的脏文件才牵连;老数据 NULL 与空数组语义分开;旧库幂等补列),`test/wiki-provenance.test.js` 10/10。变异自证:去掉 `uncommitted` 覆盖 → 前两条红;把脏清单当空数组处理 → 老数据用例红;去掉 `.chorography` 过滤 → 清单条数断言红;迁移不补列 → 迁移用例红。

- **无会话身份的系统写入整片没进写入准入:三类出口在 `if (!sessionKey) return verdict` 处直接把判据跳过了(#663)**。`writeAdmission` 的判据挂在 `service.saveWithDedupe` 的汇流口上,而 `evaluate` 开头有一句「无会话身份 = 系统写入,不进预算」就 `return`——预算确实不该进,**判据却连跑都没跑**。于是 autoSummarize 的蒸馏产出、dream 的总览落盘、`bootstrapFromDirectory` 的整目录导入这三条路,密钥/PII 与空白噪声一次都没判过;`writeAdmission.enforce` 打开时它们照样无条件入库。更麻烦的是三条调用点各自把返回值丢了:`dream.js` 写总览的地方根本不看 `saveWithDedupe` 的返回,`summaryStored = true` 无条件置位(拒绝也被当成「已写入」,直接反映到运行结果与常驻条);`bootstrap.js` 解构出 `memory` 就往 `items.push({ id: memory.id })` 走,被拒时 `memory` 是 `null`,解引用抛错掉进外层逐条 catch,**拒绝理由与被拒条数一起丢**,只剩一条 warn。修法是把「会话时间线」和「判据」拆开:第 1 级判据(空白/噪声、密钥/PII)与会话身份无关,对所有写入照跑;命中且 `enforce` 时 `gate` 记 `null`(没有会话就没有预算维度)、拒绝照样生效;判据之后才 `if (!sessionKey) return judged;` 挡在预算/同话题冷却/内容哈希之前——这三样都要一条真实会话时间线做基准,硬凑一个假 `session_key` 会把真实会话的分布污染成两截,读审计的人再也分不开。`record` 的落行条件同步改成「放行档必须有会话且有 gate,拒绝档一律落行」:系统写入的放行不该留计量行(读不出分布,只撑大审计表),但**拒绝是一次真实的丢弃,必须留行**,`session_key` 记为 NULL。调用点各自补上拒绝处理:dream 只在非 `denied` 时置 `summaryStored = true`;bootstrap 解构出 `reason`、`denied` 计数与 `items.push({ action, id: null, title, reason })`,返回对象多一个 `denied` 字段——不再让 `null` 掉进 catch 把理由吃掉;standalone HTTP 那条路径的注释从「拦不到」改成「只过第 1 级」,显式 422 分支保留(落到 `toApiList([null])` 会变 500,把「被拒」伪装成「服务器出错」)。`sensitiveScanEnabled` 的文档口径一起校正:它覆盖所有写入的第 1 级,无会话身份的路径同样在内。**两处刻意没接**:`share-import.js` 走 `store.save` 而不走汇流口(`docs/SHARE.md` 把收包侧定成信任边界,要接得先改那条路,不在本件范围);`saveWithDedupe` 的 `existing` 合并分支在准入之前就 `return`,合并写入整条绕过闸门——这是另一条缺口,同样单独处理。**测试** 3,496 → **3,502**(+6):`test/write-admission.test.js` 覆盖三档系统写入(默认档不判不落行、观察档落行且 `session_key` 为 NULL / `metadata.gate === null`、拦截档 `denied` 且零行),以及 bootstrap 与 dream 两条真实出口的集成(被拒条数、`id: null`、无 summary 行、审计行带命中类型)。变异自证:把 `if (!sessionKey) return judged;` 挪回判据之前 → 4 红;`record` 恢复旧 guard → 拒绝档落行断言红;dream 改回无条件 `summaryStored = true` → 集成用例红;bootstrap 不接 `denied` 分支 → `report.denied` 与 `items[0].id` 断言红。

- **状态页「8 周沉淀」图看得懂了:图例补颜色色标,柱高的共同基准也写明白了(用户反馈)**。图上每周两根柱(琥珀=进来的、绿=稳住的),图例却只有四段纯文字「新增 / 巩固 / 归档 / 巩固·新增」,读者只能猜哪根柱对应哪个数字;「柱高按最近 8 周单周最高新增折算」这件事也一个字没提。现在图例每一项前面挂同色圆点(直接引用 `--mneme-sink-added` / `--mneme-sink-merged` 两个语义色,不另立颜色,避免与 donut 的组件色撞成两义),归档压根不给色块、只在数值后注明「未画入柱」——它本来就不进曲线、只报总量,给它一个色标(哪怕是空心点)就会长得像第三根柱的别名;图下补一行「柱高同基准:按最近 8 周单周最高新增折算」。`lib/client.js` 是独立手写的面板 bundle(`scripts/sync-lib.js` 只做 src→lib 拷贝、不碰它),所以改动只落在它自身与测试/CHANGELOG。回归锁加在 `test/client.test.js` 的沉淀图用例:两个色标必须引用语义色、归档项必须标明未画入柱。

## [0.15.0] - 2026-11-13
Expand All @@ -15,9 +21,6 @@
**测试** 3,496 → **3,499**(+3):`test/issue-692-default-on-reads.test.js` 三条——裸配置下 guarded 类型(`constraint`)的 merge 被 skip 且理由点名 `dreamMergeGuard`、显式 `dreamMergeGuard: false` 时同一条 merge 落库、装配判据三态(`{}` / `undefined` / `true` 为开,`false` 为关)。变异自证:两处各自改回 `=== true` → 2 红(默认开那两条),还原 → 3/3 绿;「改成恒真」会被那两条反向对照抓住。

- **知识库未启用时的空态文案指错了分组名(#668 尾巴)**。空态一直写「设置 → 记忆(mneme) → 功能 → 知识库」,但 v0.13 起 `group.ext` 是独立分组、标签是「扩展功能」(英文 `Extensions`),「知识库」只是组里的一项——照这句话去设置页找「知识库」分组会找不到。中英两版都改成「功能 → 扩展功能」并点名组内那一项,`test/client.test.js` 的文案锁同步跟上。
- **无会话身份的系统写入整片没进写入准入:三类出口在 `if (!sessionKey) return verdict` 处直接把判据跳过了(#663)**。`writeAdmission` 的判据挂在 `service.saveWithDedupe` 的汇流口上,而 `evaluate` 开头有一句「无会话身份 = 系统写入,不进预算」就 `return`——预算确实不该进,**判据却连跑都没跑**。于是 autoSummarize 的蒸馏产出、dream 的总览落盘、`bootstrapFromDirectory` 的整目录导入这三条路,密钥/PII 与空白噪声一次都没判过;`writeAdmission.enforce` 打开时它们照样无条件入库。更麻烦的是三条调用点各自把返回值丢了:`dream.js` 写总览的地方根本不看 `saveWithDedupe` 的返回,`summaryStored = true` 无条件置位(拒绝也被当成「已写入」,直接反映到运行结果与常驻条);`bootstrap.js` 解构出 `memory` 就往 `items.push({ id: memory.id })` 走,被拒时 `memory` 是 `null`,解引用抛错掉进外层逐条 catch,**拒绝理由与被拒条数一起丢**,只剩一条 warn。修法是把「会话时间线」和「判据」拆开:第 1 级判据(空白/噪声、密钥/PII)与会话身份无关,对所有写入照跑;命中且 `enforce` 时 `gate` 记 `null`(没有会话就没有预算维度)、拒绝照样生效;判据之后才 `if (!sessionKey) return judged;` 挡在预算/同话题冷却/内容哈希之前——这三样都要一条真实会话时间线做基准,硬凑一个假 `session_key` 会把真实会话的分布污染成两截,读审计的人再也分不开。`record` 的落行条件同步改成「放行档必须有会话且有 gate,拒绝档一律落行」:系统写入的放行不该留计量行(读不出分布,只撑大审计表),但**拒绝是一次真实的丢弃,必须留行**,`session_key` 记为 NULL。调用点各自补上拒绝处理:dream 只在非 `denied` 时置 `summaryStored = true`;bootstrap 解构出 `reason`、`denied` 计数与 `items.push({ action, id: null, title, reason })`,返回对象多一个 `denied` 字段——不再让 `null` 掉进 catch 把理由吃掉;standalone HTTP 那条路径的注释从「拦不到」改成「只过第 1 级」,显式 422 分支保留(落到 `toApiList([null])` 会变 500,把「被拒」伪装成「服务器出错」)。`sensitiveScanEnabled` 的文档口径一起校正:它覆盖所有写入的第 1 级,无会话身份的路径同样在内。**两处刻意没接**:`share-import.js` 走 `store.save` 而不走汇流口(`docs/SHARE.md` 把收包侧定成信任边界,要接得先改那条路,不在本件范围);`saveWithDedupe` 的 `existing` 合并分支在准入之前就 `return`,合并写入整条绕过闸门——这是另一条缺口,同样单独处理。**测试** 3,496 → **3,502**(+6):`test/write-admission.test.js` 覆盖三档系统写入(默认档不判不落行、观察档落行且 `session_key` 为 NULL / `metadata.gate === null`、拦截档 `denied` 且零行),以及 bootstrap 与 dream 两条真实出口的集成(被拒条数、`id: null`、无 summary 行、审计行带命中类型)。变异自证:把 `if (!sessionKey) return judged;` 挪回判据之前 → 4 红;`record` 恢复旧 guard → 拒绝档落行断言红;dream 改回无条件 `summaryStored = true` → 集成用例红;bootstrap 不接 `denied` 分支 → `report.denied` 与 `items[0].id` 断言红。

- **验证基线只记 HEAD:工作区里还没提交的改动,提交之后就永远判不到了(#648)**。`verifiedBaseline` 一直是 `git rev-parse HEAD`,而 claims 的行号取自**磁盘上的内容**——工作区脏的时候,这两者不是同一棵树。机理很具体:改了某文件第 30 行但没提交,这时跑 T1,claim 按新内容算出的行号落库、基线却记成当前 HEAD;等这个改动被提交,`diffSince(HEAD, 新HEAD)` 从改动之后起算,diff 里根本看不到这一段,claim 会被判成「没被动过」,而且**这个窗口不会自愈**——改动永远落在基线之前。修法是**把「工作区状态」单独记一份**,而不是给基线换一种表示:T1 富化时用 `git status --porcelain -z --untracked-files=all` 取脏文件清单(`src/wiki/t0.js` 新增 `gitDirtyFiles`,与 `gitHead` 并列),连同 `verified_baseline` 一起落库(`choro_pages.verified_dirty_files`,幂等迁移补列)与 evidence 旁证,页面 frontmatter 只写一个布尔 `verifiedDirty`(清单太长,不该淹掉给人读的页头)。判定侧在 `src/wiki/claims.js` 的 `summarizeClaims` 里分两层:清单里**被 claim 引用**的文件判新理由 `uncommitted`(行号来自任何提交都证明不了的树),其余文件照旧走精确 diff——**不做整页降级**,一个无关脏文件不该杀掉整页的漂移检测。刻意不写 `git write-tree` / stash 式 ref:那会留下悬空对象或额外 ref,默认 2 周后被 gc 掉,等于把可解释性押在对象存活上;文件清单是纯文本,随库走。`.chorography/**` 从清单里滤掉——那是工具自己的产地(这一轮写盘本身就会让它脏),而 claims 永远不会指向它。**老数据不猜**:`verified_dirty_files` 为 NULL 的页 = 「不知道当时工作区状态」,不判 `uncommitted`、不回填,面板照旧按提交基线比对;空数组才是明确的「当时干净」,两者严格分开。展示面同步:面板的复核理由多一档中文/英文文案,页头 provenance 对脏工作区验证过的页改成「对着 {sha} 验证(当时工作区有未提交改动)」,不再冒充干净提交。回归测试 +4(脏工作区富化记清单且 claim 判 `uncommitted`、提交之后仍不自愈;只有被引用的脏文件才牵连;老数据 NULL 与空数组语义分开;旧库幂等补列),`test/wiki-provenance.test.js` 10/10。变异自证:去掉 `uncommitted` 覆盖 → 前两条红;把脏清单当空数组处理 → 老数据用例红;去掉 `.chorography` 过滤 → 清单条数断言红;迁移不补列 → 迁移用例红。

- **跨类型同义双录:蒸馏提示词里「宁可拆成多条」把同一件事记了两遍,而三处去重各守一道类型闸(#687);实体表 75% 的实体只被提过一次、名字只按大小写敏感精确匹配(#688)**。两条 issue 报告的是同一个根病:**记忆的「类型」被当成了「对象身份」**。#687 上报者(xueyue33)给的分布很刺眼——他自己库里 `sim>=0.85` 的 81 对近重复中 **72 对跨类型**、`>=0.90` 的 7 对中 6 对跨类型;`sim>=0.88` 的 24 对里 21 对跨类型。四处夹缝逐个核过都成立:`src/lang.js` 的 `codingSummary` 明确让模型「宁可拆成多条」而 `pitfall`(现象+根因+规避)与 `constraint`(约束+来源)本就是同一结论的两种写法;`src/service.js:1480` 的 `findSessionDuplicate` 用 `store.list({ type: memory.type, source, limit: 200 })` 锁死同类型同会话;`src/dream/clustering.js:117` 的 `findPotentialConflicts` 首行 `if (memories[i].type !== memories[j].type) continue;`;dream 巩固提示词同样写着「仅合并同类型」。**我们不开常开类型闸**:本机活跃库 9,275 条带向量的记忆里跨类型 `cosine>=0.90` 共 45 对,逐对读正文后 **36 对(80%)确为同义,9 对合并会丢信息**(5 对时间序列/状态快照、2 对症状 vs 修复、1 对结论反转、1 对版本号不同)——也就是说「凡是跨类型高相似就合并」每 5 对要吞掉 1 对真互补的信息,而这类对只占活跃总量 0.97%,收益与风险不对称。落地的三层是**源头互斥 + 批内去重 + 存量一次性处置**:① `codingSummary` 提示词(中英双语)补一条互斥约束——同一结论只记一条,`pitfall` 与 `constraint` 不双写,时间序列/状态快照写进同一条;② 蒸馏落盘前做**批内**跨类型去重,只对 `pitfall↔constraint`、`history↔project` 两组生效(正是实测最集中的应用对:9 对 + 8 对),`planBatchCrossTypeDedupe(entries, { minSim })` 是纯函数——按相似度降序、每条最多被退休一次、不许链式藏行(A 取代 B 的同时 B 又取代过 C 会把 C 永久埋在链底);门槛 `BATCH_CROSS_TYPE_MIN_SIM = 0.85`(textSim,不是余弦,词面重合是这两组对的主特征)、`BATCH_CROSS_TYPE_MIN_CHARS = 24`、`BATCH_CROSS_TYPE_MIN_LEN_RATIO = 0.5`;③ 处置走 `store.supersedeChain` 只写 `superseded_by` / `superseded_at`,**保留正文更全的一条、被取代者正文零改写、不归档、不 merge**——判错时旧行仍在库里、显式搜索照旧读得到,且 `supersededIds()` 的 `JOIN ... AND w.archived=0` 让继任者一旦被归档旧行自动回到可见,等于自带回退。刻意**没有**复用 `src/dream/decisions.js` 的 `supersedeInfoFirstPair`:它走 `service.update` 会在正文尾部追加「已被取代」前缀,与「正文零改写」直接冲突。**默认开**(2026-11-13 拍板):这个开关存在的意义就是让新写入不再双录,默认关等于修了不生效,所以 `src/config.js` 给 `true`、设 `false` 是 kill switch,且关时审计行的 `batchDeduped` / `batchPairs` 两个键根本不出现——加开关之前的行为逐字节不变。两个键同时登记进 `settings.js` 的 `FEATURE_FLAG_BOOLEANS` 与 `FEATURE_FLAG_NUMBER_RANGES`,面板上能改。存量 17 对**不由代码处置**:只读脚本 `scripts/cross-type-dedupe-dryrun.mjs` 出清单(输出**禁止落在仓库内**,清单里含记忆原文),复核后由人决定、按纯 SQL 打链。

Expand Down
Loading
Loading