# 13 · 路线 A · InvestorManifestSource (第 7 个正式数据源)

延续续 11 vendor 4 工具 + 续 12 解耦 2 运行时数据。本轮路线 A:写第 7 个 Source
通杀 5 个 *_manifest.csv (CRS/MDA/VOYG×3/mda-voyg-crs),用 1 个 Source 自动识别
3 schema (A/B/C),events 521 → 855 (+334) 单 commit。

## 1. 3 schema 摸底 (头列字段)

| schema | manifest 数 | 行数 | 主时间列 | 关键字段 |
|---|---:|---:|---|---|
| **A** investor (CRS/MDA/VOYG-inv) | 3 | 355 | `event_date` | fiscal_year/period/event_name/material_type |
| **B** customer (VOYG-cust) | 1 | 296 | `source_date` (常空) | category/source_group/relationship_tags |
| **C** unified_source (mda-voyg-crs) | 1 | 165 | 无, 推 fiscal_year+period | source_skill/document_type/event_or_filing_type |

合计 5 manifest / 816 行。Schema 识别在 csv header 行做一次, 后续每行统一映射 RawEvent。

## 2. 设计要点

**为什么 1 个 Source 而不是 3 个**:
- 3 schema 字段重叠率高 (company / ticker / source_url + 主键 + 时间 + 文档类型)
- operator UX:一条命令 `satagent fetch --source investor-manifest --path X` 通杀, 不用记 schema
- 未来续 11 wrapper 跑完产新 manifest 时, 同一 Source 自动适配

**时间策略 (occurred_at 三档)**:
- A:`event_date` + `T12:00:00`(有就用)
- B:`source_date` 若有 → `T12:00:00`; 年-月退化 → `-01T12:00:00`; 仅年 → `-12-31T12:00:00`; 空 → None (filtered)
- C:无主时间列, 用 `fiscal_year + period` 推到季末:Q1→`{year}-03-31` / Q2→`-06-30` /
  Q3→`-09-30` / Q4→`-12-31` / H1→`-06-30` / H2→`-12-31` / FY→`-12-31`

**不 hard-code mainline / impact**:跟 ShockEvents 不同, investor 材料是混合主题,
classifier 按 text 判更准; source 层只透传给 classifier。

**download_status 过滤**:
- A/C 收 `OK`
- B 收 `downloaded` 或 `OK`(B 是 api_json 状态字符串)
- 其他状态 (FAIL/SKIPPED/404) 默认跳过, `--include-failed` 显式开

**companies 透传**:`company` 字段塞 `RawEvent.companies`, 让上层 match_companies 优先用。
**警告**:CRS/MDA/VOYG 不在 SEED_COMPANIES (47 家中文卫星公司), 反查不到, thread_enrich 不会走。

## 3. 实施 (4 处联动)

### sources/investor_manifest.py 280 行
- `detect_schema(fieldnames)` → "A" / "B" / "C" / None
- `_period_to_date(fy, period)` C schema 兜底
- `_occurred_at_a/b/c(row)` 3 schema 各自的时间策略
- `_accept_status(schema, status)` A/C 收 OK; B 收 OK + downloaded
- 主类 `InvestorManifestSource(path, since_row, limit, include_failed)`

### sources/__init__.py
- 4 别名 `investor-manifest / investor_manifest / manifest / im`

### cli.py
- `fetch --source investor-manifest --path X` 必须给 path (no env default, 没意义)

### jobs.py
- `_action_investor_manifest_ingest` + `register_action('investor-manifest-ingest')`
- params: `{path, since_row, limit, include_failed, use_llm}`

## 4. 单测 (13 个全过, 全仓 363 → 376 passed)

- test_detect_schema:3 schema fixture + 不匹配 → None
- test_period_to_date:Q1/Q2/Q3/Q4/H1/H2/FY/小写/坏 input 9 例
- test_schema_a_basic_parse:4 fixture → 2 emit (1 FAIL filtered + 1 no-date filtered)
- test_schema_a_include_failed:--include-failed 后 FAIL 也进来
- test_schema_b_basic_parse:5 fixture (含 status=404 filtered, source_date 空 filtered, 年-月退化 OK)
- test_schema_c_basic_parse:5 fixture (Q1/Q4/FY 季末 / 坏 period 坏 year filtered)
- test_missing_file / test_unrecognized_schema / test_since_row_and_limit
- test_load_source_aliases / test_jobs_action_registered / test_jobs_action_callable_via_inmemory_db
- test_doc_type_labels_used_in_title

