# CONTEXT —— 给后续维护者与 AI 的上下文卡

> 本文件不是给用户看的说明（那是 `README.md`），而是**给下一个改这份代码的人/模型**的。
> 读它能避免重复踩已经踩过的坑，也不会把已经想清楚的设计当成"可以随手改的实现细节"。

## 这个插件是什么

`@yiyunet/dsh-kill-ai-slop`（中文名 **AI垃圾过滤**，英文名 **Kill AI Slop**）是一个 DSH 插件：
把用户**收到的文件**解析成文本，算两个分数（表达水分 / 工作空转），再把文件压成五要素。

设置面板挂在 **设置 → AI垃圾过滤**（`settings.section`，`order: 23`）。

**两个术语是公开词汇，不要以"脱敏"为由删掉。** `AI Slop`（表达层的填充）与
`Work Slop`（工作层的转嫁）出自公开讨论与三个上游开源项目，分别对应 A / B 两个指数，
README **应当**解释它们。脱敏清的是内部体系信息（节点编号、组织名称、汇报关系、
相邻系统名、业务域词、档位行话），**不包括公开方法论词汇**。
删了会让「表达水分分」「工作空转分」这两个名字失去来处 —— 尤其"工作空转"本身不自明。

## 八个不可随手改的设计决定

### 1. B（工作空转）不折算成分数

表达水分（A）保留 0–100 指数；**工作空转（B）不出分**，它的载体是「交付承接台账」。

这不是"B 判得更准了"，而是**不再假装能算**。三条理由：

1. 判定对象错了。B 要回答"任务有没有被接住"，需要**两个操作数**（任务要求、实际交付）。引擎只见过交付件一个。
2. 分母一动分数就跳，而且跳得像能力变强。0.2.1 把 B4/B5 改成"未能评估"后，加权问题值一直是 27.5，分母从 85 掉到 55，分数从 32.4 涨到 50 —— **同一份文件，同一个问题量，涨了 17 分**。
3. 就算分母稳定，B 实际也只剩 B1+B2 在算，两个词表维度合成一个 0–100 分，本质是两次关键词统计。

**A 与 B 也绝不合并成一个"AI 率"**：那会同时制造两类误判 —— 把高质量的技术件判差（读起来"不像真人散文"），把人写的空文件判好（朴素）。

B1~B6 仍逐条列出，只作**线索**。谁把它们合回一个分数，谁就在重造一个会撒谎的数字。

### 2. 三种状态不许互相顶替

`not-applicable`（这份文件不需要这一项）／`insufficient`（我没法判）／`scored` 且 severity=0（判过了，没问题）。
前两者**退出分母**并写理由。把 `insufficient` 当 0 分处理，等于把"我没看"报成"没问题" ——
这是这类工具最危险的失败模式，因为它看起来什么都没发生。

### 3. 覆盖不足就不出分

解析覆盖率 < `minParseCoverage`（默认 0.7）时，两个指数都返回 `null` 并写明原因。
宁可给一个不完整的结论，也不给一个让人误以为全面的数字。

### 4. 提炼是抽取式，不改写

`distill.mjs` 输出的每一句都是**原文连续片段**，带 `start/end` 可回原文逐字核对。
这不是实现偷懒，而是刻意的取舍：本地没有任何规则能证明"改写后的话与原意等价"。
代价是读起来不如人写的摘要顺；收益是**永远不会把"预计降本"写成"可以降本"**。

