# 移植偏差申报 · dsh-threat-gate ← `sources/threat_tracker.py`

按 FIXREPORT 惯例，逐字段/逐行为对照。**"等距"= 行为与上游等价；"偏离"= 有意不同，
每条给理由与锚。**本表由构建侧（DSH）在 0.1.1 审计后补齐——0.1.0 的申报声称
「三条计分规则与阈值逐字保留」，其中规则 B 的族与序**并非等距**，此前的申报不准确。

- 上游源件：`sources/threat_tracker.py`（md5 `fd1991c4…`，生产活体 PATCH-02）
- 对照方法：把上游 `_update_threat_score` 逐行转写成 JS，与本件对同一信号序列并排跑
  （探针 `threatgate-audit.mjs`，见交接实跑输出）

## 一、逐项对照

| # | 上游 | 本件（0.1.1） | 等距？ | 理由与锚 |
|---|---|---|---|---|
| 1 | `record_intent(session_id, intent_signal, gear, proposed_upgrade)` | `record(sessionId, signal, options, denyPattern)` | **偏离** | dsh 无 intent 层。`intent_signal` → 工具名；`proposed_upgrade` → 「真被拒」判据。见 §二 |
| 2 | 窗口默认 `window_size = 10` | `config.windowSize` 默认 10 | 等距 | 非法值抛错而非静默回落（0.1.1） |
| 3 | 窗口为**计数窗**，`len > window_size` 时 `pop(0)` | `signals.length > windowSize` 时 `shift()` | 等距 | 锚：`窗口滑动` |
| 4 | `len(signals) < 2` 直接返回（不更新分） | `signals.length < 2` 直接 return | 等距 | — |
| 5 | 规则 A：同 signal ≥3 ⇒ `0.1*(c-2)` | 同 | 等距 | 锚：`A:` / `A 阴性锚` / `§1 阳性锚` |
| 6 | 规则 B：`has_prelude AND has_exec`（**纯 presence，不看序**） | `firstPrelude >= 0 && lastExec > firstPrelude`（**强制序**） | **偏离（新增语义）** | 设计稿 §② 写「侦察→执行序」，0.1.0 的测试也钉了反序 → 0。**上游反序同样计分**，故序约束是本件新加的 |
| 7 | 规则 B 的 `prelude_signals = {看看,检查,查看,看一下,cat,ls}` | `PRELUDE_TOOLS = {read_file,read,ls,glob,grep,list_files,cat}` | **偏离（更宽）** | 上游是**字面量信号名**，本件收工具名。两侧交集仅 `cat/ls`。**实测：上游对 `[read_file,read_file,bash]` 得 0.00，本件得 0.30** |
| 8 | 规则 B 的 `exec_signals = {执行,运行,run,execute,bash,shell}` | `EXEC_TOOLS = {bash,pwsh,run_code,shell,execute,node,python}`（0.1.1 收窄） | **偏离（0.1.1 变更）** | 0.1.0 曾含 `write_file/write/edit`，导致「先看后改」恒得 +0.3。见 §三 |
| 9 | 规则 B 权重 `+0.3` | 同 | 等距 | 未调参 |
| 10 | 规则 C：`sum(proposed_upgrade)` ≥2 ⇒ `0.2*up_count` | `signals.filter(s=>s.denied)` ≥2 ⇒ `0.2*upCount` | 等距（判据来源偏离） | 权重未动；**`denied` 的判定**见 §二 |
| 11 | `min(score, 1.0)` 封顶 | `Math.min(score, 1.0)` | 等距 | — |
| 12 | `critical ≥ 0.7`、`suspicious ≥ 0.4` | 同 | 等距 | 锚：`阈值：0.39/0.4/0.7` |
| 13 | `get_threat_level` 无会话返回 `(0.0, "normal")` | `level()` 无会话返回 `{score:0, level:'normal'}` | 等距 | — |
| 14 | `reset(session_id=None)` 全清 | `reset(sessionId=null)` 全清 | 等距 | 锚：`reset 清空会话` |
| 15 | 上游 docstring 写规则 A 为 `+0.2/次`，**代码实为 `0.1*(c-2)`** | 代码同上游；注释写对（+0.1） | 等距（上游注释有误） | 上游 docstring 与代码不符，本件不继承该笔误 |
| 16 | 无 `breakdown` 面 | 新增 `breakdown()`（三条规则各自贡献） | **新增** | 审计用；链目 `rules` 字段取自它 |
| 17 | 无链、无 veto、无分档迁移事件 | `threat_level` 链目 + 可选 `vetoOnCritical` | **新增** | 设计稿 §④⑤；锚：`controller：等级迁移写链` |
| 18 | `plan_gate.py` 的「目标形态 ∧ 动词」（拆解前闸） | **不移植** | 未移植 | 设计稿已申报：dsh 无 decompose 面 |

