Files
WQ_GUI/.qwen/skills/facade_kwargs_defense/SKILL.md

310 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: PipelineRunner Facade 防御性 kwargs 兜底
description: WQ_GUI 14 个 stepX_... Facade 方法必须以 **kwargs 收尾——配合 PipelineRunner 调度模式杜绝 "unexpected keyword argument" TypeError
source: auto-skill
extracted_at: '2026-06-04T00:54:50.036Z'
---
# PipelineRunner Facade 防御性 kwargs 兜底
## 适用场景
在 WQ_GUI 中,**任何被 `PipelineRunner` 调用的 14 个 `stepX_...` Facade 方法**(位于 `src/core/water_quality_inversion_pipeline_GUI.py`),其形参表末尾**必须**带 `**kwargs`。触发信号:
- 用户报错 `TypeError: stepX_xxx() got an unexpected keyword argument 'yyy'`
- 改 PIPELINE_STEPS 的 `requires` 列表
- 新增 / 重命名一个 step 方法
- 重构 PipelineRunner 的 `_invoke` 注入逻辑
## 核心原则
**Facade 的形参表 = 显式声明的形参 + `, **kwargs`**`kwargs` 必须**严格位于形参表最后**Python 语法硬要求)。
```python
# ✅ 正确
def step3_remove_glint(self, img_path: str,
method: str = "subtract_nir",
# ... 30+ 业务形参 ...
skip_dependency_check: bool = False,
**kwargs) -> str:
...
# ❌ 错误:**kwargs 不能放中间或前面
def step3_remove_glint(self, img_path, **kwargs, skip_dependency_check): # SyntaxError
```
## 为什么需要这层防御
`PipelineRunner._invoke``src/core/pipeline/runner.py`)会向方法注入两类参数:
| 层 | 来源 | 形参 key 怎么定 |
|---|---|---|
| **L2** | ctx 字段(按 `spec.requires` 列表) | `_default_param_name(ctx_key)` 默认去 `_path` 后缀 |
| **L3** | `ctx.user_config[step_id]`14 panel dict 整体) | dict 的 key 原样注入 |
**L2 触发 TypeError 的真实场景**2026-06-04 真实发生):
- `PIPELINE_STEPS.step3.requires = ["img_path", "water_mask_path", "glint_mask_path"]`
- Runner 注入 `kwargs["glint_mask_path"] = ctx.glint_mask_path`
- `step3_remove_glint` 形参表**没有** `glint_mask_path`(虽然业务上耀斑掩膜是在子调用 `GlintRemovalStep.run` 内部用的Facade 本身不接)
-**TypeError: step3_remove_glint() got an unexpected keyword argument 'glint_mask_path'**
**L3 触发 TypeError 的场景**
- user_config 14 panel dict 里残留了旧字段名 / 跨 step 串味的字段
- 任何 `user_config[step_id][k]` 中的 `k` 都会被注入
`**kwargs` 一次性解决两类问题。
## 14 个 Facade 方法清单(截至 2026-06-04 已全部带 **kwargs
| step | method | 形参表闭合示例 |
|---|---|---|
| 1 | `step1_generate_water_mask` | `output_path: Optional[str] = None, **kwargs) -> str:` |
| 2 | `step2_find_glint_area` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 3 | `step3_remove_glint` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 4 | `step4_process_csv` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 5 | `step5_extract_training_spectra` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 5.5 | `step5_5_calculate_water_quality_indices` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 6 | `step6_train_models` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 6.5 | `step6_5_non_empirical_modeling` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` |
| 6.75 | `step6_75_custom_regression` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 7 | `step7_generate_sampling_points` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
| 8 | `step8_predict_water_quality` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` |
| 8.5 | `step8_5_predict_with_non_empirical_models` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` |
| 8.75 | `step8_75_predict_with_custom_regression` | `skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]:` |
| 9 | `step9_generate_distribution_map` | `skip_dependency_check: bool = False, **kwargs) -> str:` |
## 标准操作
### 1. 编辑(最小外科手术式)
每个方法的最后形参是 `skip_dependency_check: bool = False`,把这一行改成:
```python
skip_dependency_check: bool = False, **kwargs) -> str:
```
**注意缩进必须与原行一致**13 空格 / 35 空格 / 47 空格 / 48 空格不等,按方法原始缩进)。用 `edit` 工具的 old_string **必须含 docstring 第一行**`"""步骤X: ..."""`)作唯一标识。
### 2. 验证
写一个临时校验脚本(项目根目录运行后删掉):
```python
import ast, re
target = r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\core\water_quality_inversion_pipeline_GUI.py'
src = open(target, encoding='utf-8-sig').read()
ast.parse(src) # AST 语法
expected = [
'step1_generate_water_mask', 'step2_find_glint_area', 'step3_remove_glint',
'step4_process_csv', 'step5_extract_training_spectra',
'step5_5_calculate_water_quality_indices', 'step6_train_models',
'step7_generate_sampling_points', 'step8_predict_water_quality',
'step9_generate_distribution_map', 'step6_5_non_empirical_modeling',
'step6_75_custom_regression', 'step8_5_predict_with_non_empirical_models',
'step8_75_predict_with_custom_regression',
]
pat = re.compile(r'def\s+(\w+)\s*\((?:[^()]|\([^()]*\))*\*\*kwargs\)\s*->\s*[^:]+:', re.DOTALL)
found = set(pat.findall(src))
print('Missing:', [m for m in expected if m not in found])
print('Extra :', [m for m in found if m not in expected])
```
期望输出:两个列表都为空。
### 3. Windows 执行
```bat
cd /d D:\111\office\ZHLduijie\1.WQ\WQ_GUI
py _check.py
del _check.py
```
> 必加 `utf-8-sig``water_quality_inversion_pipeline_GUI.py` 头部可能含 BOM`code_replacement_state_audit` skill 里有同样提示)。
## ⚠️ 这层防御不能解决什么
**` **kwargs` 兜底 ≠ 形参名错位修复**。如果 Runner 注入 `kwargs["training_csv_path"]` 而方法形参是 `csv_path`
-**不会报 TypeError**`training_csv_path``**kwargs` 收走)
-**但 `csv_path` 仍是 None**(方法体内的 `if csv_path is not None: ... else: ...` 走 fallback 分支,可能读 `self.training_csv_path` 哨兵)
**已知形参名错位的方法**2026-06-04 已修commit `64aa5b8`
| step | Runner 注入的 ctx key | 方法实际形参 | 实际落地的修复 |
|---|---|---|---|
| step6_5 | `training_csv_path` | `csv_path` | `parameter_map={"training_csv_path": "csv_path"}` |
| step6_75 | (已切到 `indices_path` | `csv_path` | ⚠️ 见下方"step6_75 路由修复"专题 |
| step8_5 | `models_dir` | `non_empirical_models_dir` | `parameter_map={"models_dir": "non_empirical_models_dir"}` |
| step8_75 | `models_dir` | `custom_regression_dir` | `parameter_map={"models_dir": "custom_regression_dir"}` |
> **parameter_map** 是 `StepSpec` 已有字段runner.py:33作用是把 ctx 字段重命名到方法形参名。**优先用 parameter_map 而非改 requires**——保持 ctx 字段语义清晰(声明式描述上游依赖),形参名是方法私有约定。
### step6_75 路由修复特殊案例2026-06-04 commit `64aa5b8`
`step6_75_custom_regression` **不是简单的 ctx 字段名错位**——方法体内的 fallback 链透露了**真正的数据源是 `indices_path`**
```python
# step6_75 形参csv_path
# 方法体 fallback:
# if csv_path is not None: input_csv = csv_path
# elif self.indices_path is not None: input_csv = self.indices_path # ★ 真相
```
`parameter_map``training_csv_path → csv_path` 看似能跑通,但**实际用错了数据**training_csv 是 step5 输出,不是 step6_75 想要的 indices CSV
**正确做法 = 同时改 requires + parameter_map**
```python
StepSpec(
step_id="step6_75", method_name="step6_75_custom_regression",
requires=["indices_path"], # ★ 从 training_csv_path 切到 indices_path
produces=["models_dir"],
parameter_map={"indices_path": "csv_path"}, # ★ 同步改 key
description="自定义回归分析",
),
```
**配合 `skip_when_missing` 兜底**:若用户没跑 step5_5`ctx.indices_path` 为 Nonerunner 自动 skip 整个 step6_75不会用错位数据静默执行。
**判别何时需要"路由切"vs"纯 rename"**
- 看方法体 fallback 链fallback 到 `self.indices_path`/`self.deglint_img_path`/其他 ctx 字段名 → **需要改 requires**
- 仅是 key 名字不同,方法体直接用形参 → 只改 parameter_map
## L2 注入顺序冲突:多个 requires 字段解析到同一形参名
### 场景
`StepSpec.requires` 里有**多个 ctx 字段**,经过 `_default_param_name` / `parameter_map` 解析后,**会落到同一个方法形参名**。L2 注入是**顺序敏感**的(后者覆盖前者),后注入的会**默默覆盖**前一个的赋值。
### 真实案例2026-06-04 step5 修复)
业务需求step5 真正需要 step4 产物 `processed_csv_path`,但**保留 raw `csv_path` 字段**作为 `user_config` 覆盖入口。
**❌ 错误的 parameter_map 写法**(用户原方案的隐藏 bug
```python
StepSpec(
step_id="step5", method_name="step5_extract_training_spectra",
requires=["deglint_img_path", "processed_csv_path", "csv_path", ...], # raw csv_path 也在
parameter_map={"processed_csv_path": "csv_path"}, # ★ 只映射了一个
...
)
```
L2 注入顺序(`runner.py:184-186`
1. `deglint_img_path``kwargs[deglint_img_path] = ctx.deglint_img_path`
2. `processed_csv_path``kwargs[csv_path] = ctx.processed_csv_path` ← 主路径生效
3. `csv_path`(无映射 → 默认)→ `kwargs[csv_path] = ctx.csv_path`**后注入 None 覆盖了主路径!**
**症状**step5 形参 `csv_path` 拿到的是 raw `ctx.csv_path`(通常是 None方法体 fallback 到 `self.processed_csv_path`——但这个 fallback 也可能是 Nonestep4 没跑step5 内部空跑 → "**静默错误**"。
**✅ 修法parameter_map 双向映射 + 占位名落 **kwargs**
```python
parameter_map={
"processed_csv_path": "csv_path", # 主路径(注入到方法形参)
"csv_path": "_raw_csv_ignored", # 占位(落到 step5 形参列表末尾的 **kwargs
},
```
注入顺序重排后:
- `processed_csv_path``kwargs[csv_path] = ctx.processed_csv_path` ← 主路径
- `csv_path``kwargs[_raw_csv_ignored] = ctx.csv_path` ← 落 **kwargs被吞
step5 形参 `csv_path` 最终拿到 `ctx.processed_csv_path` 的值 ✓。
### 验证模板(行为模拟)
写临时 `_verify_l2_inject.py` 复刻 `runner.py:184-186` 的 L2 注入循环,**不要只靠 AST 静态检查**——parameter_map 的 key 顺序、requires 的字段顺序都是动态的:
```python
import sys
sys.path.insert(0, r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI')
from src.core.pipeline.context import PipelineContext
from src.core.pipeline.runner import PIPELINE_STEPS
spec5 = next(s for s in PIPELINE_STEPS if s.step_id == 'step5')
# 复刻 L2 注入(与 runner.py:184-186 完全一致)
def l2_inject(spec, ctx):
kwargs = {}
for ctx_key in spec.requires:
param_name = spec.parameter_map.get(ctx_key, ctx_key) # ★ 必须原样复刻
kwargs[param_name] = ctx.get(ctx_key)
return kwargs
# 关键断言
ctx = PipelineContext(processed_csv_path='/csv/processed.csv', csv_path='/csv/raw.csv')
kw = l2_inject(spec5, ctx)
assert kw.get('csv_path') == '/csv/processed.csv', \
f"形参 csv_path 应等于 processed_csv_path, 实际 {kw.get('csv_path')!r}"
assert '_raw_csv_ignored' in kw, "占位名应被注入到 kwargs"
print(f'OK: csv_path 形参 = {kw["csv_path"]!r} (processed, 主路径正确)')
print(f'OK: _raw_csv_ignored 占位 = {kw["_raw_csv_ignored"]!r} (raw, 落 **kwargs 被吞)')
```
跑完删掉:`py _verify_l2_inject.py & del _verify_l2_inject.py`Windows 一行模式)。
### 何时需要警惕这个冲突
修改 StepSpec 时检查清单(**先看这一段再写 parameter_map**
- [ ] **requires 里是否有多于 1 个 ctx 字段,解析后会落到同名方法形参?** 典型撞车:
- 同名字段(如 `processed_csv_path``csv_path` 都能映射到 `csv_path`
- 同名 `_default_param_name` 退化(如 `boundary_path``boundary_shp_path` 默认都映射到 `boundary_path`——但要注意 `_default_param_name` 已废弃去后缀,原样返回 ctx key所以 `boundary_path``boundary_shp_path` 默认就会撞 `boundary_path` / `boundary_shp_path` 不会撞,要撞就必须显式 parameter_map
- 字段名 + parameter_map 重命名撞车
- [ ] **"主路径"字段在 requires 列表靠前位置**(让后续"备路径"覆盖,但**这不解决冲突**——只要有第二次注入就一定会覆盖)
- [ ] **"备路径"字段**用占位名 `_xxx_ignored` / `_xxx_kwargs_only` 映射,让它落到 **kwargs
- [ ] **确认方法形参表末尾有 `**kwargs`** 兜底(`facade_kwargs_defense` skill 核心要求,已 14/14 落地)
### 反例(不要做)
- ❌ "我让 `csv_path` 不在 requires 里就行了"——会**丢失 user_config 覆盖入口**(如果用户想用 raw CSV 而不是 processed
- ❌ "改 L2 注入循环,让 parameter_map 字段最后注入"——会**改变 runner 通用语义**,影响所有 step 的注入顺序
- ❌ "加 `if param_name in kwargs: continue` 在 L2 注入里"——隐式"第一次优先"语义,新人读代码摸不着头脑
- ❌ "用 position in requires 做加权"——把数据语义哪个字段优先级高塞到列表顺序里runner 应该保持"声明式"
### 与"纯 rename"的区别
| 维度 | 纯 rename已有 skill 案例) | 多→1 冲突(本节案例) |
|---|---|---|
| 典型场景 | step6_5/6_75/8_5/8_751 个 requires 重命名到形参 | step52 个 requires 撞到同一形参 |
| parameter_map | 1 个 key→value | 2 个 key→同名 value + 占位名 |
| requires | 1 个字段 | 2 个字段(主 + 备) |
| 冲突来源 | 不会出现(单 key | 出现(顺序敏感 + 撞名) |
| 修法 | 只加 parameter_map | 双向 parameter_map + 占位名 |
## 与其他防御层的关系
```
PipelineRunner.run() 主循环
├─ L1 runner.py:152 skip_when_missing ─── ctx.<required> 全 None → skip step
├─ L2 runner.py:182 ctx 字段注入 ─── 形参表里没声明 → TypeError ⚠️ → **kwargs 兜底
├─ L3 runner.py:188 user_config 合并 ─── user_config 有"空字符串"/None → 跳过(上一轮加的守卫)✅
└─ L4 runner.py:211 except 捕获 ─── 业务抛异常 → ctx.status="error" + raise
```
`**kwargs`**L2 的"消极兜底"**——宁愿吞掉多余 key 也不报 TypeError。**真正的"积极修复"是 parameter_map**(让 ctx 字段名映射到正确形参名)。两层配合:
- **保守期间(重构初期)**:先 `**kwargs` 兜住TypeError 消失
- **稳定阶段**:补 parameter_map让方法收到正确数据
## 反例(不要做)
- ❌ "不写 `**kwargs`,靠 type hint + IDE 检查兜底"——Runner 是运行时注入IDE 看不到
- ❌ "把 `**kwargs` 放形参表中间"——Python 语法错误
- ❌ "改 requires 列表去掉冗余 ctx 字段"——会导致 `skip_when_missing` 误判(以为 step 不需要该 ctx 字段),应该用 `parameter_map` 重命名而非删除 requires
- ❌ "在 14 个 Facade 方法体里加 `if 'glint_mask_path' in kwargs: kwargs.pop('glint_mask_path')`"——脏活,且每个方法都要加,远不如 `**kwargs` 一行优雅
## 案例来源
- 2026-06-04 WQ_GUI PipelineRunner 迁移第二步
- 触发:`step3_remove_glint() got an unexpected keyword argument 'glint_mask_path'`
- 根因:`PIPELINE_STEPS.step3.requires` 写了 `glint_mask_path`,但 `GlintRemovalStep` 内部使用Facade 自身不接这个形参
- 落地14 个 Facade 全部加 `, **kwargs`0 个 TypeError
- 验证:临时 `_check.py` 14/14 命中 + AST 解析通过
-4 个 parameter_map 全部落地commit `64aa5b8`),含 step6_75 路由切到 indices_pathL3 非空过滤同步加入 `runner._invoke:188`
- 2026-06-04 step5 严格依赖修复:发现 L2 注入顺序冲突requires 多个字段解析到同一形参名),引入"双向 parameter_map + 占位名落 **kwargs"模式step5 形参 `csv_path` 真正接到 `processed_csv_path`step4 产物raw `csv_path` 保留为 user_config 覆盖入口,落占位名 `_raw_csv_ignored` 后被 `**kwargs` 吞。skip_when_missing 块同步加 `_notify` 通知,**拒绝静默跳过**15 条 _notify 全带具体 missing 字段列表证据)。