# 11 · D 路 · ShockEventsSource (第 6 个正式数据源)

延续 09 / 10 (HarnessFarm bridge → C 路 jsonl source) 的同一管线模板:这次把
HarnessFarm-skills/ 下的策展冲击事件 csv 升级成**第 6 个正式 Source**, 与
jsonl / text / rss / x-sqlite / harnessfarm 平权。

输入: `public/HarnessFarm-skills/commercial-space-satellite-internet-shock-events/events.csv`
(21 条人工策展, 近 5 年 2021-2026, 严重性≥中, 全负面冲击事件)

输出: `agent/satellite_agent/sources/shock_events.py` + 3 处联动 + 10 单测 + data.html 4→5 live

## 1. 选这条做的理由

C 路 (10-c-route-harnessfarm-source.md) §6 "后续接入点" 列了 3 个候选:
- **shock-events.csv (22 条) → 第 6 source** ← 本轮
- MDA / VOYG / CRS investor materials (半结构化, 需 LLM 抽取)
- SpaceX orbital AI compute briefing (单份长文, 人工 review)

shock-events 是结构最干净的 csv (8 列 enum + 多类目 ; 分), 跟 HarnessFarm jsonl
schema 是平级的人工策展输出, 实现成 Source 几乎纯映射逻辑。优先做这条是因为:
- 全负面冲击事件 → 给 thesis 削弱预警和 trigger 提供天然 ground truth
- 5 主线全覆盖 (Blue Origin / SpaceX / Starlink / OneWeb / Astra / Virgin Orbit /
  ViaSat / Intelsat 等都在内)
- csv 比 jsonl 更稳, DictReader 兼容性强

## 2. schema 映射核心

| csv 列 | 处理 |
|---|---|
| id (E01-E21) | since_id 字符串排序游标 |
| date | `_normalize_occurred_at`:`YYYY-MM-DD` → +T12:00:00; `'2025-04 and 2025-09'` 退化取最后一个 `YYYY-MM` 补 -01 |
| event_name | title 前缀 `[E0X]` 让反查时一眼区分 |
| category (; 分多类目) | `_infer_mainline`:发射/运载/亚轨道 → 运载发射; 在轨/星座/卫星异常 → 核心网; 其他 → unknown 默认跳过 |
| company_or_program | 拆 ; 进 RawEvent.companies, 给 match_companies 兜底参考 |
| affected_satellite_or_payload | 拼进 body, 让 classifier 看得到 |
| impact_summary | 主 body |
| severity (高/中) | 拼进 body, 让 classifier 看得到 |
| primary_sources | `_extract_first_url` 取第一个 `https?://` 作 url |

**全源硬编码**:source 标签 `ShockEvents · {first_category}`; **impact 默认 削弱**
(全部是事故/失败, 不下穿到 classifier)

## 3. 三入口实测

| 入口 | 命令 | 结果 |
|---|---|---|
| Python | `load_source('se').fetch()` | 20 events 吐, stats 显示 21 fetched / 1 unknown filtered |
| CLI 首跑 | `satagent fetch --source shock-events` | ingested=20 duplicates=0 errors=0 |
| CLI 复跑 | 同上 | ingested=0 duplicates=20 (按 url+occurred_at 判重) |
| jobs | `job add --action shock-events-ingest --params '{"limit":5}' → job run` | status=success, stats.emitted=5 |

## 4. 真 DB ingest 后 events 分布

入 `agent/data/agent.db`:events 501 → **521** (+20)。

ShockEvents 5 主线分布(按 event_threads):

| 主线 | 计数 | 代表事件 |
|---|---:|---|
| 运载发射 | 15 | NG-4 静态点火 / NG-3 入错轨 / Starlink G9-3 上面级异常 / Virgin Orbit 破产 / Astra 失败 / Firefly Alpha 首飞爆炸 |
| 核心网 | 5 | Starlink 地磁暴 40 颗损失 / ViaSat-3 反射器 / Intelsat 33e 碎裂 / 在轨宽带异常 |
| 运营支撑 | 1 | 资本市场冲击类 |
| 终端 | 1 | (一条提到 direct-to-device 终端的 BlueBird) |

(classifier 会同时给一条事件多个主线, 总数 > 20 是因为部分事件命中多主线)

## 5. 跟 HarnessFarm jsonl source 的对照