## 二、`denied` 的判据（本件最大的一处语义偏离）

上游 `proposed_upgrade` 是**明确的权限事件**——调用方自己声明「我要求了更高档位」。
dsh 侧 `tools/result` 只给 `isError`，**而 `isError` 包含普通故障**（网络失败、路径不
存在、磁盘满）。

0.1.0 直接把 `isError` 当被拒 ⇒ 规则 C 被普通故障灌满：

```
read×5 → write 失败×2（ENOSPC）  ⇒  A(0.1)+B(0.3)+C(0.4) = 0.80 ⇒ critical
一旦 vetoOnCritical=true ⇒ 会话被守卫锁死，只能人工 /threat reset
```

**"磁盘满"变成"守卫停摆"** —— 这是守卫自己制造停摆，与「降档必须可恢复」同族的失败模式。

**0.1.1 修法**：只有**像守卫拒绝**的错误才计入。判据 `DEFAULT_DENY_PATTERN`
（`core.js`）识别三个守卫现有的拒绝措辞 `<plugin> denied`，可用
`config.denyPattern` 覆盖以适配其它拒绝源。

**这是一条启发式，不是干净信号，如实申报。** 更彻底的做法是让 `tools/result`
或其上游提供真实的拒绝事件；在那之前本判据是"够用且保守"的选择——**倾向漏判而非误判**，
因为误判的代价是锁死会话。

## 三、规则 B 执行族收窄（0.1.1）

0.1.0 的 `EXEC_TOOLS` 含 `write_file/write/edit`，于是：

```
read×3 → write  = 0.40 ⇒ suspicious     （任何编码会话的默认循环）
read×5 → edit   = 0.60 ⇒ suspicious
```

收窄为「运行代码」类（`bash/pwsh/run_code/shell/execute/node/python`）后：

```
read×3 → write  = 0.10 ⇒ normal   ✓
read×5 → edit   = 0.30 ⇒ normal   ✓
read×3 → bash   = 0.40 ⇒ suspicious  ✓  （真形态仍然保留）
```

理由：侦察→执行的形态要成立，「执行」指的是**运行代码**这类高后果能力，不是写自己
正在编辑的文件。写盘行为仍由规则 A/C 与形状面覆盖。

## 四、`plan_gate.py` 未移植（照旧申报）

「目标形态 ∧ 动词」是**拆解前闸**的判据，dsh 无 decompose 面。本票只收评分层；
若后续要「目标白名单」判据，另起一票。

## 五、上游没有的语义：自基线与偏离度（0.2.0 新增）

上游 `threat_tracker.py` **没有**基线概念：分数只对绝对阈值（0.40/0.70）。语义三（FINDING-F1 后立项）
是本件的**全新读数层**，不是移植，特此申报以免被当作"漏移植"：

| 项 | 上游 | 本件（0.2.0） | 说明 |
|---|---|---|---|
| 基线 | 无 | 每会话自基线（各分项在自身历史读数上的 `p50`/`p90`） | 不引入新判据；特征向量即三条既有规则 |
| 偏离判据 | 无 | `aboveP90`（当前窗 > 自身历史 p90，**排除当前窗**） | 无新绝对刻度——绝对阈值正是动态范围被吃光的根因（FINDING-F1） |
| 暖机 | 无 | 满 20 条窗口前读数前报 `warming` | 基线排除当前窗，就绪边界因此滞后一个窗口 |
| 作用面 | — | 只进 `threat_level` 链目与状态报告，**不进 veto 路径** | 语义二维持：`vetoOnCritical` 不建议开 |

**威胁模型边界（如实申报）**：自基线抓节奏*突变*；完美延续会话自身节奏的攻击读作正常。
