# 贡献指南 · CONTRIBUTING（dsh-see-world / 仓库 dsh-open-eyes）

> 开眼看世界是 DSH 决策层插件：判定「何时搜索」（决策层），不自带搜索引擎（能力层）。
> 本指南重点说明**测试集扩充机制**（附录 B 是核心资产，PRD §6.5）与开发/回归流程，
> 以及两条**踩坑注意事项**（改代码前必读，见 §3）。

## 1. 开发环境

```bash
npm install
npm run build            # tsc 编译 TypeScript → lib/（测试前必须先构建）
npm test                 # 全部单元测试（node --test：tests/*.test.js + test/*.test.js）
npm run test:regression  # 附录 B 判定器回归（fixture 离线确定性后端，输出报告）
npm run regression:live  # 附录 B 用真实模型打分（需 OPEN_EYES_* 环境变量）
npm run cases            # 决策日志 → 附录 B 候选样例回流（见 §2）
```

环境变量（live 后端）：`OPEN_EYES_API_BASE` / `OPEN_EYES_API_STYLE`（openai 或
ollama）/ `OPEN_EYES_API_KEY` / `OPEN_EYES_MODEL`，详见 `test/appendix-b-runner.js` 头部。

## 2. 测试集扩充机制（附录 B）

**测试集是核心资产**：`test/samples/appendix-b.json` 随仓库分发，任何判定器行为变更
必须附带测试集回归记录（防「调档位导致回归」）。

### 2.1 测试集结构

- `samples[]`：每个样例含
  `id`（1-based 连续编号）、`group`（`should_trigger` 应触发 / `should_not_trigger`
  不应触发 / `boundary` 边界）、`message`（本轮用户消息）、`history`（最近对话上文，
  数组）、`expected`（预期 `need_search` 布尔）、`expected_label`、`note`（触发逻辑
  说明）、`fixture`（fixture 后端的判定模型 recording 应答——离线确定性回放用）；
- 评分口径（默认档 lenient）：应触发漏网 ≤ 1/10、不应触发误触发 ≤ 1/10；边界仅记录不评分。

### 2.2 提交新样例的两种途径

**途径 A：直接编写/社区反馈**。在 `appendix-b.json` 追加样例（或在 issue/PR 中给出
消息与预期），维护者评审确认后并入。判断规则见 PRD R1 判定标准：外部世界事实 /
选型决策 / 方案存在性核查 → 应触发；纯本地工作 / 闲聊 / 稳定知识 → 不应触发。

**途径 B：从决策日志回流（推荐，真实案例驱动）**。`scripts/export-cases.mjs`
从 R4 决策日志（默认 `~/.dsh/dsh-open-eyes/decisions/decisions.jsonl`，可用
`--log` 指定或 `DSH_OPEN_EYES_DECISIONS` 覆盖）筛选「预期不符」案例：

| 类别 | 含义 | 导出组 |
|---|---|---|
| `no-but-searched` | 判定为不搜但模型实际搜索（疑似应触发/判定过保守） | `should_trigger` |
| `must-not-searched` | 用户强制「先搜」但判定为不搜（理论不可能，疑似 bug） | `should_trigger` |
| `never-searched` | 用户强制「不搜」但判定为要搜（理论不可能，疑似 bug） | `should_not_trigger` |
| `yes-but-not-searched` | 判定触发但回合结束前未搜索（R2 违规） | `boundary` |
| `degraded` | 判定器降级（无有效判定） | `boundary` |

流程（**人工确认后再并入基线**，脚本绝不直接改写 `appendix-b.json`）：

1. 使用插件跑几回合（包含疑似误判/强制标记/未搜等场景），产生决策日志；
2. `npm run cases` → 生成候选样例 `test/samples/exported-cases.json`
   （可加 `--session` / `--since` / `--categories` / `--limit` 过滤，
   `--no-write` 只打印）；
3. **人工评审**每一条候选：确认 `group` / `expected` 与判定预期一致；补全
   `message` 原文（日志默认只存摘要）、`history`（如需多轮上下文）、
   `fixture.suggested_queries`（建议搜索词）；
4. 重新编号：`id` 取当前基线最大 id + 1 起连续（候选文件中为 `null`）；
5. 并入 `test/samples/appendix-b.json`（`samples[]` 末尾追加）；
6. 跑回归：`npm run test:regression`，确认漏网/误触发仍达标；若新增样例暴露
   判定器缺陷，修复判定器后附回归记录；
7. 提交 PR（附：新增样例、回归报告、变更说明）。

> 隐私注意：决策日志默认只存判定输入摘要（`log_input_verbatim` 默认 false）。
> 只有开启过原文保存（`log_input_verbatim: true`）的记录才会导出 `input_verbatim`；
> 提交样例前请确认不包含敏感信息。

