# R4 · 检索区分度与配额治理（2026-09-18 用户批准立项）

> **来源**：用户 2026-09-18 对话中提出的三个真实痛点 + 一处实测 bug。
> **用户原话**：
> - 「现在其实还是区分度有点问题，更加有含金量的结论和每一次都有的日志会混在一起。在 memory recall（记忆唤起）的时候，有什么好的处理方式吗？」
> - 「配合这个问题也困扰我很久。有的时候配额太少，效果完全没有，或者有些大条目可能就被过滤掉了，一点用都没有；有的时候配额多了，我又怕浪费 token」
> - 「我尝试把其中一个固定额度加大成两倍的时候，还遇到了一个锁死的问题。所以现在确实需要整一整额度的问题了。不过，确实得基于长期的观察，科学的（测量），不能拍脑子。」
> **归口**：本节独立于 L/M/S 执行顺序（不参与层级冻结），但**用户已批准纳入作战计划**。

---

## 0. 一句话

**结论与流水在检索里区分度不足** —— 语义臂内部完全不分层，融合层的层次优势（≈0.001）被流水层的数量优势（本机 449 vs 16）淹没。同时，**配额系统存在一个已确认的「锁死」缺陷**（见 §3），必须一并治理。

---

## 1. 实测证据（本次勘查，附行号）

### 1.1 语义臂内部完全不分层

```js
index.js:6048-6057  corpus 收集：所有来源**平铺**进一个数组（layer 字段存了但没用）
index.js:6068       filter(sc >= minScore)       ← 只看分数
index.js:6069       sort((a, b) => b[1] - a[1])  ← ★ 纯分数倒序，不看 layer
index.js:6070       slice(0, max(2, limit))
```

### 1.2 检索输出**不显示 layer**

```js
index.js:6093  out.push('· ' + s.label + ' [' + s.id8 + '] ×' + s.score.toFixed(2) + ' ' + s.l0)
index.js:5977  out.push('· [' + c.id + '] ×' + sc + fr + ' ' + reason + ' ' + label + ' — ' + l0)
```
⇒ 模型看到的两个 `MEMORY.md`（结论）与三个 `2026-09-xx.md`（流水）**视觉上完全同级**。

### 1.3 层次优势被数量优势碾压

| 层 | rank 1 时的 RRF 贡献 | 本机语料量 |
|---|---|---|
| project（结论） | `1/(60+1)` = 0.01639 | **16 条** |
| log（流水） | `1/(60+5)` = 0.01538 | **449 条** |

差距 **0.001**，而流水数量是结论的 **28 倍**。⇒ 结论在检索中几乎必然被淹没。

### 1.4 ★ 但这个能力**注入侧早就有了**（可直接复用）

```js
tier0-catalog-pre.js:391  TIER0_QUOTA_DEFAULTS = {
  projectRatio: 0.6,
  floorRatio: 0.1,
  projectLayer: 'project',
  floorLayers: ['whiteboard', 'user'],
}
```
其 docblock 原话点明了同一个问题：「语料实测 77 块把 800 token 全占，whiteboard/user/log 一条都进不来，**分层退化成单层**」。

⇒ **注入侧已解决，检索侧没有。** 这是本项工作的核心不对称。

---

## 2. 三个方案（用户已批准"可以加进作战计划"）

### ★ 方案 A：作废条目**返回但显式标记** + 指向最新结论

**现状**：`index.js:5838` `if (!isCurrentPre(it)) continue` ⇒ 作废条目**被完全跳过**，模型不知道它存在过，也不知道有过"此路不通"的教训。

**改为**：
```
✗ [已作废] MEMORY.md [a1b2c3d4] — 旧结论：方案用 A 实现
            ↳ 已被取代，最新结论见 mem_x9y8z7w6
```

**关键前置**：需要 `supersededBy` 指针字段（记录"被谁取代"）。
- 现状：`status` 只有三值，**没有指向后继的字段**
- 建议：G3 写 `superseded` 时**顺便记下取代者 id**
- ⚠️ **待确认**：这是否违反 S10.4「不新建状态源」？
  （**倾向不算** —— 它是 `status` 的伴生属性，与 `status` 同级；但需用户/文档明确）

### ★ 方案 B：检索结果**分层展示**（用户：「分层显示可以先做」）

