# 证据 / Evidence

本地、可重跑。测试命令与数字都来自这台机器，不是估算。

## 跑法

```bash
npm test          # 存储、状态、投影、召回、跨层、客户端投影、接口契约
npm run bench     # 固定数据集上的准备时延与长期序列
npm run bench:evolution  # 实验规则/样例/检查点/回执的存储与投影成本
```

## 本轮门禁（U01–U06）

| 编号 | 场景 | 现状 | 证据 |
| --- | --- | --- | --- |
| U01 | 两轮读同一 ID，组件折叠/重挂载 | 引用按每轮自己的工具结果重建，不读全局面板状态 | `client.test.mjs`：`citationsOfTurn` 只从节点取；同一轮去重 |
| U02 | PTC 子结果与普通结果 | 子调用与主结果同一套元数据解析；canonical 值不带维护字段 | `client.test.mjs` 子调用断言；`contract.test.mjs` 元数据键集合 |
| U03 | 旧行、noop、needs_resolution | 没有元数据只读；`changed !== true` 不出现撤销；旧版本撤销返回 409 | `client.test.mjs`；`contract.test.mjs` 过期撤销 |
| U04 | keep/undo 网络失败后重试 | keep 的 `operationId` 存在组件 ref 里，重试复用；undo 按 id + 版本重试 | 代码路径；未做真实断网点击 |
| U05 | 收起/切轮后旧正文迟到 | 每次请求带代次，切换、收起、卸载都作废旧响应 | 代码路径；未做真实页面点击 |
| U06 | 引用已更新/删除/移动 | 正文按 id 现查当前层：版本、标题、归档状态现取；找不到显示已删除 | `contract.test.mjs`（当前版本、`scopeLabel`、`gone`）；`memory_read` 归档可读已实测 |

真实页面点击需要已登录的 Web 视图；本次环境拿不到该视图的登录态，所以 U04/U05 只到代码路径与逻辑单测，没有冒充成页面验收。

## 固定数据集性能

2026-10-29，macOS arm64，Node v24.16.0，本地 SSD。数据：项目 200 条 + 全局 200 条，全部带标题、摘要、`when`、正文；另加一条正面场景，确认全局卡片真的进得来且不超预算。0.18.40 版复测（两次取稳定的一次）。

0.18.0 起每次保存都走「写临时文件 → fsync → rename → fsync 目录」并保留上一版，代价是单次保存多两次落盘；下面这组数是带这套持久化保证后测的。

| 指标 | 数值 |
| --- | --- |
| 准备时延 P50 | 48.6 ms |
| 准备时延 P95 | 51.4 ms |
| 冷启动首次准备 | 69.6 ms |
| 最慢一次 | 69.6 ms |
| 每次准备上下文 | 368 B |
| 正面场景上下文 | 335 B，含 1 张全局卡片，未超 3 条 / 1200 字节 |
| 每次准备文件读取 | 0.2 |
| 本次数据规模 | 项目 200 条 + 全局 200 条 |

数据翻倍到两层各 200 条后仍在 deadline 150 ms 内：0.18.40 复测 P95 51.4 ms。模型 provider 在准备阶段无 I/O、无网络。

## 长期序列

150 轮保存 / 读取 / 检索 / 遗忘交错后：

| 指标 | 数值 |
| --- | --- |
| 项目活跃条目 | 350 |
| 全局活跃条目 | 200 |
| `state.json` | 396 KB（上限 2 MiB） |
| 层内文件 | `state.json`、`state.prev.json` 两个，不随轮数增长 |
| RSS | 315 MB（含进程内缓存与测试数据；两次 301/315 MB） |
| 序列耗时 | 5.8 s（150 轮全落盘） |

## 实验整理成本（0.11.0）

固定 32 条实验规则、64 条回执，全部带 4 个样例、3 个检查点、3 条来源。macOS arm64，Node v24.16.0，本地 SSD：

| 指标 | 数值 |
| --- | --- |
| 规则数 / 整理输入规则数 | 32 / 6 |
| 整理输入字节 | 2,719 B（上限 8,192 B） |
| `state.json` | 123 KB（上限 256 KiB） |
| 样例 / 检查点 / 来源深度 | 4 / 3 / 3 |
| 效果回执窗口 | 64（已知 usage 16 次） |
| 单条 `memory_read` 投影 | 256 B |
| 冷启动快照 | 1.9 ms |
| 热快照 P50 / P95 | 1.1 / 1.3 ms |