### 2.3 判定器行为变更的回归义务

- 修改 `src/judge.ts` / `src/config.ts` 的判定逻辑（档位、规则、快速通道、提示词）后，
  必须运行 `npm run test:regression` 并确认达标（漏网 ≤ 1、误触发 ≤ 1）；
- 回归报告（`test/samples/appendix-b-report.md` / `.json`）随 PR 附上；
- 测试集扩充与判定器改动应尽量分离提交，便于评审。

## 3. 踩坑注意事项（改代码前必读）

这两条都是**真实事故**换来的回归锁：测试替身复现不了、只在真实 DSH harness 里炸，
改相关代码时务必守住约束，并在改完后跑对应回归测试。

### 3.1 禁止用 `session.append` 发自定义事件

**事故**：早期判定动效信号曾用 `session.append('dsh-open-eyes/judge', …)` 写入会话
日志，导致所有带该事件的会话历史无法加载（`SessionFormatUnsupportedError`）。

**原因**：DSH 会话日志的读取端要求未知事件类型带 `ignorable: true` 标记（表示「可
安全跳过」），而 `session.append(type, data)` 的 API **无法设置该标记**（事件结构固定）。
未标记 ignorable 的未知事件会让读取端**拒绝解析整份日志**——一次判定动效毁掉整条
会话历史（「测试绿、真实挂」：测试替身不校验，真实 harness 严格校验）。

**现行方案（回归锁，测试锁定）**：

1. **禁止**用 `session.append` 写入任何自定义事件类型到会话日志（`tests/judge-signal-emit.test.js`）；
2. 主机 → 浏览器信号只走**进程内信号总线 + 插件自有 SSE 路由**：
   主机半部发布到 `JudgeSignalBus`，经 `/dsh-open-eyes/judge.sse` 推送给浏览器半部
   （`SseJudgeSource` 用 EventSource 订阅，自动重连 + 尾巴回放恢复状态）——完全不进会话日志；
3. SSE 路由注册采用「立即 + 迟到」两段式（webServer 服务可能晚于插件激活，见
   `tests/judge-bus.test.js` 集成回归）；
4. 所有信号链路失败静默降级（R5），绝不影响回合与历史。

> 底线：**任何自定义事件不得进会话日志**。需要主机→浏览器通信时，继续用
> `judge-bus.ts` 的总线 + SSE 模式，不要发明新通道。

### 3.2 访问 `ctx.web` 等服务必须先声明 `inject`

**事故**：回合流程里直接访问 `ctx.web`（R5 搜索缝探测）与 `ctx.tools`（标准搜索工具
回退路径）时，未声明 inject 的真实 harness 会抛
`cannot get property "web" without inject` 并**中断整个回合**（M2-3 试用实测发现，
测试替身无法复现——替身不校验注入）。

**规则**：

- 插件入口 `src/index.ts` 必须显式声明用到的服务：
  `export const inject = ['web', 'tools', 'settings']`（现已在入口；**新增服务访问时同步扩充**）；
- cordis 运行时要求显式声明 inject，否则访问未注入的服务属性会抛错；这三个服务在
  dsh-base 恒常挂载，声明不影响插件激活；
- 新增宿主服务访问时：先在 `inject` 数组里声明，再写访问代码；**不要**用
  `(ctx as any).xxx` 绕过（绕过后真实 harness 仍会炸，只是炸得更隐蔽）。

> 自检：改动了访问 `ctx.*` 服务的代码，跑一次带真实 harness 的集成回归
> （`tests/` + `test/` 下联动用例）确认没有「测试绿、真实挂」。

### 3.3 其他约定

- 主机 → 浏览器信号只走总线 + SSE 路由（同 3.1）；
- 所有信号/日志链路失败静默降级（R5），绝不影响回合；
- 新增公开 API 需在 `src/index.ts` 统一 re-export 并补测试；
- 决策日志字段（`DecisionRecord`）为对外公开格式：新增字段只做追加、不删除/改名；
- 判定器/模型路由改动同步更新 `CONFIG.md` 白话说明（用户反馈过「配置看不懂」，文档须跟代码走）。

## 4. PR 检查清单

- [ ] `npm test` 全绿；
- [ ] `npm run test:regression` 达标（附报告）；
- [ ] 新增/修改样例已人工确认（途径 B 需走完评审流程）；
- [ ] 无 `session.append` 自定义事件（§3.1 回归锁）；
- [ ] 新访问的 `ctx.*` 服务已加入 `inject` 声明（§3.2）；
- [ ] 文档同步（README 配置表 / CONFIG.md / 本指南流程）；
- [ ] 无面板/弹窗类 UI 新增（PRD 非目标）。