**不改排序**，只改**呈现**：
```
【结论层 · 项目笔记/用户级】
· MEMORY.md [e5f6g7h8] ×0.79 项目铁律：写入前必须备份
· MEMORY.md [m3n4o5p6] ×0.77 架构决策：语义臂可降级

【流水层 · 每日日志】
· 2026-09-18.md [a1b2c3d4] ×0.82 今天修了个 bug
```
**零排序风险** —— 直接解决"看不见层次"的问题。**建议先做这个。**

### ★ 方案 C：检索侧**层次保底配额**（用户：「单独评估」）

复用注入侧 `TIER0_QUOTA_DEFAULTS` 的思路：语义臂输出时给 project/whiteboard/user **保底条数**，即使分数低也保证露面。

⚠️ **会改变排序结果** ⇒ 必须单独评估、单独验证。

---

## 3. ★ 「锁死」缺陷（用户实测踩到，本次定位到代码级）

**用户原话**：「我尝试把其中一个固定额度加大成两倍的时候，还遇到了一个锁死的问题。」

### 机制（附行号）

```
:4621 capacityLimit(layer)        读 userCapacityChars/noteCapacityChars，<500 视为脏值回落默认
:4655 ensureBudget                 超限 → 进入整理循环（最多 3 轮）
:4744 const allowFold = !(last && now - last < COMPACT_THROTTLE_MS)
:227  COMPACT_THROTTLE_MS = 10 * 60 * 1000     ← ★ 10 分钟节流
:4799 / :4917 return { ok: false, reason: 'no-removable' }
:4832 this._lastCompactAt[layer] = Date.now()  ← 整理过一次就上锁 10 分钟
```

### ⛔ 本节的「节流锁死」推断已被实测证伪（2026-09-18 23:15 更正）

> **下面从"锁死的三步成因"起的整段分析作废，保留仅为记录推断过程。**
> 复现证据见 `artifacts/_probe-budget-lockup.mjs`（抽取生产方法体真实执行，10/0）与
> `tests/smoke/smoke-test-r4-budget-lockup-pre.mjs`（25/25）。

**证伪的三条硬证据**：

1. **`reason:'throttled'` 是死分支** —— 全仓 grep 仅有 6 处命中，其中**没有任何生产者**，
   只有 `budgetRefusalTextPre` 里的文案。既然没有代码路径返回它，"被 throttled 拒绝"不可能发生。
2. **节流只挡 AI 折叠、不挡整条归档** —— `compactLayer` 的注释（`:4742` 附近）即如此声明；
   实测「整理一次 → 10 分钟窗口内第二次写入」**成功**，不存在 10 分钟锁。
3. **真缺陷在别处，且是 legacy 路径（出厂默认走这条）**，两处：
   - **`keepBudget` 口径用错变量**：旧式 `Math.max(COMPACT_PROTECT_RECENT_CHARS, limit - deficit - 1)`
     展开即 `2*limit − curChars − add − 1` ⇒ **保留目标随额度单调递增**（方向反了），
     且把保护窗口从**软**下限变成**硬**下限 ⇒ 额度落进死亡区间时被拒、**更小和更大的额度却能写** = **非单调**。
     修为 `Math.max(0, curChars - deficit - 1)`。
   - **回写量护栏缺失**：「AI 不可用」分支把刚归档的老段落**原样写回**主文件
     （实测 `[compacted] note: 1729 -> 1730 chars`，不降反升）⇒ **归档发生了、空间却没腾出来**
     ⇒ 三轮后返回 `still-over-capacity`。补上锚点路径同款护栏。

**为什么原推断看起来很合理**：`:4832` 确实在成功路径上无条件打 `_lastCompactAt` 时间戳，
`compactLayer` 也真的读了 `allowFold` —— 但那个标志**只决定"要不要再跑一次昂贵的 AI 折叠"**，
与"能不能写入"无关。**教训（可复用）：看到时间戳 + 节流常量，不等于看到拒绝路径；
判断一个 reason 是否是活分支，grep 生产者而不是读消费者文案。**

### （已作废）原推断：锁死的三步成因