配套机制：`hardProtection()` 命中的句子（数字/日期/责任人/承诺/条件结论/**边界表述**）**强制入选**，
任何压缩档位都不得丢。保真是靠结构保证的，不是靠事后比对发现的。

**边界表述为什么必须硬保护**：压缩里最危险的一类错误是把"不做什么"压成"做什么"。
「不直接铺大众电商」「而非万能诊断仪」被删掉之后，摘要从"有边界的方案"变成"无条件的承诺"，
而字数上完全看不出来。普通否定词（"不"）频率太高，硬保护会让提炼退化成照抄，
所以只把 `而非 / 暂不 / 仅限 / 只做 / 不包括` 这类**成句的边界判断**升级为硬保护。

### 5. 判不了的维度必须说"判不了"，不许印成"没问题"

`insufficient` 不是装饰。B4（逻辑一致性）与 B5（决策支持）在规则层**没有判断能力**：
B4 只能核对数值口径，B5 只能确认文中出现过"风险"一类词。零命中一律返回 `insufficient`，
并在报告**结论区**点名，不许藏进附录。

B3（论据支撑）属于同一类，但更隐蔽：它能判"同一句里有没有出处标记"，判不了"这个依据是否支撑这个结论"。
**补一句"根据上述调查"就能满足规则，推论跨越照样存在。** 它是这一类里最难看出来的一项 ——
因为它每次都在给出一个看起来很具体的百分比。

这条错误的推广形式：**凡是规则判不了的语义结论，不得输出为判定。**
不是"这两个维度要小心"，是"这一类都不许"。0.2.1 只按维度名修了 B4/B5，
下一个版本就得去修 B3 和 B6，因为缺陷的粒度是**类**，不是维度名。

反面教材就在这个仓库的历史里：0.1.x 时期这两维零命中时返回 `severity: 0`，报告印出
"逻辑一致性：未发现问题"。**这是错误的确定性，比不确定危险得多** —— 人说"不知道"，
读者会去核实；工具说"没问题"，读者会直接跳过这一项。

### 6. 语义判断不归这个插件管，但语义结论的**证据**归它管

Work Slop 是「任务要求 − 实际交付」的差，两个操作数。一份文档算不出来。
所以：拆义务、判覆盖由具备语义能力的一方做；**引文是否真实存在、行号对不对**由本插件做。

这条分工的根据很硬：模型最容易骗人的地方是编行号和编引文，而规则层在这件事上 100% 可靠。
`slop_obligation_audit` 从不判断结论对错，只判断证据有没有 —— 引文对不上的判定，一律降级为不可采信。

### 7. 零依赖、零联网、零上传

`parse.mjs` 自己实现 ZIP + OOXML + PDF 内容流解析，不用 mammoth / pdf-parse / xlsx。
理由：只需要纯文本；自行实现让依赖数与攻击面同时归零（不执行内嵌内容、不解析样式、不落盘）。
`npm run verify` 有一条断言专门守这个承诺 —— **加依赖会让它直接红**。

### 8. 插件不拆义务，面板不是台账入口

台账需要一份**义务表**才能成立。义务必须由具备语义判断能力的一方给出。

**为什么不让插件自己拆**：从文档结构倒推出来的义务，本质是"最佳实践清单"。
用它判承接，每一份写得中规中矩的文件都会"通过" —— 真正的任务缺口被洗成"已满足"。
这是比"不出结论"严重得多的错误：它给了一个看起来完整、实际无效的答案。

所以入口分工是固定的：

| 入口 | 能给什么 | 不能给什么 |
|---|---|---|
| 设置面板 | A 表达指数、结构检查、五要素提炼 | 承接结论（面板里没有模型，拆不出义务） |
| 对话（宿主 agent） | 拆义务 + 判覆盖，再交插件复核 | —— |
| `slop_audit_text` / `slop_audit_file` | 收到 `task` + `obligations` 时出完整台账 | 不给就不出，绝不猜 |

四种状态**不得互相顶替**（这是第 2 条在台账上的推广）：
`null`（没给任务）／`pending`（给了任务没给义务表）／`error`（义务表不合契约）／`ok`（台账成立）。
每一种都必须在报告里**明写是哪种**，不许用沉默代替。

## 改代码前必须知道的四件事

1. **`lib/` 是产物**，唯一真源是 `plugin-src/`。构建脚本会在每个产物文件头写横幅；
   直接改 `lib/` 会在下次构建被覆盖。
2. **客户端不能用 import**。`plugin-src/client/impl.mjs` 必须只导出一个
   `export function createClientPlugin(React)`，构建时去掉 `export ` 后嵌进
   `window.__ModuleLoader__` 的 factory 内部。写成普通模块会拿到 `undefined` 的 React，
   症状会报在**别的插件**上（例如 client-connection 未注册），极难归因。
3. **改规则就要升 `RULESET_VERSION`**，改计算流程就要升 `ENGINE_VERSION`（且与 `package.json` 同步）。
   不升版本 = 新旧分数被当成可比 = 所有历史结论作废。
4. **`npm run build` 之后必须重启 DSH。** 热重载只重新导入改动过的文件，
   于是 `audit.mjs` 换了新版、它 `import './rules.mjs'` 却命中进程里的旧版 ——
   **报告上写着新版本号，算分用的是旧规则，全程不报错**（这是实测踩过的 F7/F11）。
   一致性闸门能抓"跨版本混装"，但**抓不到"两个模块同版本、都还是旧的"**：
   这时候 `moduleConsistency.ok === true` 恰恰是坏消息，它只证明内部一致，不证明是最新。
   **判断"跑的是哪一版"，唯一可靠的动作是看 `slop_engine_info` 返回的 `engineVersion`。**

## 必须改的时候，按这个顺序

1. 改 `plugin-src/host/rules.mjs` 里的词表/阈值 → 升 `RULESET_VERSION`
2. 跑 `npm run build`（产物必须重建）
3. 跑 `npm test`（单测直接测 `plugin-src/`，不是测 `lib/`）
4. 跑 `npm run verify`（十一条契约断言）
5. 更新 `CHANGELOG.md` 的「已知限制」——**新增能力要写上限，不写上限等于过度承诺**

## 绝对不要做的事

- ❌ 输出"AI 生成概率 X%""疑似 AI 撰写" —— 文风不是作者身份的证据
- ❌ 用这个分数做员工绩效/转正评价 —— 同一份文件在不用用途下分数本就该不同
- ❌ 在 `package.json` 里加 `dependencies` —— 会打破零依赖承诺，且 verify 会红
- ❌ 声明 `postinstall` / `install` / `preinstall` —— pnpm 10+ 会拦，消费者安装直接失败
- ❌ 在 README 里写没有验证过的能力（"支持全部 PDF""准确率 95%"）—— 见 `CHANGELOG` 的已知限制对照写
- ❌ 把权重/阈值说成"标准" —— 它们现在是 **初案（未经校准）**，首个盲测后才回写实测值

## 当前状态（诚实版）

| 项 | 状态 |
|---|---|
| 代码 | 已实现（宿主 8 个模块 + 客户端 2 个模块 + 4 个脚本 + 4 个测试文件） |
| `npm run doctor` | ✅ 通过（Node 24.19.0，GBK 可用，零依赖） |
| `npm run build` | ✅ 通过（`lib/` 9 个文件；装载层自证：顶层执行 / 注册 / 导出三件全 OK） |
| `npm test` | ✅ **77 / 77 通过** |
| `npm run verify` | ✅ 12 类发布契约断言全过 |
| 装进 DSH 跑通 | ✅ **已装并在跑（0.3.6）** —— 设置 → AI垃圾过滤 面板可开；宿主注入 6 个工具；txt / md 输入多轮实测 |
| 真实文件回归 | **未做** —— 单测与实测都停在 txt 与自造 fixture，**没跑过一份 Word / PDF / Excel 真件** |
| 盲测校准 | **未做**（权重与阈值全部为初案） |
| 发布（npm） | **未发布** |

下一步的两个动作，按顺序：

1. ~~装进本机 DSH，确认面板能开、能出报告~~ ✅ **已完成** ——
   验的是"宿主契约"，结论是契约成立（面板位、RPC 信封、工具注入、模块装载全过）。
   顺带暴露并修掉 12 条现场缺陷（F1–F12），**没有一条是单测能抓到的**。
2. 拿 5 份真实文件（docx / pptx / xlsx / 含文本 PDF / GBK txt）人工核对"抽出来的字是不是该抽的字" ——
   这一步验的是"解析准确度"，也是唯一能支撑"能用"两个字的证据。
   **在它之前，"支持 Word/PDF"这句话不成立**：自造 ZIP 只能证明"我的读取器能读我造的包"，
   不能证明"能读 Word 导出的包"。
