# 设计稿:经验库(v0.8.0) > 状态:**已实现**。事实来源是 `README.md` 的「经验库」小节与 `lib/lessons.js` 的模块注释; > 本文件保留为**决策记录**——记下当时为什么这样定,以及刻意不做什么。 --- ## 一、问题 到 0.7.x,stash 已经回答了两个问题: | 问题 | 谁回答 | 存在哪 | |---|---|---| | 这条库**允许怎么取**? | 库的 `access` 声明 | `sources.mjs` / `sources.local.json` | | 这次取数**取到了什么**? | 取数台账 | `ledger.ndjson` | 但还有第三个问题没人回答:**「这条库踩过什么坑、正确写法是什么」**。 它和前两个不一样:`access` 是事前声明,台账是事后留痕,而坑是在**失败之后才被认识的**。 0.7.x 的四个载体都不承接它: | 载体 | 能承接什么 | 缺口 | |---|---|---| | handler 代码固化 | 内置 handler 的已知坑(口径 / 翻页 / 聚合字段) | 只有改代码能加,运行时加不了 | | `stash_doctor` 深度校验 | 注定取不到数的写法 | 规则硬编码 | | 取数台账 | 取过 / 失败 / 被边界拒绝 + `contentHash` | 只记指纹,不记「为什么失败、下次怎么改」 | | 库条目的 `notes` | 注意事项(自由文本) | 手写库根本写不进去(见下) | **`notes` 的致命处**:手写的 `sources.mjs` 永不被程序改写,而现实里的库常常就住在手写文件里 (本机就有这样的库)。所以对这批库,模型**无路可写**—— 而它们恰恰是最需要记经验的。 ## 二、目标(由使用者给定) > 各库适用的经验各不相同,stash 只提供**方便其整理存储**的。 这句话定了两件事: 1. **不做内容判断。** 不规定经验长什么样、不做分类、不做语义检索。 2. **只做设施。** 存放、校验、按库带出——判断内容属于使用它的人。 ## 三、决定 ### 3.1 独立文件,按库 id 分组 `$DSH_HOME/stash/lessons.json`,第一层键就是库 id: ```jsonc { "trade_stats": [ { "id", "at", "title", "body", "action", "tags", "evidence" } ] } ``` 这是**被上一条约束逼出来的**:经验不能挂在库条目上(手写文件不可写),只能独立存放。 好处是手写库与代写库一视同仁,而「各库经验各不相同」正好落在文件的第一层键上。 ### 3.2 松结构:只有 `title` 必填 | 字段 | 必填 | 说明 | |---|---|---| | `title` | ✅ | 一句话说清这条经验是什么 | | `body` | — | 细节:现象 / 原因 / 正确写法 / 复现步骤 | | `action` | — | 这条经验针对哪个 action | | `tags` | — | 自由标签,**不设枚举** | | `evidence` | — | 通常填台账 `ledgerId` | `evidence` 是唯一一处「结构带来新能力」的设计:它把**这次踩的坑**与**这一次取数的留痕**接起来, 于是「我当时试过、它这么返回」变成可复现的。除此之外不再加字段——每加一个字段, 就是替某个库决定它的经验应该长什么样。 ### 3.3 三处带出 | 时机 | 给什么 | 为什么 | |---|---|---| | `stash_catalog` 全量清单 | 条数 + 最近 3 条标题 | 让人知道「这条库有经验可看」而不灌爆上下文 | | `stash_catalog <库id>` | 全文 | 那一刻正是要动手取数 / 排查的时候 | | `stash_doctor` | 总量 + 「有失败台账却没记经验」的库 | 失败是证据、经验是结论;缺结论的库会再踩一遍 | | `stash_fetch` 失败 | 提示可记一条,附 `ledgerId` | 失败正是「学到东西」的时刻——但只提示,不代替决定 | ### 3.4 有界,且到顶报错而不是淘汰 每库 200 条。**到顶时拒绝写入并提示去整理**——静默淘汰最旧的,等于悄悄丢掉别人写下的教训。 ### 3.5 两条写入路径,地位相同(0.8.1 补) 一开始只考虑了工具写这条路,结果手写条目露了个洞:人往 `lessons.json` 里加一条经验, 通常只写 `title` 与 `body`,不会去算 id——而 `removeLesson` 按 id 定位, 于是这条经验**能被读出、却删不掉**,在列表里也显示不出标识(已实测确认)。 修法是在**读取时**为没有 `id` 的条目派一个稳定 id(按 `at | title | body | action | tags | evidence` 哈希): - 同一条内容每次读出得到同一个 id → 手写条目与工具写的条目在 `list` / `remove` 面前完全等价; - 内容改了 id 就变——但那就是另一条经验,符合预期; - 派生 id 在工具下次回写时落盘,文件自我修复。 同时明确两处**不对称**,因为假装对称会更糟: 1. 手动录入的内容**不过**密钥特征扫描(扫描挂在 `addLesson` 上,即工具那条路)。 手写文件本来就是人对自己负责的地方;而且经验库本就不该放口令。 2. 工具回写会**规范化排版**(排序 + 缩进 + 落盘派生 id)。这与 `sources.mjs` 的 "程序永不改写"不同——`lessons.json` 是工具写、人可整理的文件,两者的边界本来就不一样。 ## 四、明确不做的事 1. **不做语义检索。** `query` 是子串匹配,与 `stash_files` 一致。语义检索归 harness 的检索能力。 2. **不做自动写入。** 取数失败**不自动**生成经验——自动生成的多半是噪音,而经验的价值全在「有人真的想清楚了」。 3. **不碰密钥。** 正文与标签都过 `lib/secrets.js` 的厂商前缀扫描,命中即拒绝。经验叙述最容易顺手把口令抄进去,而这文件是明文的。 4. **不参与 `healthy`。** 经验是补充,不是健康条件;没有经验的库不该显示为「有问题」。 5. **不做经验与台账的双向同步。** `evidence` 只是一条引用字符串,台账删了它也不会失效——单向引用比双向绑定结实。 ## 五、留给下一轮 - ~~经验的**导出/导入**(换了机器想带走它——目前靠手拷 `lessons.json`)。~~ **已在 v0.10.0 做掉**:`/stash export` / `/stash import`,见 `DESIGN-portability.md`。 - 按 `tags` 的统计与横向检索(跨库「这类坑出现过几次」)。 - 经验与 `stash_doctor` 深度校验规则的联动:如果某类坑反复出现,是否该升级成一条**代码级**校验规则。 这一条要小心,它会把「使用者写的文本」变成「程序强制的规则」,边界不同,值得单独设计。