1. ~~用户调大配额 ⇒ 但文件里已有内容不变 ⇒ 本不该再拒绝~~
2. ~~若仍超限 ⇒ 触发整理~~
3. ~~整理成功一次后上锁 ⇒ 10 分钟内任何再写入都被 `throttled` 拒绝~~

### （已作废）原推断：更隐蔽的一层

~~`:4689` `still-over-capacity` 时 10 分钟锁同样生效 ⇒ "没腾出空间"与"节流中"叠加。~~
**实际成因**：是回写护栏缺失（见上），与节流无关。

### 待办 → 已全部完成（2026-09-18）

- [x] **复现**：已写探针与套件，**推翻了原假设**（不是 throttled，是 keepBudget 口径 + 缺护栏）
- [x] **判定**：判定为**缺陷**（非设计），且是**默认可达、用户可见**的缺陷
- [x] **修复**：`keepBudget` 修为 `max(0, curChars - deficit - 1)` + 补回写护栏
- [x] **验收**：套件 25/25 · 变异 5/5 真红 · SHA256 逐字节还原 · 全量回归 PASS 121

---

## 4. 配额调优：用户要的「科学测量」而非拍脑袋

**用户原话**：「确实得基于长期的观察，科学的（测量），不能拍脑子。」

### 现状：配额是**静态配置**，没有测量回路

| 键 | 默认 | 作用 |
|---|---|---|
| `injectBudgetChars` | 8000 | 每轮注入总预算（**与容量上限是两回事**，见 `:296` docblock） |
| `tier0MaxTokens` | 400 | Tier-0 目录 token 上限 |
| `tier0BudgetShare` | 0.25 | 目录占总预算比例 |
| `noteCapacityChars` / `userCapacityChars` | 24000 | **文件本体**容量（M9 刚调过） |

### 已有的观测面（可复用，不新建状态源）

```js
index.js:6537  budgets: (() => { ... })      ← debugInfo 已暴露
index.js:5099  state.tier0Meta = { tokens, items, candidates, dropped, perLayer }
index.js:5040  const budgetChars = Math.max(Number(cfg.injectBudgetChars) || 1600, 400)
```

**`tier0Meta` 已含 `candidates / dropped / perLayer`** ⇒ **"因为配额被丢掉多少条"这个数据已经在采集了**，只是没被用来调参。

### 建议的测量闭环（不新建状态源，复用 degrade 台账）

1. **采集**：每轮把 `tier0Meta.dropped / perLayer` 追加到 degrade 台账（已有落盘通道）
2. **观察**：面板展示「近 N 轮各层 dropped 趋势」
3. **判定**：某层**长期 dropped > 0** ⇒ 配额偏小；**长期 dropped = 0 且远未用满** ⇒ 配额偏大、浪费 token
4. **再调整**：按观察结果改默认值，**不拍脑袋**

> **与 R3 降级台账同源**：degrade 台账本来就是「跨轮可查询的观测面」，配额测量是它的第二个消费者（第一个是降级事件）。**零新建状态源。**

---

## 5. 优先级与依赖

| 项 | 风险 | 依赖 | 建议顺序 |
|---|---|---|---|
| **B 分层展示** | 极低（只改呈现） | 无 | **第 1** |
| **降级留痕补齐**（py→C2→词法每一跳） | 低 | 无 | **第 1**（与 B 同批） |
| **§3 锁死复现 + 修复** | 中 | 需先写复现套件 | **第 2** |
| **§4 配额测量闭环** | 低（只采集） | 复用 degrade 台账 | **第 2** |
| **A 作废标记 + supersededBy** | 中 | G3 实施后 | **第 3**（随 G3） |
| **C 层次保底配额** | 高（改排序） | 需 A/B 先跑出数据 | **第 4**（单独评估） |

---

## 6. 与既有工作的关系

- **G3**（结论层状态写入）：A 方案依赖 G3 写 `supersededBy` ⇒ **两者合并实施**
- **R3 降级台账**：§4 配额测量复用其通道；降级留痕补齐也是 R3 的收尾
- **M2.5b 层次臂**：本项是它的直接延续 —— 层次臂"只能放大已有优势"，而 **C 方案是"给结论保底露面"**，二者互补
- **S10.4**：全项**不新建状态源**（A 的 `supersededBy` 需确认边界）

---

*本文档为专项立项，执行细节并入 BATTLE-PLAN 的 R 系列。*
