# Changelog

本文件记录本项目的所有重要变更。

格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)，
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

## [Unreleased]

## [0.11.1] - 2026-10-24

修一处只影响安装期的问题，并把 `0.11.0` 之后改的包描述带出来。插件行为、库结构、公开接口
都没变（`user_version` 仍是 5）。

### Fixed

- **`peerDependencies` 的范围把预发布版静默排除了**。原来两个宿主包都写
  `>=0.1.0-rc.2 <0.3.0-0`：DSH 自己的兼容性检查带着 `includePrerelease`，所以插件一直装得上、
  跑得通；但 npm 与 pnpm 的普通解析不带这个选项，`0.2.0-rc.1`、`0.2.1-rc.2` 这些版本会被判成
  不满足 —— 而当前的宿主运行时正是 `0.2.1-rc.2`，别人用 npm 直接装就会撞 `ERESOLVE`。
  范围改成按版本元组逐个列出预发布分支：
  `>=0.1.0-rc.2 <0.2.0 || >=0.2.0-rc.1 <0.2.1 || >=0.2.1-rc.0 <0.3.0-0`
  （实测：`0.1.0-rc.2` / `0.1.0-rc.6` / `0.2.0-rc.1` / `0.2.1-rc.2` / `0.2.5` 满足，
  `0.3.0-rc.1` 仍不满足）。

### Changed

- 包描述（`package.json` 的 `description`，也是 npm 与插件列表里显示的简介）改成一句讲清
  「存什么、怎么找回、数据在哪」，不再用「结构化、可检索」这类自评词。

## [0.11.0] - 2026-10-24

界面这一层第一次成形：设置页里多了入口，入口里分成管理与配置两页，配置页能改两项并立刻生效，
整库能带走一份。发布准备也在这版：模型默认从 ModelScope 取、失败再试官方源，插件声明与实现
对齐（唯一的出网是首次加载时下载权重），文档不再写项目状态。检索与写入行为不变，库结构不变
（`user_version` 仍是 5）。

本版是**首个对外发布版**。界面这批改动原记在 `0.10.0` 段，那个版本没有单独发布过，
随本版一起发出 —— npm 上从没有过 `0.10.0`，留一个空版本号只会让人以为它存在过。

### Changed

- **默认镜像从 `hf-mirror.com` 换成 `modelscope.cn`（魔搭）**：同一台机器直连实测，取 8 MB 模型分片
  三次，ModelScope 0.18 / 0.23 / 0.47 秒、hf-mirror 0.46 / 0.55 / 2.33 秒；连续取模型配置十次，
  两者中位分别是 0.07 秒与 0.29 秒；当时 `hf-mirror.com` 解析到 `160.16.86.14`（境外地址），
  `modelscope.cn` 解析到 `39.99.133.195`（阿里云）。两家的文件路径格式不同（ModelScope 多一层
  `models/`、分支名是 `master`），所以「下载源」从单个主机名改成「主机名 + 路径模板」。`encoderMirror`
  仍是字符串：填已知域名会连带用它的模板，填 `https://hf-mirror.com/` 即换回旧的镜像
- **模型下载按顺序试两条源**：先 `https://huggingface.co/`，失败再试第二条源，
  前一条成功就不再试。原来只有一个源，而「只用官方」在国内多数网络里不可达、「只用镜像」
  又在镜像失效时没有退路；两条都失败的那种失败方式，用户只能看到最后一句错误，分不清是网络
  断了还是源换了。`encoderMirror` 的语义随之变成「第二源」：留空等于默认镜像，填成官方地址
  就只试官方一次。上一版填了镜像的配置继续有效，只是现在会先试一次官方
- **失败原因逐源留档**：`lattice_ping` 的 `caches.encoder.attempts` 与规模路由的 `encoder.attempts`
  按顺序记下每个源的结果（成功与否、耗时、错误原文）；界面的配置页在两条都失败时逐条列出，
  并说明后果（召回退回词面）与下一步（离线放模型、或换 `encoderMirror`）
- **主仓库改成两条长期分支**：`main` 只放发布过的提交，开发与 PR 都进 `dev`，发版时才把 `dev`
  合进 `main`。这只影响协作方式，对使用者没有行为变化；发版 workflow 多一步断言「tag 指向的提交
  在 `main` 上」，防止在别的分支打 tag 之后把没进发布线的代码发出去
- **tag 从本版起带 `v` 前缀**：打成 `v0.11.0`，与 `release.yml` 的 `tags: ['v*']` 一致；
  `v0.11.0` 之前的 8 个 tag 没有 `v`（`0.2.0` 到 `0.9.1`），不追溯重打。这条写给维护者：
  打成 `0.11.0` 时 tag 会建成功、`git push` 也不报错，但 workflow 不触发，npm 上不会有这个版本
- **末端的网络声明改成实际行为**：`package.json` 里这条插件原来写「运行期不出网、模型离线放置」，
  而实现一直是首次加载时在进程内下载。现在声明与实现一致：唯一的出网是首次加载时约 23 MB 的
  权重下载，其余不联网
- **文档不再写项目状态**：README 顶部删掉「尚未发布」那段状态，换成三个徽章与一行代码来源说明
  （全部代码由 DeepSeek 生成）；`docs/RELEASING.md` 删掉「当前状态」一节。仓库里的文档只有发布
  之后才会被读到，状态写在里面，发布那一刻就已经是错的。项目状态一律记在本文件：npm 上
  `dsh-lattice` 仍是占位包 `0.0.0`（`0.0.0-stage` 已弃用，首个正式版本发布后 `latest` 会指向它）；
  远端仓库已建、公开、仍为空；发布用的 trusted publisher 已登记（GitHub Actions +
  `topkie/dsh-lattice` + `release.yml`）

### Added

