# 代码缺陷报告：`audit_legacy_csv` 在历史数据缺失时崩溃

## 结论摘要

- **失败阶段**：M1 数据刷新（`stageIndex 1/3`）
- **退出码**：`1`
- **分类**：`CODE_ERROR`（提示词第 6 节；程序异常，**不允许重试**）
- **旧看板**：**可用**（`index.html` 980.2 KB 未变，`version.json` 仍是 `5a3c54a0…`，原子替换按设计保留了上一版）
- **是否重试**：未执行（非 `TRANSIENT_NETWORK`）

## 复现证据

`reports\dashboard\logs\update_20260914_160314.log`：

```
[2026-09-14T16:03:14+08:00] START M1 数据刷新
Traceback (most recent call last):
  File "C:\FundamentalWorkbench\run_m1.py", line 26, in main
    summary = run_inventory(settings)
  File "C:\FundamentalWorkbench\src\fundamental_mvp\inventory.py", line 413, in run_inventory
    master, issues, summary = build_security_master(settings)
  File "C:\FundamentalWorkbench\src\fundamental_mvp\inventory.py", line 145, in build_security_master
    master = master.merge(legacy, on="source_code", how="left")
KeyError: 'source_code'
[2026-09-14T16:18:35+08:00] END M1 数据刷新 exit=1 duration=920.6s
```

## 根因

`src\fundamental_mvp\inventory.py`：

```python
def audit_legacy_csv(data_dir: Path) -> pd.DataFrame:
    records = [_csv_edge(path) for path in sorted(data_dir.glob("*.csv")) if not path.name.startswith("idx_")]
    return pd.DataFrame(records)          # 第 53-55 行
```

当 `data_dir` 里**没有任何 .csv**（本机 `data\input` 只有 `README.md`）时，
`records == []`，`pd.DataFrame([])` 返回一个**零列**的 DataFrame。
第 145 行随后用 `on="source_code"` 去 merge，pandas 在右表找不到该列 → `KeyError`。

即：**这个「可选」输入实际上不是可选的**——它只在非空时才不炸。

### 触发条件

`config\local_paths.toml` 的 `legacy_data_dir` 指向一个不存在或为空的目录。本机为 `data/input`（空）。

原发布包的配置指向打包机器的 `E:/Codex_量化/Codex历史文件/bt/data`，那台机器上该目录存在，
所以原作者从未触发这个分支。迁移到干净主机后必然暴露。

### 加重问题：`configure_host.ps1` 会把它重置为默认值

```powershell
$legacyData = if ($LegacyDataDir) { ... } else { "data/input" }
```

不带 `-LegacyDataDir` 参数调用时，会把 `legacy_data_dir` 从原值**重置为空目录 `data/input`**。
本次配置正是如此（`configure_host.ps1 -TdxRoot C:\new_tdx64`）。

## 建议修复（需人工或 Codex 执行，我未改动代码）

按运维提示词第 1 节，`.py` 文件不得由我修改。建议二选一：

### 方案 1：让空输入真正可选（推荐，改 1 行）

`inventory.py` 第 55 行改为保证 schema 存在：

```python
def audit_legacy_csv(data_dir: Path) -> pd.DataFrame:
    records = [_csv_edge(path) for path in sorted(data_dir.glob("*.csv")) if not path.name.startswith("idx_")]
    if not records:
        records = [{
            "source_code": None, "legacy_csv_rows": 0,
            "legacy_csv_first_date": None, "legacy_csv_last_date": None,
            "legacy_csv_header": "", "legacy_csv_size": 0,
        }]
    return pd.DataFrame(records)
```

（或让 `_csv_edge` 返回的全部字段用显式 dtype 建空表。）

### 方案 2：提供真实的历史数据目录

把原机器的 `E:\Codex_量化\Codex历史文件\bt\data` 复制过来，然后用显式参数重配：

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\configure_host.ps1 `
  -TdxRoot "C:\new_tdx64" -LegacyDataDir "<历史数据目录>"
```

注意：该目录里的 `*.csv` 与 `names.json` 会直接影响：
- `legacy_name`（证券名称兜底）
- `has_legacy_csv`、`legacy_csv_covered_stocks` 统计
- **M1 因子/回测的历史深度**

方案 1 能让流水线跑通，但 `has_legacy_csv` 会全为 `False`，历史名称兜底缺失——
属于「能跑但研究证据变弱」，需要在报告中如实标注，不能当成数据完整。

## 附带发现（不阻塞，仅记录）

`vendor\pytdx\reader\block_reader.py:117,128` 有 `SyntaxWarning: "is not" with 'str' literal`。
pytdx 2.0 时代的写法，Python 3.12 下只是警告，不影响运行。属上游依赖问题，不建议改。

## 验收核对（提示词第 4 节）

| 验收项 | 结果 |
| --- | --- |
| `update_status.json.state == "success"` | ❌ `failed` |
| `version` 与 `version.json.version` 一致、`runId` 一致 | ✅ 均为 `5a3c54a0c40f4de3b34fc5af3edf8ff4` |
| `stage == 看板生成`、`stageIndex==3`、`stageCount==3` | ❌ `stage=数据刷新`、`stageIndex=1` |
| `version.json` 含 `generatedAt`/`asOf`/`factorAsOf` | ✅ 均存在 |
| `index.html` 存在且 > 0 | ✅ 980.2 KB |
| 本次 `logPath` 指向存在的日志 | ✅ |
| 上一版看板是否仍可用 | ✅ 可用 |

**结论：本次日更未完成，看板停留在 `asOf 2026-09-11` 的旧快照。**