| 维度 | HarnessFarm jsonl | ShockEvents csv |
|---|---|---|
| 体量 | 205 (194 in_domain) | 21 (20 in-mainline) |
| 主线分布 | 5 主线全覆盖 | 5 主线全覆盖 (运载发射占主) |
| 哲学 | 94% strengthen (偏强) | 100% 削弱 (全负面) |
| schema | jsonl 多枚举字段 | csv 8 列纯文本 |
| 增量 | since_case_id (R001-R205) | since_id (E01-E21) |
| 过滤开关 | include_out_of_domain / include_unknown_mainline | include_unknown_mainline |
| 别名 | 3 个 (harnessfarm / harness-farm / hf) | 4 个 (shock-events / shock_events / shocks / se) |
| 代码量 | 192 行 | 168 行 (无 LLM 兜底分支, 更薄) |

两个一起灌:events 376 (06-08) → 501 (06-09 续 6 HarnessFarm) → **521** (06-09 续 7
本轮 ShockEvents)。HarnessFarm 偏强 + ShockEvents 全弱, 互相平衡 thesis 评分的
"幸存者偏差"风险。

## 6. 单测 (10 个)

`agent/tests/test_shock_events_source.py`:
1. test_basic_parse · 7 fixture → 5 emit (跳过 unknown + empty)
2. test_include_unknown_mainline · 开关打开后 E05 进来
3. test_since_id_cursor · since_id='E03' 只剩 E04 E07
4. test_limit_early_break · limit=2 emit 2 后立即 break
5. test_date_degraded_takes_last_ym · 退化日期取最后 YM
6. test_date_full_iso · 完整 YYYY-MM-DD +T12:00:00
7. test_missing_file_silent · 路径不存在 yield 0 + last_error
8. test_load_source_aliases · 4 个别名都能起
9. test_jobs_action_registered · ACTIONS 含 'shock-events-ingest'
10. test_jobs_action_callable_via_inmemory_db · jobs e2e 5 条都入

全仓回归 350 passed / 7 失败(litellm 缺包, 历史已知)/ 0 真实回归。

## 7. 下次 cron 启用步骤 (operator)

D 路代码已经在, 但**没有挂 cron**。如需启用增量 daily, operator 自己跑:

```bash
satagent job add --name shock-events-ingest-daily \
  --action shock-events-ingest \
  --params '{}' \
  --schedule '0 5 * * *'
# 然后 (用 launchd 或 cron) 在 05:00 调:
#   /usr/bin/python3 -m satellite_agent.cli job run shock-events-ingest-daily
```

但 shock-events.csv 不是高频更新源 (是人工策展, HarnessFarm 那边定期补新事件后才会涨),
实际**没必要 daily**; operator 看到 HarnessFarm 那边出新 commit 后手动跑一次即可。
所以默认不挂 cron。

## 8. 后续接入点 (本 commit 不做)

C 路 §6 剩下的两个 follow-up 都还在:
- MDA / VOYG / CRS investor materials → 半结构化 md/docx, 需 LLM 抽取
- SpaceX orbital AI compute briefing → 单份长文, 人工 review

也可以倒过来用 ShockEvents 做产品:
- 给每条 shock event 写 trigger (如 NG-4 类爆炸 → 触发 Blue Origin 仓位预警)
- 接进 debate, 让 Bear 引 shock events 作削弱论据
- D V2 真 LLM E2E 时把 ShockEvents 单独当对照 corpus (因为 100% 削弱标签明确)

## 9. 经验

- csv schema 比 jsonl 多枚举混合更简单, 但要处理多类目 ; 分 (我们用关键词匹配, 不用 enum)
- shock events 不需要 expected_analysis ground truth — 源层硬编码 impact=削弱 就够
- ingest_pipeline 按 (url, occurred_at) 判重, fixture 要给每行**唯一 URL**才能跑 e2e
  测试 — 第一次 fixture 用同一 URL 直接导致 e2e 测试 5→2 假阳性, 改 URL=按 id 区分后 OK
- 退化日期 (`2025-04 and 2025-09`) 没必要丢, 取最后一个 `YYYY-MM` 补 -01 比 skip 更稳
- source 标签前缀 `ShockEvents · {category}` 让 events 表反查 `LIKE 'ShockEvents%'`
  就能挑出全部冲击事件子集