- **界面的入口叫「lattice 记忆库」**：宿主同时装着几个记忆类插件时，「记忆管理」这种泛称看不出是谁的界面。
  改造之前没有这个入口 —— 这是本版本新增的一整个设置页
- **入口里分「管理 / 配置」两个选项卡**：管理页是筛选条、搜索、记忆列表与翻页；配置页是读数、
  配置表单、健康体检、维护动作与导出。分页的理由是两类使用频率差一个量级，堆在一页时列表会被
  诊断块挤到下面
- **配置页能改两项**：`viewBudget`（注入预算）与 `topN`（候选条数）在页面上有输入框，
  保存后下一次调用即生效、不用重启。写的是宿主的设置服务，不是插件自己的存储；宿主对这两个
  字段标了 `volatile`，就地换引用。其余十几项改了不会生效（要么在建连接时用掉、要么被写进
  上下文快照），因此只显示读数、不给输入框
- **整库导出**：配置页底部两个按钮，导出 JSON 或 Markdown。导的是每条记忆的正文与属性、
  主题、以及它们之间的关系；不含检索用的入口词与向量（能从正文重算，导出它们等于把派生物
  当原始数据），也不含日志（那是另一条已存在的导出）。格式靠 `?format=json|md`，缺省不猜 ——
  缺 `format` 或取值非法一律 400 并列出可选值。查看 `docs/ARCHITECTURE.md` 第八节
- **工具返回值末尾带一条下一步提示**：召回每五次提一次「还没有写入过记忆」，本进程写过之后
  不再提；写入时若同主题已有两条以上，回执说明这个主题现在有几条，并给出「改写用 `supersedes`、
  反查用 `lattice_recall`」两句。不是每次都提 —— 那句与本次召回的内容无关，每次都说会变成可以
  跳过的噪声；判空时也不提，库里没内容时模型可能只是在问常识，此时提示「该写了」会让它每次提问
  都写一条
- **配置页多一张「编码器状态」读数**：已加载 / 加载中 / 加载失败 / 尚未加载。它取的是最近一次
  加载的结果，刷新页面不会把 23 MB 的模型重新拉一遍

### Fixed

- **导出按钮点了没反应**：原来用 `window.open` 打开下载地址，宿主渲染进程不放行新窗口 ——
  既不下载也不报错。「导出日志」用的是同一手法，很可能一直是坏的。现在改成先 `fetch` 取回内容、
  再用一个带 `download` 的 `<a>` 交给浏览器存盘；文件名取自响应头 `content-disposition`
  （日期只有服务端那一侧算得准），两处导出共用同一段下载逻辑
- **认不出自己在宿主设置里的那一条**：宿主返回的字段描述是 `uid` + `refs` 的形状，
  字段名在根节点的 `dict` 里；原来只按 `schema.properties` / `schema.dict` 找，
  结果配置页一律报「宿主设置里没有本插件」。现在先按插件名匹配，再按字段名兜底
- **卸载后库连接不关**：插件被停用或热重载时只关了日志句柄，主库连接一直开着。后果有两个 ——
  Windows 上删数据目录报 `EPERM`（目录里残留 `lattice.db-wal` / `lattice.db-shm`），以及同一进程重载后
  写入仍落在旧库。现在卸载会关连接；换 `dataDir`、或换 `encoderModel` / `tauJoin` 这类影响运行时的
  配置时也会重开，不再沿用上一份配置的连接（`viewBudget` / `topN` 是可热改的，不触发重开）
- **编码器会话不释放**：换模型或重新加载时只把引用置空，onnxruntime 的原生会话要等进程退出才归还。
  同一份实测里，显式释放的稳态比不释放低约 40 MB。现在换模型与卸载都会调用包提供的 `dispose()`
- **编码器状态卡片的说明文字统一称呼**：卡片标题是「编码器状态」，第三行却写「模型没加载起来时
  召回只走词面」，同一个东西两个名字。改成「编码器没加载起来时召回只走词面」。只改文字

### 已知限制（首个对外发布版）

这一版不是「全部做完」。下面这份表把代码里**待评审的阈值**按它们在管线里作用的位置归成 11 类：
此前它们记在仓库内一份临时清单里（1 条缺测量条件 + 78 条没有按常量名断言的测试，按条目计 79 条），
清单已删除，改为对外可读的限制说明 —— 每类给出条数、影响与一条自查命令。

这 11 类只是**这 79 条所在的段**，不是 `--area` 的全部取值：全表的段名共 16 个，另五个（配置、目录、
召回判定、工具输出、迁移切分）的 `--unguarded` 都是 0 条、也不在 `--needs-review` 里 —— 它们的值
要么有测试按常量名钉住，要么已经复算过并记了测量日期。`--area` 打错段名会直接报错并列出可用段名 ——
不这样做的话，打错一个字的输出会和「这一类真的条条都有测试」长得一模一样。

两点读表须知。**其一**，标着 `--unguarded` 的类别合计 78 条，共同点是：没有测试按常量名断言它们的
默认值，所以改了默认值测试不会红，只能靠评审发现；这一层原来由门禁里的存量比对守着，现在换成了这张表。
**其二**，11 类合计 79 个名字，与 79 条一一对应。2026-10-23 给四条补上测量条件之后两个集合不再有交集
（`WRITE_COST_MEASURED_MS` 原先既没有测试、又缺测量条件，所以各记一次），`DUP_LEX` 也不再出现在
`--needs-review` 里，重排去重这一类因此是 0 条。

另：七个量（`tauJoin` / `tauLex` / `tauNew` / `tauLexOnly` / `DUP_LEX` / `DUP_TEXT` / `NEAR_COS_MIN`）在 2026-10-23 做过一次灵敏度实测 —— 建一个 98 条记忆的库，每个量取当前值附近几档看结果怎么变。七个都保持现值：五个给出了「当前值坐在什么位置上」（`NEAR_COS_MIN` 0.70 是零孤立点的最后一档；`DUP_TEXT` 的两类分布相差 0.68；`tauJoin` 换到 0.72 时主题数 50、0.84 时 86，`tauLex` 换到 0.02 时 49、0.10 时 74），两个测不出（`tauNew` 与 `DUP_LEX` 的触发分支在这批数据里一次都没走到）。表与判定见 `docs/THRESHOLDS.md` 第 8 节。

