# FIX-04 丝锥 Fz 被缩放 · 根因与修复报告

> 发现者：防退化评测门禁（`D:\NX_AI\tests\run_eval.py` 首次运行）
> 定位与修复：验收方（DeepSeek Harness），2026-10-16
> 修复前备份：`D:\NX_AI\backup\rule_engine.py.before_FIX04`
> 状态：**已修复并验证；门禁由"意外退化 1"转为"意外退化 0"**

## 一、现象

| 刀具 | 螺距 | 修复前 Fz | 修复前 F | 应 F | 悬伸比 | 判定 |
|---|---|---|---|---|---|---|
| **M3** | 0.500 | **0.3750** | 318.3 | 424.4 | 6.67 | ✗ 违反红线 |
| M4 | 0.700 | 0.7000 | 445.6 | 445.6 | 5.50 | OK |
| M5 | 0.800 | 0.8000 | 407.4 | 407.4 | 5.00 | OK |
| M6/M8/M10/M12 | — | 等于螺距 | — | — | 5.00/4.38/4.00/3.75 | OK |

违反任务书红线：「丝锥 Fz = 螺距，绝不缩放（不乘状态/刀径系数），保守靠降转速；
feed = rpm × 螺距（齿数按 1）」。

## 二、根因（`data/rules/rule_engine.py`）

```
Step 7（第 369~384 行）RL-007 悬伸降额：
        feed *= (1 - overhang_param_reduction_pct)      ← 降的是 feed

Step 8（第 386~398 行）fz 从降额后的 feed 反算：
        fz_calc = feed / (rpm * teeth_for_calc)         ← 于是 Fz 被"顺带"缩放
```

丝锥 M3 的 Fz 因此变成 0.5 × 0.75 = **0.375**。

**为什么只有 M3 中招**：它的悬伸比 **6.67D 超过 severe 阈值(6D)**，进入降额分支；
其余丝锥（5.50 / 5.00 / 5.00 / 4.38 / 4.00 / 3.75）都只落在 warn 区间(>4D)，**只告警不降额**。

**为什么长期没被发现**：
修复前后 `F` 都是 **318.3**（848.8×0.375 == 636.6×0.5），材料去除率 MRR 一模一样。
也就是说——**只看进给或 MRR 永远看不出问题**，但丝锥每转进给不等于螺距，
上机结果是乱扣甚至断丝锥。必须断言「Fz == 螺距」这个不变量才能抓住。
另外，原有的单元测试没有遍历**全部**丝锥，只覆盖了部分情形。

## 三、修复

丝锥单独分支：**只降转速，绝不动进给**（进给由螺距几何锁死）

```python
if tool.is_tap:
    _pitch = tool.tap_pitch_mm
    rpm *= (1 - wr["overhang_param_reduction_pct"])
    if _pitch:
        feed = rpm * _pitch
    params.notes.append("RL-007 降额：丝锥只降转速不改进给（Fz=pitch 锁死，红线 RL-002）")
else:
    feed *= (1 - wr["overhang_param_reduction_pct"])
```

## 四、修复后验证（全部 7 把丝锥）

| 刀具 | 螺距 | Fz | F | 应 F | 悬伸比 | rpm | 判定 |
|---|---|---|---|---|---|---|---|
| M3 | 0.500 | 0.5000 | 318.3 | 318.3 | 6.67 | 636.6 | OK |
| M4 | 0.700 | 0.7000 | 445.6 | 445.6 | 5.50 | 636.6 | OK |
| M5 | 0.800 | 0.8000 | 407.4 | 407.4 | 5.00 | 509.3 | OK |
| M6 | 1.000 | 1.0000 | 424.4 | 424.4 | 5.00 | 424.4 | OK |
| M8 | 1.250 | 1.2500 | 397.9 | 397.9 | 4.38 | 318.3 | OK |
| M10 | 1.500 | 1.5000 | 382.0 | 381.9 | 4.00 | 254.6 | OK |
| M12 | 1.750 | 1.7500 | 371.4 | 371.3 | 3.75 | 212.2 | OK |

注意 M3 的 rpm 从 848.8 降到 **636.6**（降 25%）——这才是"保守靠降转速"的正确体现。

门禁结果：
```
防退化评测  通过 12/13   已知未修 1   意外退化 0   退出码 0
  [OK   ] RL-002 丝锥Fz=螺距   全部 7 把丝锥：Fz==螺距 且 F==rpm×螺距
  [KNOWN] RL-006 精加工Ap区间  finishing Ap=4.000mm（要求 0.10~0.30）→ 卡号 FIX-02
  特征化基线：10 个上下文，漂移 0 处
```

## 五、防复发

1. **断言不变量，而不是快照数值**：`Fz == pitch` 是几何不变量，任何缩放都会被抓；
   而"F 的值"是结果，改了系数也可能碰巧相同（本例就是）。
2. **遍历全部同类工具**，不要只测第一把 —— 本次正是"只看第一把"漏掉了 M3（M3 恰好是第三把）。
3. **门禁常态化**：改规则/数据/学习层后必须跑 `run_eval.py` 且"意外退化 = 0"。
4. 遗留待办（已在 FIX-04 卡中派给 Trae）：补遍历全部丝锥的单元测试、
   对照检查钻头分支是否有同类语义问题（降 feed 是否改变了"每转进给"的含义）。

## 六、施工方复核与补测（FIX-04 卡收尾，2026-10-16）

**复核结论**：丝锥只降转速分支在引擎 Step 7b 在位（FIX-03 重构后该逻辑保留，
由 `test_cutting_rules.py` Case 7 丝锥 8D 断言与本节新测试双重覆盖），未改一行业务代码。

**补测**：新增 `tests/test_tap_fz.py`，4 个用例 **4/4 PASSED**：

| # | 守住的语义 | 实测 |
|---|---|---|
| 1 | **遍历全部 7 把丝锥 × 2 工序**（粗/精）：Fz==螺距 且 F==rpm×螺距，数据驱动读库不硬编码清单 | 7 把全过 |
| 2 | **M3 历史 bug 回归**：构造 M3+悬伸 20mm（6.67D severe，复现原事故触发条件）→ 出 error、rpm 降 25%、**Fz 仍=0.5**（绝不许回到 0.375）、F=rpm×0.5 | OK |
| 3 | **遍历全部 11 把钻头**：F/(rpm×fz)==1.000（齿数=1，无误乘 2） | 11 把全过 |
| 4 | 钻头降额语义对照（见下） | feed -25% 合法、fz 同步、公式自洽 |

**钻头同类检查结论（卡片第 3 条）**：钻头分支**无同类语义 bug**，无需修复。
本质区别在于不变量是否存在：
- 丝锥 `Fz = 螺距`是**几何锁死**（螺纹牙距决定每转必须前进一个螺距），降 feed 必然乱扣 → 只能降转速；
- 钻头 `fz = 每转进给`是**工艺参数**（无几何锁死），真悬伸时降 feed 25% 是合法保守手段，
  且降额后 `F = rpm × fz`（齿数=1）仍严格成立（实测 E10+悬伸 70mm：feed 191.0→143.2，fz 0.08→0.06，公式自洽）。
- 共同的纪律：两者都不许"误乘齿数"（钻/攻 teeth_for_calc 强制=1），已由 Case 3 全量守住。

验收命令：`python D:\NX_AI\tests\test_tap_fz.py`