# 修复记录：M1 因上游未发布 Q3 数据而失败（2026-10-12）

## 现象

```
state   : failed
stage   : M1 数据刷新
message : M1 数据刷新 失败，退出码 1；旧看板仍可用
```

## 根因

`src\fundamental_mvp\financial_sector.py` 第 74 行的 merge 崩溃：

```
KeyError: 'secucode'
```

完整链条：

1. 通达信新下载的**专业财务数据**里出现了 **2026-09-30（Q3）** 报告期，
   于是 `financial_statement_pit.parquet` 的最新报告期从 `2026-06-30` 变成 `2026-09-30`。
2. `refresh_financial_sector_data` 先做缓存检查：本地缓存的金融行业指标是 `2026-06-30`，
   与新报告期不一致 → **跳过缓存，去调上游 API**。
3. 上游是东方财富 `datacenter.eastmoney.com` 的 `RPT_F10_FINANCE_MAINFINADATA` 接口。
   实测该接口对 `REPORT_DATE='2026-09-30'` 返回 **`success=False`、0 条记录**。
4. `rows` 为空 → `pd.DataFrame([])` 是**零列**的 DataFrame → `rename` 无事可做 →
   `financial.merge(frame, on="secucode")` 抛 `KeyError: 'secucode'`。

**根本原因是数据条件变化，不是代码回归**：银行/证券/保险的 Q3 指标尚未披露
（A 股 Q3 报告披露截止为 10 月底），所以上游暂时没有这一期数据。

> 这与 `audit_legacy_csv` 的 `KeyError: 'source_code'` 是**完全同一类 bug**：
> 空 `pd.DataFrame([])` 没有列，后续按列 merge 就崩。

### 实测证据

| REPORT_DATE | 接口返回 |
| --- | --- |
| `2026-09-30`（Q3） | `success=False`，0 条 |
| `2026-06-30`（Q2） | `success=True`，3 条（含 600036/601398/601318） |

项目自身的 `_fetch_chunk('2026-09-30')` 返回 `[]`，
`pd.DataFrame([])` 的 `shape` 为 `(0, 0)`，**列名列表为空**。

## 处置（用户先选"失败"，后改为"跳过 + 警告"）

用户最初选择「继续失败，但报错说清楚」。实测后发现代价比预想重：
**M1 失败会连坐 M2 与看板**，看板会一直停在 10-08 直到 10 月底上游发布。
用户随后改为：**上游缺该期时跳过重建 + 记警告**，让链路能继续。

关键认识：**保留 06-30 那张表并不是伪造数据**——它的 `report_period` 字段
仍然是 `2026-06-30`，任何人都看得出是 Q2。真正不可接受的是"把 Q2 改标成 Q3"，
而那一步不需要做。

### 改动 1：`financial_sector.py` 增加跳过逻辑

merge 之前加守卫。当取数为空（`frame.empty` 或缺少 `secucode` 列）时：

- **有缓存** → 原样沿用（**不改任何字段、不改期数**），
  返回结果并带上 `warning` 说明；`rebuilt=False`
- **无缓存** → 仍然抛错（没有可讲真话的东西可产出）

### 改动 2：`FinancialSectorDataResult` 增加 `warning` 字段

### 改动 3：`run_m1.py` 把警告接到两处

- `m1_run_summary.json` 的 `financial_sector_build.warning`
- **追加到 `summary["limitations"]`** —— 看板的「解释边界」面板就是渲染这个字段，
  所以警告会直接出现在用户会看的地方

### 实测效果（2026-10-12 20:45–20:55，exit 0）

```
看板 asOf                : 2026-10-12    ← 恢复正常更新
金融行业指标表 report_period: 2026-06-30    ← 未被改标 ✅
financial_sector_build.warning : 已写入
limitations 条数         : 7（新增金融那一期警告）
公网页面中警告            : 已确认存在
```

## 预期行为

在东方财富发布 2026-09-30 期金融行业指标之前（A股 Q3 披露截止为 10 月底），
每次日更会**照常完成**，只是金融行业指标沿用 06-30 期并在看板「解释边界」里标注。
上游发布后无需改任何东西，重跑即自动更新。

---

# 同期修复：看门狗误杀健康隧道（2026-10-12）

## 现象

`public_url.txt` 被写成 `tunnel was dead; restarting...`，
但**那条隧道其实是活的**（`operator-pattern-variety-soccer` 实测 HTTP 200、DNS 正常）。

## 根因：我的健康检查太敏感

新看门狗用「本地通 + 公网不通」判定隧道死亡，但阈值只有 **2 次**失败。

隧道日志显示 15:43 前后存在**短暂的 QUIC 抖动**，且**它自己重连成功了**：

```
2026-10-12T07:43 ERR accept stream listener error ... timeout: no recent network activity
2026-10-12T07:43 INF Registered tunnel connection      <- 自己恢复了
```

看门狗正好在那两分钟探测，连续 2 次失败 → 误判死亡。

**更糟的是它还把 `public_url.txt` 覆盖成占位文字**，导致即使隧道是好的，
**可用的地址记录也丢了**。

## 修复

1. **提高阈值**：连续失败从 2 次改为 **5 次**（约 5 分钟持续故障）。
2. **每次探测加重试**：单次探测失败后等 15 秒再探一次，两次都失败才算一次失败。
   QUIC 抖动的典型持续只有几秒到几十秒。
3. **不再覆盖 `public_url.txt`**：占位文字由 `start_tunnel.ps1` 在真正新建隧道时处理。
   看门狗只负责在确认死亡后杀掉进程。

## 验证

- 观察 240 秒（8 次检查）：看门狗存活、**地址完好、隧道未被误杀** ✅
- 公网稳定性 5/5 ✅
- 看门狗实例数 1 ✅

---

# 顺手修复：health_check 的 runId 误报

失败时 `update_status.json` 根本不会写入 `runId`（没走到那一步），
但健康检查仍拿它和 `version.json` 比较，产生「runId 不一致」的噪声告警。

已改为**仅在 `state == 'success'` 时**比较 runId。

## 当前状态（2026-10-12 20:40）

```
M1/日更     : 失败（上游缺 Q3 数据，预期行为）
看板 asOf   : 2026-10-08（旧看板保留可用）
公网地址    : https://operator-pattern-variety-soccer.trycloudflare.com
公网探测    : HTTP 200，稳定性 5/5
看门狗      : 1 实例（修复版）
隧道        : 2 进程
健康检查    : FAIL（真实失败：state != success）
```

## 另一个教训

排查过程中我又一次写了**转义引号**的进程检查：

```powershell
@(Get-CimInstance Win32_Process -Filter \"Name='wscript.exe'\" ...).Count   # ❌ 恒为 0
```

得到「看门狗实例 0」的假结果。**这是第三次犯同一个错误**
（9-17、10-08 各一次，都已记入运维手册）。
必须写成变量：`$filter="Name='wscript.exe'"; ... -Filter $filter`。