另：编码器下载失败时，`caches.encoder.attempts` 里只留错误的 `message`（例如 `fetch failed`），
不带底层原因 —— 连不上、超时、证书错、404 在记录里长一个样。自查命令：`node scripts/verify-encoder.mjs`。
要细分原因，看 `attempts[].host` 判断是哪个源失败，再手工访问那个地址；需要 `cause` 只能自己加日志。

| 分类 | 影响 | 自查命令（在仓库根跑） |
|---|---|---|
| 落位判据（9 条 = `--unguarded` 8 + `--needs-review` 1） | 写一条记忆时「并入已有主题还是新建」的判定线与候选规模。三条并列阈值只在一次内部中文标定里定过，登记项缺编码模型与语料规模 —— 换编码模型或换领域时要重标，但仓库里推不出怎么重标 | `node scripts/print-thresholds.mjs --unguarded --area=落位`（8 条）；`node scripts/print-thresholds.mjs --needs-review`（1 条，段为落位） |
| 入口抽取与词项（20 条） | 哪些词能成为入口、词项权重、学习冷却与遗忘。含两个非纯数值常量（一个内联表达式、一条判据线），扫描器按定义处登记，不按数值。20 条都没有按常量名断言的测试 | `node scripts/print-thresholds.mjs --unguarded --area=入口`（20 条） |
| 召回融合（8 条） | 三路各取多少条、RRF 的平滑常数、候选上限。改小会漏召，改大拖慢并稀释注入预算 | `node scripts/print-thresholds.mjs --unguarded --area=召回融合`（8 条） |
| 注入组装（9 条） | 每段的字符预算与条数上限。改小会少给内容，改大可能超出宿主给的空间 | `node scripts/print-thresholds.mjs --unguarded --area=注入`（9 条） |
| 写入与切分（8 条） | 审核扫描的时间预算与条数、长文切分的段数与长度、正文最大字符。只在开发机的规模上试过，超大文本或小内存机器上的行为没测 | `node scripts/print-thresholds.mjs --unguarded --area=写入`（8 条） |
| 重聚类（6 条） | 近邻数、最小簇、桥边、批大小与计划延迟。整理的结果与耗时随之变；取值只在 1374 条基准库上定过 | `node scripts/print-thresholds.mjs --unguarded --area=重聚类`（6 条） |
| 图扩展（5 条） | 近邻边数、余弦线、出度上限、声明边的置信度与类型长度。库越大，扩展耗时与孤立率越依赖这几条 | `node scripts/print-thresholds.mjs --unguarded --area=图扩展`（5 条） |
| 降级日志（8 条） | 队列长度、导出上限、单文件与总量上限。决定日志占多少磁盘、失败窗口里丢多少条 | `node scripts/print-thresholds.mjs --unguarded --area=降级日志`（8 条） |
| 资源与等待（5 条） | mmap 与页缓存比例、写锁等待的倍数、一次写入的实测耗时。小内存机器上页缓存会变小，等待时间也跟着变 | `node scripts/print-thresholds.mjs --unguarded --area=资源`（5 条） |
| 重排去重（0 条待评审） | `DUP_LEX` / `DUP_TEXT`：同一来源内按词面重叠判重复的线。两个都有测试按常量名断言默认值（`test/search.test.mjs`），测量条件也补上了记录，只剩测量日期缺失（见下节）。仍值得留意：换语言、换切分粒度时要重看 | `node scripts/print-thresholds.mjs --unguarded --area=重排去重`（0 条） |
| 显式删除（1 条） | `FORGOTTEN_SCAN_LIMIT`：一次删除扫描多少条。库很大时，一次调用可能漏掉超出上限的条目，需要再跑一次 | `node scripts/print-thresholds.mjs --unguarded --area=显式删除`（1 条） |

门禁第 6 项 `thresholds-review` 现在报的就是上面这两组条数（`--check-review` 打印当前
needsReview / unguarded 条数后正常退出），不再与一份存量文件比对；新增数值常量没登记仍由门禁
第 5 项 `thresholds` 当场判红。

#### 测量日期缺失（8 条，跨上面多类）

`node scripts/print-thresholds.mjs --undated` 报出 8 条可复算、但没有记录测量日期的阈值：
`DEFAULT_THRESHOLDS.tauLexOnly`（落位）、`DEFAULT_KNN_K`（重聚类）、
`NEAR_K` / `NEAR_COS_MIN` / `GRAPH_INFER_MAX_ATOMS`（图扩展）、
`DUP_LEX` / `DUP_TEXT`（重排去重）、`WRITE_COST_MEASURED_MS`（资源）。

2026-10-23 补上了四条（`tauJoin` / `tauLex` / `tauNew` / `REFERENCE_QUANTILE`）：它们的测量脚本都还在仓库里，
重跑一遍数字与登记项记的一致，就把当天作为复算日期写进去。剩下这 8 条不补，原因分两种：
`DEFAULT_KNN_K` / `NEAR_K` / `NEAR_COS_MIN` / `GRAPH_INFER_MAX_ATOMS` 的来历是一次性探针，脚本没有留在仓库里，
重跑不了；`DUP_LEX` / `DUP_TEXT` / `WRITE_COST_MEASURED_MS` 今天能算，但算出来的口径与原记录不同
（词面重叠按多集算是 0.708、原记录 0.64；写入耗时在内存库上是 0.004 ms/行、原记录 3.5 ms 含 WAL 与磁盘），
拿不同口径的数冒充原测量没有意义。仓库的规矩是「原记录没有就留空，不猜」——提交日期不是测量日期。