`npm run bench:evolution` 会在达到任一上限时以非零码退出，可当门禁用。

## 效果评测

§17.4 的 2×2 防污染实验要跑真实模型、固定评分，不在 CI 里，也没有预填改善百分比。`test/scenario.test.mjs` 与 `bench/evolution.mjs` 只证明场景边界、读取条件和成本上限；不声称模型更聪明。GitHub 上的功能主张只对应这些确定性测试和本地性能数字，模型侧效果等实跑后再写。

## 增删改后基线（0.18.43）

回执上限 256→64、`hits` 改记真实交付后的同一台机器重跑（macOS arm64，Node v24.16.0）：

| 指标 | 0.18.41 | 0.18.43 |
| --- | --- | --- |
| 热快照 P50 / P95 | 67.1 / 84.6 ms | 70.3 / 85.7 ms |
| 单次注入字节 | 368 B | 365 B |
| 2000 次操作后 `state.json` | 406 KB | 406 KB |
| 热点层回执条数上限 | 256（实测 35.7 KB） | 64（约 9 KB） |

P50 差值在同机噪音范围内（同一进程内 3 次中位数），注入字节与状态体积未增长；
`state.json` 仍远低于 2 MiB 上限。命令：`npm run bench`。

## 包体 / client.js

2026-10-31，本机 Node v24 v24.16.0。量法固定为 `raw = Buffer.length`、`gzip = zlib.gzipSync(raw, { level: 9 }).length`（别和 `gzip -9` 混用，两者差约 400 B）。基线存 `bench/bundle-baseline.json`，`test/bundle.test.mjs` 只许下降。

| 指标 | 数值 |
| --- | --- |
| raw | 186590 B |
| gzip -9 | 46309 B |
| 40 KiB 目标 | 40960 B |
| 距目标 | 还差 5349 B（记作待还的债，不算达标） |

## 规模基线（0.18.68 工作区，2026-10-31）

同机 macOS arm64 / Node v24.16.0，`npm run bench` 用 `LITE_MEMORY_BENCH_N` 放大候选量，
项目层与全局层各 N 条；每档都过完整 gate（序列 150 存 / 150 读 / 150 搜 / 50 归档、
正向问题必须带卡片、`state.json` 低于上限）：

| N（每层） | 总条数 | cold | P50 | P95 | max | 注入字节 | 正向卡片 | 文件读/次 | rss 结束 | state.json |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 200 | 400 | 67.8 ms | 89.5 ms | 94.8 ms | 99.2 ms | 358 B | 343 B（含全局） | 0.4 | 370 MB | 424 KB |
| 400 | 800 | 111.8 ms | 143.8 ms | 152.2 ms | 157.9 ms | 358 B | 343 B（含全局） | 0.4 | 435 MB | 663 KB |
| 1000 | 2000 | 262.6 ms | 308.3 ms | 327.0 ms | 353.2 ms | 359 B | 345 B（含全局） | 0.4 | 455 MB | 1383 KB |

P95 始终低于本轮放宽后的召回上限，说明这一档没有靠「超时后再补」混过去：投影算完就投递。

### 两处按规模修的判断

1. **召回耗时上限按条数放宽**（`recallDeadlineFor`）：原来固定 150 ms，600 条左右会把已经算完的
   投影在最后一步丢掉（`bytes: 0`）。现在 200 条以内仍是 150 ms，每多 8 条加 1 ms，上限 400 ms；
   显式传更宽的值（测试 5000 ms）仍然优先，不会被封顶收紧。
2. **数字按整段比**（`view.matchScore` 的 `number` 加分）：切词会去掉分隔符，「条目 7」和
   「条目 77 / 797」字面分完全并列，谁在前面只看 id。标题里数字整段对上加 2 分，
   只在正文/摘要对上加 1 分；上面 1000 条档的正向问题就是靠这条才稳定拿到那一条。

### 容量饱和时的归档

1000 条档位本来就在项目层硬上限（1000）上，序列再存 150 条时最旧未读的会被挤进归档：
结束后 1000 活 + 150 归档（50 条来自显式 forget，100 条来自容量策略）。gate 核对的是
「50 次 forget 全部归档成功 + 150 条序列记录都还在」，不再假设归档数刚好等于 forget 数。