## 5. Smoke 真跑 5 manifest 结果

| manifest | schema | fetched | emit | FAIL filt | no-date filt | 入 events |
|---|---|---:|---:|---:|---:|---:|
| CRS investor          | A | 99  | 92  | 7  | 0   | 92  |
| MDA investor          | A | 191 | 97  | 94 | 0   | 94 (3 dup) |
| VOYG investor         | A | 62  | 41  | 21 | 0   | 41  |
| VOYG customer         | B | 295 | 0   | 26 | 269 | **0** ⚠ |
| mda-voyg-crs unified  | C | 164 | 107 | 8  | 49  | 107 |
| **合计**              |   | **811** | **337** | **156** | **318** | **334** |

events 521 → **855** (+334);3 重复在 MDA 内 (同一 event 多个 material 类型挂同 url)。

## 6. VOYG customer 0 入分析 (新发现)

VOYG customer 295/295 行 `source_date` 全空 → 全 filtered_no_date。这是数据本身缺陷:
manifest 设计时没填日期 (api_json 类的 usaspending 抓取没记录抓取时间), 不是 source bug。

**下一轮处理选项**:
1. 加 `--fallback-date YYYY-MM-DD` 参数, operator 兜底打成 "今天事件" (有假事件风险)
2. 走路线 B LLM 抽取 PDF/JSON 内容推断时间
3. 接受 0 入, source 标签里加 "*no-date-skip*" 让上层知道

## 7. 新发现的 P1 暗坑 · mainline 命中率

334 个 InvestorManifest events **0 命中**任一主线 → 整体 mainline 命中率从 33% 稀释到 20%
(174/855 events 有主线分流)。

**原因**:CRS/MDA/VOYG 不在 SEED_COMPANIES,enrich_with_company_threads 没法反推主线;
事件 text 也短 (只是 source_title + material_type), 关键词命中 4-5 主线规则少。

**下一轮处理选项**:
1. 扩 SEED_COMPANIES 加 CRS/MDA/VOYG 美国公司 + ontology 加英文关键词
2. 走路线 B LLM 抽取 PDF 内容补充 text 密度
3. 接受规则版命中率, 等 D V2 真 LLM 接入

## 8. 后续路线

- **VOYG customer 时间兜底** (1-2 小时)
- **SEED_COMPANIES 扩美国公司** + ontology 英文关键词 (半天 · 让 334 events 命中主线)
- **路线 B LLM 抽取** MDA 中文投研 xlsx + SpaceX briefing PDF (3-5 天)
- **顶层 4 zip/xlsx 内容**(MDA 中文投研 xlsx 优先, VOYG⭐ 16 MB / MDA quarterly 6 MB / orbit-wars 7 KB)

## 9. 经验

- **schema auto-detect 用 header 关键字** 比 ext 名 / 路径 推更可靠 (3 schema 都是 csv 但内容不同)
- **C schema 兜底日期** 用 fiscal_year+period → 季末:商业逻辑符合财报披露通常对应季末
- **status 过滤口径不同 schema 不一样** (A/C "OK" / B "downloaded") 因为 B 是 api_json 抓取脚本输出
- **stats 通过 jobs action 暴露而非 CLI fetch** — CLI 当前只看 ingest_pipeline 标准返回; 想看 stats 用 `satagent job add → run` 或读 jobs.py 返回
- **mainline 命中率不能光看 source 入库数** — 334 入库但 0 主线 是产品级问题,不是 source bug; source 把数据递好, classifier 命不中是另一层
- **大批数据时, 直接看 sqlite3 比 CLI 输出有效** (`SELECT thread, COUNT(*) FROM event_threads et JOIN events e WHERE source LIKE 'InvestorManifest%' GROUP BY thread`)