其中 6 条已经按段落在上表的类里；`DUP_LEX` 与 `DUP_TEXT` 不在这 79 条里 —— 它们有测试按常量名
断言默认值、测量条件也齐，只缺日期。

#### 留在登记表里的 12 条（跨上面多类）

`node scripts/print-thresholds.mjs` 的「留在表里的条目」一节报出 12 条：对象组 8、内联 1、
非数值 1、静态读不出 2。这些值没有独立常量可挂 `satisfies`，只能在表里登记。其中对象组与
「静态读不出」这 10 条能靠重构消掉，但要连同测试断言一起做 —— 拆开后这些子键都没有测试
钉住默认值。

## [0.9.1] - 2026-10-24

0.9.0 的修正版：三处都是文档与元数据的修正，运行行为不变。

### Fixed

- **Node 最低版本从 22 收紧到 22.13**：`node:sqlite` 在 v22.5 引入，但到 v22.13.0 才不再要求
  `--experimental-sqlite` 标志。22.5–22.12 上装得上，一启动就 `ERR_UNKNOWN_BUILTIN_MODULE`。
  `engines`、README 与开发指南的要求已同步。这条改动在 0.9.0 之后才提交，所以记在这里而不是 0.9.0 段
- **删除一份查不回来的基线结果文件**：它在 bench 结果目录下，文件名是被测提交号，历史整理后那个号已经
  查不回来（`git cat-file -t` 报 `fatal: Not a valid object name`，全历史零匹配），文件里还有内部用语。
  按 `docs/BENCHMARK.md` 自己的「不写查不回来的 sha」删除文件 —— 这条记录本身也不写出那个号，
  数字保留在文档里，第八节写明为什么没有结果文件
- **README 把模型下载来源与镜像说明并到安装一节**：权重不在 npm 包里，第一次加载才去 `huggingface.co` 取；
  国内网络实测解析到 `31.13.95.17`、连接 21 秒超时，编码器停在未加载、召回退回纯词面，与另两种原因
  （`PATH` 里没有 node、构建脚本被包管理器拦下）表现相同。镜像配置项与判断方法原来相隔 37 行，现在相隔 3 行

### 说明

- `0.9.0` 的 tag 指向提交 `adb80e7`（tag 对象是 `e610965`，`git rev-parse 0.9.0` 得到的是后者），
  不包含上面三处。要这三处修正请用 `0.9.1`

## [0.9.0] - 2026-10-23

### Breaking

- **模型侧工具从 6 个减到 5 个**：`lattice_feedback` 的定义还留在 `src/tools/index.ts`（`FEEDBACK_TOOL_SPEC`），
  但不再注册进工具表。它写的是「模型判断哪条有用」这类标签，而判定线自 0.5.0 起只吃分布、不吃标签；
  整条链的另一端（样本库 871 行、四道闸、重标、切点扫描）随之拆除。实测依据：真机样本库自 10-18 起
  只积累到 1 条（`source=feedback` 1 条、`source=auto` 0 条），四道闸里「正样本 ≥ 10」那道永远不满足，
  重标从未发生
- **删除 `samplesEnabled` 配置项**：它只控制样本的自动收集，样本库删除后没有作用。旧配置里留着它不报错，
  但不再有任何效果
- **`lattice_ping` 不再报 `samples` 字段**：一个永远为 0 的字段会让人以为样本还在收集、只是碰巧没有
- **数据目录不再创建 `samples/`**：`<dataDir>` 下只有 `lattice.db` 与 `logs/`
- **删除 `meta` 表**：0.5.0 把它留给外部标定包的校准快照（包在同一个事务里写 5 个键，写前要求表已存在）。
  标定功能退役、标定包归档后，它没有任何读写方；现在新建的库不再有这张表。将来要重新标定，
  得先改标定包的存储、再单独加一张表 —— 跨包契约不能由本仓库单方面恢复

### Added（文档）

- `docs/INTEGRATION.md` 新增「调用与副作用的三条纪律」（判断类工具只收语义位置、跨会话无意义的数据不落库、
  写是副作用时须写明失败方向），每条带一个反例 —— 这三条与反馈工具是否启用无关，是留下的判断
- `docs/ARCHITECTURE.md` 的反馈一节改写为「反馈（工具定义保留，当前未启用）」，写明删它的直接原因与重启条件

## [0.5.0] - 2026-10-22

### Breaking

- **新建的库是结构版本 5，旧版本库不再支持打开**：打开结构版本 1–4 的库会直接报错，
  提示它来自未公开发布的构建、没有迁移路径，并给出「从备份恢复，或用建出这个库的那一版导出内容后重新导入」。
  此前每次新建库都在重放一段历史迁移脚本，那条链因此不能删也不能改；改成只保留一份当前结构后，
  结构与版本号可以用一条断言核对（打开时比对库里的对象与代码里的建表语句，对不上就报出缺了哪些）
- **元数据从一张 key-value 表改成四张专用表**：构建信息、重聚类状态、校准检查状态、待切分队列各自成表，
  有列类型、有主键、队列行带外键（删掉一条记忆，它的队列行跟着消失）。此前的 key-value 表里
  这些状态混在一个列里，读改写整份清单，一处写坏就全份读不出来。那张表此后的唯一写入方是外部标定包的
  校准快照，标定包在 0.5.0 退役后这张表没有读写方了，留着等下一次结构变更时删

