# C 路 · HarnessFarmSource 正式纳入数据源体系

> 续上一节 A+B 双路接入。C 路把 HarnessFarm 从"一次性脚本"升级为**第 5 个
> 正式数据源**, 与 jsonl / text / rss / x-sqlite 并列, 支持 CLI + jobs 双入口。

## 1. 改动文件清单 (4 个仓库代码 + 1 个测试 + 1 个文档)

| 文件 | 改动 |
|---|---|
| `agent/satellite_agent/sources/harnessfarm.py` | **新建** 175 行 HarnessFarmSource 实现 |
| `agent/satellite_agent/sources/__init__.py` | `load_source()` 加 `harnessfarm/harness-farm/hf` 3 别名 |
| `agent/satellite_agent/cli.py` | `fetch --source` choices 加 `harnessfarm`; path 可选分支 |
| `agent/satellite_agent/jobs.py` | 新增 `_action_harnessfarm_ingest` + `register_action("harnessfarm-ingest", ...)` |
| `agent/tests/test_harnessfarm_source.py` | **新建** 10 测覆盖 (基础 / OOD / unknown / since 游标 / limit / 缺文件 / 坏 JSON / 3 别名 / jobs 端到端) |
| 本文件 | 沉淀 |

## 2. HarnessFarmSource 设计

复用 X SQLite Source 模式:

| 维度 | 实现 |
|---|---|
| path | `HARNESSFARM_PATH` env / 默认 `../public/HarnessFarm-satellite-agent/.../real_ingest_cases.jsonl` (相对 agent/ cwd) |
| 增量游标 | `since_case_id` (R001 → R002 字符串排序), CLI / jobs 都可传 |
| limit | 单跑 emit N 条提前 break |
| 默认 skip | out_of_domain (11 条负样本) + mainline=unknown (11 条) |
| 可开关 | `include_out_of_domain=True` / `include_unknown_mainline=True` 全收 |
| 错误兜底 | 单行坏 JSON 写 `last_error` 不阻塞;路径不存在 yield 0 + last_error |
| source 标签 | `HarnessFarm · {publisher}` 前缀 |
| occurred_at | publish_date (YYYY-MM-DD) 补 `T12:00:00` |

枚举映射字典定义在 source 模块顶部 (与 `agent/scripts/import_harnessfarm.py` 字典一致, 但**不共享代码** — 字典只有 8 行, 两边各自持有更易测):
```python
MAINLINE_MAP = {"core_network":"核心网", "terminal":"终端",
                "chip":"芯片", "ops_support":"运营支撑", "unknown":None}
IMPACT_MAP = {"strengthen":"增强", "weaken":"削弱", "neutral":"中性"}
```

注: 映射字典写在 source 层 **不下穿到 classifier** — HarnessFarm 的
`expected_analysis` 是 ground truth, 应该走 validate 路径, 不要污染 events
表的 classifier 自动判定。Source 只做 schema 翻译, classifier 仍按文本走规则版。

## 3. 三入口验证

| 入口 | 命令 | 结果 |
|---|---|---|
| Python | `from satellite_agent.sources import load_source; s = load_source('hf')` | path 自动定位, fetch() iterator OK |
| CLI | `satagent fetch --source harnessfarm` | `ingested=0 / duplicates=194` (A 路已灌过, 全数判重, **正确**) |
| jobs | `satagent job add --name X --action harnessfarm-ingest --params '{"limit":5}'` → `job run X` | `success / fetched 5 / emitted 5 / duplicates 5` |

## 4. 单测覆盖 (10/10 passed)

```
test_basic_parse                  6 条 fixture → 3 条 emit
test_include_out_of_domain        OOD 开关
test_include_unknown_mainline     unknown 开关
test_since_case_id                R002 游标 → 只剩 R003
test_limit                        limit=2 提前 break
test_missing_file_silent          缺文件不抛, 留 last_error
test_bad_json_line_does_not_abort 坏 JSON 不阻塞其余
test_load_source_aliases          harnessfarm/harness-farm/hf 三别名
test_jobs_action_registered       'harnessfarm-ingest' in ACTIONS
test_jobs_action_callable_via_inmemory_db  端到端 → ingested=3
```

全仓回归: **316 passed / 2 failed** (litellm 模块缺失, memory 里记的同源问题, 与本次改动无关)。

## 5. 跟 A+B 路的关系

| 路径 | 何时用 | 产物寿命 |
|---|---|---|
| **A** 一次性脚本 | 首次接入做 schema 探查 + dry-run 看分布 | `agent/data/imports/harnessfarm_events.jsonl` 是中间产物, gitignore, 脚本可重生 |
| **B** corpus 升级 | 跨产线 ADVICE 对比, 一次性产出独立 baseline | `agent/samples/labeled_harnessfarm_v1.jsonl` 入 git, 是永久 corpus |
| **C** 正式 Source | 日常 cron / 增量同步 / 跨账号联动 | `sources/harnessfarm.py` 是代码, 永远可调; A 路脚本完成历史使命可保留也可删 |

C 路上后, A 路的 import 脚本理论上可以退役 (因为 source 内置同样映射逻辑)。但保留它做 dry-run 工具仍有价值 — 不入 DB 就能看 mainline / impact 分布,
而 source 入口必须经过 ingest_from_source。

## 6. 后续接入点

C 路只覆盖 HarnessFarm 一个 jsonl。`public/HarnessFarm-skills/` 下还有 18+
skill dir 产物(CRS / MDA / VOYG / shock events / SEC filings / international
reports / SpaceX briefing 等)各自 schema 不同, 是 follow-up:

- **shock-events.csv** (22 条) → 已分析的事件列表,可写成第 6 个 source
- **MDA / VOYG / CRS investor materials** → 半结构化 md/docx,需要 LLM 抽取
  (用 expert-wiki-ingest skill pattern 入 wiki 知识库, 不进 events)
- **SpaceX orbital AI compute briefing** → 单份长文,人工 review 后挑句进 text source

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

C 路代码已经在, 但**没有挂 cron**。如需启用增量 daily, operator 自己跑:
```bash
satagent job add --name harnessfarm-ingest-daily \
  --action harnessfarm-ingest \
  --params '{"limit":50}' \
  --schedule '0 4 * * *'
# 然后 (用 launchd 或 cron) 在 04:00 调:
#   /usr/bin/python3 -m satellite_agent.cli job run harnessfarm-ingest-daily
```

但 HarnessFarm jsonl 不是高频更新源 (是 fixture 集), 实际**没必要 daily**;
operator 看到 HarnessFarm 那边出新 commit 后手动跑一次即可。所以默认不挂 cron。