- **召回输出的形态换了，旧的三段与三档没有了**：注入段只剩【入选】一段，【入口】与【路】两段取消；
  按「全 / 精简 / 摘要」三档决定给多少的规则取消，改成按字符预算一条条装到装不下为止
  （旧形态按 55/20/25 分三段，实测只用到 76% 预算 —— 约 1400 字符既没给锚、也没被别的段用上）；
  「最高相似度 0.74」这类绝对分数与「可能不相关」的提示都不再出现。理由：绝对分数不可跨语料标定 ——
  三个中文语料族各 1374 / 1346 / 1496 条记忆，同一套人工提问扫出的最优切点差 0.11，换语料族动 0.075–0.113；
  同一份 12–96 档的发布值与复现值方向相反（发布值随规模上升 0.466→0.514，复现值随规模下降 0.5594→0.5331）。
  代之以本库内的相对量：48 条与库无关的内置问句在本库各查一次，取各自最高余弦的 90 分位作参考线

### Changed（召回机制）

- **判定改成相对分位**：一条查询的最高余弦低于参考线就判「库里没有针对这个问题的内容」，一条记忆行都不给。
  交互式会话里越线率实测 10.4%（48 条问句交叉验证 5/48），三库判空率 96% / 100% / 83%
- **排序改成纯余弦**：此前排序用 RRF 混合分（`w/(RRF_K+i+1)` 逐路累加，top1 恒为 1/61，只携带「命中几路、排第几」），
  而判定另用余弦，同一结果集两套刻度。现在 RRF 只负责合并通道，排序与判定都用余弦
- **归属加权删除**：主属 > 仅次属 > 仅图路的乘数只改先后不改集合，20 条提问 × 3 个语料的四个指标逐位相同
- **重复压制改成「同一来源 + 文本重叠」**：此前按主题分桶、用余弦 ≥ 0.92 判重复。同一文档的条目落在不同主题的
  占 59%–90%，按主题分桶压不住；同文档对的余弦（0.62–0.78）与跨文档对（0.48–0.63）大面积重叠，余弦判不出重复。
  改用 3-gram 覆盖度 ≥ 0.7，逐字重复是 1.000、同文档不同小节是 0.242–0.573。来源为空一律不参与比较 ——
  空桶会让整库互比，实测把「系统代理必须走本地端口…」与「系统代理配置本地端口…」判成重复（覆盖度 0.800）
- **标定包退役**：`lattice-calib` 从依赖里移除。它提供的「按库规模插值的精简档下界」随三档判定一起取消；
  判定用的相对分位是运行时派生量，落库会造出第二个会漂的副本，所以按 atom 行数缓存在内存里，库一变整份重算
- `lattice_ping` 回执删 `calibration` 字段：算参考分布要编码 48 条问句（约 1 秒），健康检查不该这么重

### Added

- `lattice_write` 新增 `unsupersede`：撤销一次改口 —— 传被取代的那条 id，把它的「已被 #N 取代」清掉。
  只清标记，不删行、不改内容；本来没有被取代时如实说明，不报错。与 `supersedes` 互斥，同一次调用只能给一个

### Changed（写入接口）

- `edges` 只收 `related` / `refines` / `duplicate` / `requires` 四种关系。写 `supersedes` 会被拒并说明
  「取代请用写入参数 `supersedes`；`edges` 里的 `supersedes` 不生效」—— 此前两条路都收，
  而只有顶层参数会真的改变去重状态，走 `edges` 那条只记一条边，不生效也不报错
- `lattice_write` 新增可选参数 `source`：同一篇材料切出来的几条传同一个值。来源相同、正文的
  3-gram 覆盖度达到 0.7 的两条在注入段只留一条（留余弦高的那条），段末写
  「另有 N 条与同源内容重复，未列出」；不传就不压 —— 没有来源证明不了同源。此前这个分桶依据
  只有导入脚本在填，工具写入的条目全是空来源，同源压制在工具写入的库上从不生效

### Docs

- 新增 `docs/THRESHOLDS.md`：代码里那些数字的来历怎么登记、四条查询各查什么、这套检查查得出什么查不出什么

### Fixed

- **被取代的条目不再作为答案出现**：`superseded_by` 指向的旧条在召回结果里让位给新条 ——
  条件是「新条也在最终结果里」。新条不在结果里时保留旧条，注入文本给一句
  「另有 N 条已被取代，未列出」。此前这一列只影响「已被 #N 取代」这行标记、不起过滤作用：
  真实库 366 条里 24 条被取代的记忆仍照常参与召回
- 显式检索（列表、界面搜索、展开）不受影响，仍能看到被取代的条目
- 上面这条在 49 对历史版本上实测过（人为构造的「旧知识 → 新知识」，98 条记忆）：旧条出现率 49/49 → 1/49；
  拿旧陈述当查询时新条排第一的比例 **0/48 → 39/48**。代价是注入字符中位从 4621 涨到 4916（+6.4%），
  花在「替换品也不在结果里、放回并标注」的旧条上，每查询中位 5 条、最多 10 条

### 已知限制（0.5.0）

这一版不是「全部做完」。下面是使用者会遇到的限制，每条给出影响与一条自查命令。

#### 检索质量只在中文上测过

基准用的是内部中文库：1374 条记忆、44 条中文查询（其中 20 条有目标答案），Recall@5 0.75（15/20）、
Recall@12 0.95（19/20）。仓库里只有这一份结果文件，也没有 LongBench 之类的公开基线。
换成新机制后另测过三组（各 20 条人工提问，语料 1374 / 1346 / 1496 条记忆，口径是「目标文档里至少一条记忆进了注入段」）：
预算 6000 时目标文档命中率 90% / 100% / 95%，注入字符中位 5967 / 5988 / 5988；预算 5000（0.5.0 的默认）时
命中率 90% / 100% / 90%，注入字符中位 4968 / 4988 / 4988。**降低预算的代价只在 Vue 上体现：掉 5 个百分点**，
技术笔记与新闻持平；再降到 4000 技术笔记会掉到 80%。要换回 6000，改 `viewBudget` 即可。
中文以外没有数字可引用：换成英文或日文语料，参考线会怎样、分位判定是否还成立，都没测过。

自查：`bench/` 下的结果目录里应当只有一个结果文件。

#### 编码器不可用时是静默降级

模型下载失败或加载失败都不报错，召回退回纯词面，回复里多一行「（编码器不可用，本次为纯词面召回）」。
而 `lattice_ping` 的 `encoder` 字段仍然报出模型名，单看它看不出失败。

自查：先召回一次，再看 `lattice_ping` 返回的 `caches.encoder.state` —— `ready` 走向量、
`unavailable` 已降级、`idle` 还没试过、`off` 是配置里关掉了。

#### 判定用本库的相对量，边界上的真内容会被判成没有

判「这次有没有内容」不看绝对分数，看一条查询的最高余弦有没有超过「48 条无关问句在本库的最高余弦 90 分位」。
参考线随库现算，所以换语料不用重标；代价是两种情形会判错：

- 库里讲的正是日常生活（那 48 条问句的主题），参考线被抬高，边界上的真内容会被判成没有
- 库少于 48 条记忆时不判（判了也不可靠），回执明说「库里只有 N 条，判定还不可靠」

参考线超过 0.65 时标定判据本身失效（三个自有语料只有 0.484–0.571，合并成 4216 条后 0.540–0.585，都没有触到）。
要引入比这三个更宽的语料（百科、全网新闻）才可能触到，那时需要重新看这条线。

自查：召回一次，看回执第一句是不是「库里没有针对这个问题的内容。」；再 `lattice_list` 看首行的 atom 数。

#### 两个实例同时写，后到的会失败

后到的写入等约 5.6 秒后收到 `database is locked`，工具调用直接失败，不重试。读不受影响。

自查：两个窗口同时各写一条，后写的那条会报 `database is locked`。

#### 运行环境的两个边界

- `engines.node` 写的是 `>=22`，但 `node:sqlite` 到 22.5.0 才有，22.5 到 22.12 还要加
  `--experimental-sqlite`，22.13.0 起才免旗标。22.5 以下起不来。
- 第一次加载编码器要联网下载约 23 MB 的模型文件，下载失败即降级（见上一条）。

自查：`node -p "process.versions.node"`；断网后召回一次，看是否出现降级提示。

#### 降级日志只记事件名，不记原文

日志表里只有事件名与错误类名（例如 `reason=TypeError`），查询文本、记忆 id、模型名、错误原文
都不落盘。排障时只能知道「失败过」，不知道失败在哪一步。

自查：数据目录下的 `logs` 子目录里有 `lattice-log.db`，用 node 打开查 `log` 表。

#### 整理（重聚类）要手动触发

在设置页点「整理」才开始，点下去之后 3 秒内可以取消。没有定时自动执行，
库变大之后不会自己重新整理。

自查：等一天看 `lattice_ping` 的 `caches` 与日志里有没有自动整理记录（应当没有）。

## [0.4.1] - 2026-10-21

修 0.4.0 的一处回归与两处文档缺口。**运行时行为一处未变**：阈值取值、工具参数与界面都没动。

### Fixed

- **三个来历查询在 0.4.0 上运行即报错**（`--needs-review` / `--unguarded` / `--undated`）：
  查询的分派早于 `ENTRIES` 的初始化，触发暂时性死区。0.3.0 可用；回归由「查询读两种来源」
  那次改动引入。0.4.0 的 `CHANGELOG` 与 `docs/DEVELOPMENT.md` 把三条命令写成可用，是不准确的
- **三个查询函数的默认参数指向只剩 20 条例外的 `THRESHOLDS`**：调用方忘传条目集合时会静默
  漏掉搬到定义处的 117 条，结果变短、看起来像缺口都补完了。改为必填，漏传由类型检查报错
- 打印摘要把三类互斥的来历写成「可复算 22 行（其中公式 10 行）」：22 已不含公式 10，改成并列
- 评审基线里一个条目的理由含两处替换字符（`60 这��默认值`）

### Added

- 门禁增加一条来历查询步骤（9 项 → 10 项）：`--check-review` 只触达查询函数、不触达命令行
  分派 —— 0.4.0 上三条命令全崩，门禁 9 项全绿、452 条测试全过
- `CONTRIBUTING.md` 补一条约束：动了日志事件必须同步 `src/log/policy.ts` 的白名单，
  写门禁步骤名 `log-boundary` 而不是编号

### Changed

- `src/thresholds.ts` 的 `ThresholdValue` 联合类型改名 `EntryValue`，与 `threshold-types.ts`
  里带来历参数的泛型 `ThresholdValue<K, T>` 区分

## [0.4.0] - 2026-10-21

本版是阈值记录形态的改造：**运行时行为一处未变**（阈值取值、配置项、工具参数、界面都没动），
改的是「来历写在哪里」与「登记表还剩什么」。

### Added

- `ThresholdValue<K, T>` 类型（`src/threshold-types.ts`）：常量在定义处用
  `satisfies ThresholdValue<'recheckable' | 'chosen', {...}>` 带上自己的来历，编译后不留运行时代码
- `--doc-keys` 命令：按设计说明分组列出「文档键名 → 常量 → 文档默认值」；文档改了一条后，
  用它定位受影响的条目。清单现生成、不落文件
- 登记表条目的 `unmovable` 理由（内联 / 非数值 / 静态读不出 / 外部包 / 派生自外部包 / 对象组）
  与对应断言：往表里加一条，先得回答它为什么搬不动

### Changed

- **阈值来历从登记表搬到常量定义处**：116 条从登记表搬到各自常量的 `satisfies` 类型参数里，
  值与来历在同一处；另有 1 条新常量（`CONFIG_TOP_N_DEFAULT`）在定义处带了来历，合计 117 条；
  登记表从 136 条降到 20 条，剩下的都是「一个条目对应不到一个模块级常量 + 一个数字」
- 类型参数里可以写：`area` / `failsWhen` / `affects` / `display` + `unit` / `guardedBy` / `diverges` /
  `refs` / `otherSites`，以及设计对照的 `doc`（短名）/ `docKey` / `docDefault` / `docRange` /
  `docScale` / `docKeys` 与 `zod` 指针
- `divergence` 只存原因，文档值从 `design.default` 读 —— 同一件事实不再有两份记录
- 三个来历查询（`--needs-review` / `--unguarded` / `--undated`）与评审基线读「表 + 定义处」两种来源；
  以前只看表，搬走的条目会从结果里静默消失
- `docs/DEVELOPMENT.md` 的阈值登记一节重写：来历写在常量定义处；`display` 改成模板（`{v}` 与 `{NAME}`），
  单位换算用 `unit`
- 打印结果多了「留在表里的条目」一节，按 `unmovable` 分组

### Removed

- 登记表里的 `design` 与 `zod` 抄写：搬到类型参数后，只在那 20 条例外里保留
- 打印脚本里的 `displayNumber`（`display` 改成模板 + `unit` 换算之后不再需要）

### 已知局限

- **20 条留在表里**：对象组 11（子键的值有别名、有 `Infinity`，或设计对照按「一行三键」写）、
  外部包 3、非数值 2、静态读不出 2、内联 1、派生自外部包 1。其中对象组与「静态读不出」这 13 条
  能靠重构消掉，但要连同测试断言一起做 —— 拆开后这些子键都没有测试钉住默认值
- 6 条可复算的阈值仍缺编码模型名与语料规模（与 0.3.0 相同），等开源发版前的重标补上

> 勘误：本节两个数字已过时，且「等重标补上」不会再兑现 —— 重标链路整体移除，那两个字段
> 不会自动补齐。当前值见 `[0.11.0]` 段的「已知限制」一节：留表 12 条、`--needs-review` 1 条、
> `--undated` 8 条。历史段的数字保持原样，不改写已发布版本的记录。

## [0.3.0] - 2026-10-21

本版是未开源前的代码风格治理：**运行时行为只有一处增加**（`lattice_ping` 多了 `caches`），其余是记录格式与工程约束。

### Added

- `lattice_ping` 新增 `caches`：五处模块级缓存的**实时占用**（向量缓存按 atom、topic 词面指纹按 topic 并附 terms、
  查询侧词表按 term、重聚类前的归属快照按 atom、编码器报加载状态）。只读、不触发重建、不发 SQL，
  也不设容量上限 —— 先有读数才谈得上「多大算大」
- 阈值登记表的**三个来历查询**与**评审基线**：`--needs-review`（可复算但缺编码模型与语料规模）、
  `--unguarded`（没有测试按常量名断言默认值）、`--undated`（没有记录测量时间），以及 `--check-review`
  （基线只许变短、每条带理由、名字必须在冻结名册里）

### Changed

- **136 条阈值登记项全部改成「来历条目」**：`recheckable`（有测量、可复算，必填 `tool` 与 `finding`）或
  `chosen`（没有测量能给出它，必填 `why` 且**不许出现测量数字**）。`chosen` 的第二参数是字符串，
  传对象在编译期报错
- 三条常量改成由别的常量算出：每主题条目数 = 抽取配额的一半、重标取样上限 = 样本库容量的五分之一、
  日志总量上限 = 保留文件数 × 单文件上限（三处取值不变）
- `display` 改成按值现取（22 处）：改常量后打印出的数字跟着变，不再是写死的字符串
- 记录里的「文件:行号」改成「文件 + 符号名」，打印时核对符号存在
- 阈值表的 zod 对照字段改成**现读**：表里只记字段名，默认值与范围从 `src/index.ts` 的 schema 读，不再抄一份；
  登记的 zod 字段必须在 schema 里存在，且 `.default()` 必须是常量引用（写字面量即红）
- 注释与文档里不再引用内部流程编号（82 处）：读者查不到的出处等于没有出处

### Removed

- 阈值登记表里的 `evidence` 与 `source` 两个过渡字段（解释并入 `finding` / `why`，来源指针并入 `tool` / `at`）
- 18 处手写的「设计说明 §7 没有这个键」，改由打印时按字段缺失自动生成

### 已知局限

- 6 条可复算的阈值仍缺编码模型名与语料规模（`tauLex` / `tauNew` / `tauLexOnly` / `DUP_LEX` /
  `WRITE_COST_MEASURED_MS` / `RECALIBRATE_DRIFT`），记在评审基线里；开源发版前的重标会一并补上
- 重聚类前的归属快照是模块级缓存、不按数据库连接区分，换库后 `lattice_ping` 可能报上一个库的条数

## [0.2.0] - 2026-10-20

### Added

- **[设计原则](docs/PRINCIPLES.md)**：根本约束——职责边界（模型管内容、插件管形式）、
  四条行为边界、数据边界与刻意放弃项。与之冲突的改动一律不做
- 写入管线：两票落位（向量余弦 + 词面重叠）、主题增量维护、入口三级权重
  （declared / extracted / learned）
- 召回管线：四路融合（向量 / 入口 / 正文 / 图）+ 归属加权，三段式注入组装
  （锚 / 入口 / 路）与按顺序降级的裁剪
- 宿主工具：`lattice_ping` / `lattice_write` / `lattice_recall` / `lattice_expand` / `lattice_list` /
  `lattice_feedback`
- **校准样本库**：模型对召回的反馈（`lattice_feedback`，`ref` 是上次召回【锚】段的 1-based
  序号）与自动收集的负样本，写入一个**独立的** SQLite 文件（`<dataDir>/samples/recall-sample.db`），
  只存查询向量与命中的 atom id，**不存查询原文**；保留 90 天且总量 1 万条，「明显无关」一档另设
  2000 条上限，换编码模型时旧样本标失效不删。样本不写 `edge`、不写主库、不触发重标；
  写入经内存队列延后落盘，召回路径上没有同步 IO。`lattice_ping` 报样本条数；
  配置项 `samplesEnabled`（默认开）可关掉自动收集，显式反馈不受影响
- **标定工具链独立成包 `lattice-calib`**：伪查询抽样、库内几何量 S、切点扫描、τ/S 越界判定、
  重标与标定快照写入从插件剥离，作为一个独立仓库发布、按 semver 被插件依赖。
  插件运行时因此**只依赖本项目自研的这一个包**（该包自身零第三方依赖）——
  「零第三方依赖」指这一点，不是「没有任何依赖」；路径：锚点表（库规模 → 档位下界）
  只有一份，在 calib 里，插件只做转发
- **降级日志库**：降级痕迹写入一个独立的 SQLite 文件（与记忆库分离、不共享生命周期），
  只记白名单内的事件与字段（时间 / 等级 / 事件名 / 数值 / 错误类名或码），查询文本、
  atom id、路径与模型名不写入。内存队列批量落盘（上限 200 条、满了丢最旧），
  单文件 4 MB 轮换、最多保留 5 个；写失败只降级到内存并限频记 `log.write_failed`，
  不阻塞召回。设置页可导出 CSV（四列，超过 10 万行或 10 MB 截断并标记）与清空
  （二次确认，只删日志文件，不碰主库）
- `lattice_list`：列目录（id / 类型 / 摘要 / 所有权 / scope / 主题 / 取回次数），分页并可按
  条件过滤 —— 补上「库里有什么」这个此前无法被回答的问题
- **声明关系边**：`lattice_write` 新增 `edges: [{type, target}]`，把「这两条记忆是什么关系」
  写成 `origin = declared` 的边，与写入**同一个事务**落库。type 不做白名单，只校验长度
  （≤32 字符）与字符集（小写字母、数字、下划线），推荐 related / refines / supersedes /
  duplicate / requires；一条记忆最多 16 条，不合法的边**只跳过并回执**，记忆照写。
  `related` 进图路（库里只有声明边时图路也有输入），`supersedes` / `duplicate` 不进；
  召回元数据把声明边与推断边**分开报数**，注入的路段按 origin 分标；全量重建只删
  `origin = 'inferred'`，声明边一行不动；声明边**不写样本库**
- 迁移与评测脚本：现有卡片迁移、等 token 召回对比、多组真实模型验证
- 存储层：单库 SQLite schema，含 atom / topic / entry / edge 等表与两层检索索引
  （正文 trigram + 入口 bigram）
- 文本工具：CJK bigram 切词、码点安全的摘要抽取、Markdown 清洗、词面 Jaccard
- 向量工具：float32 BLOB 编解码、点积 / 范数 / 归一化 / 余弦
- 插件骨架：可被宿主加载的插件契约与健康检查工具
- 状态信号：`atom.origin`（用户 / 模型 / 导入）、`superseded_by` 的写入与展示、召回命中计数。
  锚段行按需带状态标记；`lattice_write` 用 `origin` 与 `supersedes` 表达所有权与取代关系

### Changed

- 存储层：schema 改为**版本化迁移**（`MIGRATIONS` 按版本递进），新增 `meta` 表承载库自描述
- schema v3：`atom` 增加 `origin` / `recall_hits` / `last_recalled_at`。迁移按 `source` 推断
  已有行的所有权归属（导入路径带 source，运行时写入不带），追溯不到的一律留在默认值
- 标准验证库：基准库现在自带「我是用哪套规则生成的」，生成方式固定为
  `scripts/build-baseline.mjs`；入口规则的改动必须同步 `ENTRY_RULE_ID`
- 新增 `scripts/verify-recall-baseline.mjs`：基准库上的召回回归，入口规则改动后必跑
- 注入按**匹配质量分档**（完整 / 精简 / 摘录）：无关查询的注入字符从约 4968 降到 995~1056，
  而 gold 的 Recall@12 保持 95.0%。低相关提示线由 0.68 改为 0.45 —— 原值实测永不触发

### Fixed

- 迁移只会「一次建全部表」：库已是 v1 时重跑建表语句会因表已存在而失败，升级路径实际不存在。
  改为按版本递进，只跑缺的那几步
- 入口词混入数字与符号片段（`u4e00` / `0x1c` / `9_` / `v1` / `0335` 等）：它们因「绝无仅有」
  被 IDF 排到高位，挤占每主题只展示 8 条的窗口。改为候选资格判据「必须是纯字母或汉字」。
  实测展示窗口噪声 250/2223 (11.2%) → 0，Recall@12 与 @24 均保持 95.0%
- 入口抽取**退化成了字典序**：词表统计取自 entry 表（数的是「已经被选为入口的词」），
  df 被压成 0~5 六个取值，75.99% 的 atom 十六个权重完全并列 —— 排序由字典序决定，
  实际选出的是「候选里码点最小的 16 个」，高频真词被系统性挤掉。改为从 atom 正文统计，
  并列时按语料频次决定。实测 Recall@12 90.0% → 95.0%
- 整段中文混入入口：切词把「连续汉字」整段当成一个词，于是「全程没有建分支」「为什么
  会这样」这类原文切片也成了候选 —— 它们不会被任何查询命中，却占着每 atom 只有十几个的
  槽位。限制到四字以内后，展示窗口里超过四字的片段由 405 条降为 0

### Removed

- 两个与本机环境耦合的脚本（桌面端迁移与对照评测）移出仓库——它们依赖特定来源系统的
  只读接口与数据格式，外部用户无法使用。导入能力改为「由模型判决、脚本按来源临时编写」，
  设计约束见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) 的「导入既有记忆」

[0.9.0]: https://github.com/topkie/dsh-lattice/releases/tag/0.9.0
[0.5.0]: https://github.com/topkie/dsh-lattice/releases/tag/0.5.0
[0.2.0]: https://github.com/topkie/dsh-lattice/releases/tag/0.2.0
