# 需求:报表把采集失败渲染成事实-源非ok时核心指标印采不到而不是0
状态: done(2026-10-10 已落地:report.py 三处(d1 驱动 8 格+趋势逐日+归因节)改判据,7 单测+真实通路端到端实证(临时目录跑 main,驾驶舱 parse 读到⏸采不到);口径已落 ops/README.md + 当日 metrics note)
归属: 公司运营(运营日报/驾驶舱)
来源: 登记缺口(2026-10-10 12:4x 日报假零修复收口时自查发现:采集通道 d1/r2 已修,报表的排法没修)
日期: 2026-10-10
建议: 做,10-10 日报假零靠人肉对比打标口径才发现;report.py 对失败源一律 .get(...,0),「不知道」被印成「0 个用户」,同类误判必然复发

## 现状

今天 08:00 那份日报读出来是「注册用户总数 **0** / 有效订阅 **0** / 近30天新增 0」。真实值是 10-08 打标后的正式口径**用户 3 / 订阅 0**——总数不可能自 0 掉回 0,所以那是假零。根因在采集(d1 的 wrangler token 断供 + r2 的 CF GraphQL 出口次序),12:4x 已修并回正 10-06~10-08 历史行。

**但报表本身的排法仍在,是另一条独立的缺陷**:`ops/report.py` 里 d1 驱动的指标格全部写成 `users.get("new_today", 0)` 这种「取不到就当 0」的形态(第一节 8 格、第三节趋势逐日 4 格、第十节归因表)。采集失败时 `sources.d1` 长这样(10-06~10-08 的 error 行实证):

```
{"status": "error", "detail": "wrangler 失败: … Authentication error [code: 10000] …"}
```

里面根本没有 `users`/`subs` 键 → `.get(..., 0)` 全部落到默认值 → 报表把「我不知道」排成「0 个用户」。第五节「数据源健康」确实标了 ⚠️ error,但它和第一节隔了三节,**boss 看的是第一节的数字**——今天的假零是我人肉拿打标口径对比才发现的,机制上没有任何东西会报。

同一个失败还制造了第二条错误解释:第十节 `users_by_client` 为空时固定印「当日快照无归因数据(2026-09-12 前的旧快照)」,把采集失败说成历史快照,方向完全错。

结构性判据:**「0」与「采不到」在报表上必须是两个可分辨的读数**。现在它们长得一模一样,所以任何一次采集断链都会被读成「业务掉到 0」——这类误判的代价是拿假数据做决策(今天差点得出「用户清零」)。

## 方案

只动 company 仓 `ops/report.py` 的**渲染判据**(第四节口径变更先落 `ops/README.md`),不碰采集通道(collect.py 已修)、不碰数据、不碰产品仓:

1. 模块级单一判据函数 + 占位词:`MISSING = "⏸ 采不到"`,`cell(ok, text)` 在 `ok=False` 时返回 MISSING,否则返回原值。第一节与第三节共用同一个函数,不散写 if。
2. 可读性判定 `d1_ok = not src_bad(d1) and bool(d1)`,复用既有 `src_bad`(状态非 ok/None 即坏),并把「快照里根本没有 d1 块」也算不可读(不能因键缺失当绿灯)。
3. 第一节 8 格(users 5 格 + subs 3 格 + 转化率)套 `cell(d1_ok, …)`;**转化率在分子分母都不可读时印 ⏸ 采不到**,不印 0.00%。
4. 第三节趋势逐日 4 格按**该日自己的** d1 状态判定(历史 error 日不得排成 0 把趋势线拉弯;已 `--backfill` 回正的行不受影响)。
5. 第十节归因:`d1_ok=False` 时明确印「⏸ 采不到:d1 源非 ok」,与「旧快照无归因字段」两种情形分开解释。
6. 试用行(`if trials:`)不动——d1 失败时该键不存在、整行不出现,不产生假数;不为此加新分支(避免把范围铺大)。
7. 口径先落正本:`ops/README.md`「数据口径」新增「采不到 vs 真 0」判据一句话,并在 `ops/data/metrics.jsonl` 当日行的 `notes` 里登记这次口径变更(纪律:改口径=先改本节+当天加 note,历史数据不回改)。

## 验收标准

- [x] 源 error(d1 只有 status/detail)→ 第一节注册用户总数/新增/活跃/订阅/转化率各格印 `⏸ 采不到`,不再出现 0(**真实通路端到端实证**:临时目录喂一条 error 快照真跑 `report.main()` 出 md,八格全为 `⏸ 采不到`,断言 `**0**` 不在该节)
- [x] 变异对照:源 ok 且值真为 0 → 仍印 `0`(单测 `test_real_zero_prints_zero` 锁字面 `| 注册用户总数 | **0** |`、`| 今日新增注册 | 0 |`、转化率 `0.00%`;今日真实数据里「今日新增注册 0/有效订阅 0」照旧印 0,未被糊成采不到)
- [x] 第三节趋势:历史 error 日该日 4 格印 `⏸ 采不到`,同日 ok 行不受影响(单测逐日自判 + 端到端 md 里 `| 2026-10-10 | ⏸ 采不到 ×4 |`、10-09 ok 行仍出数字)
- [x] 第十节归因在 d1 不可读时报「采不到」,不再误称「2026-09-12 前的旧快照」(单测断言节内不含「旧快照」)
- [x] 新增单测覆盖上述四态全绿,ops 全量 pytest 不回归(`ops/test_report_fake_zero.py` 7 用例;**ops 全量 431 passed / 1 skipped**)
- [x] 驾驶舱对 `⏸ 采不到` 正常渲染成文字(不崩、不变 0):用 `dashboard.parse_report()`(读数原函数)端到端跑 error md,`注册用户总数`/`注册→付费转化率` 读回字符串 `⏸ 采不到`;KPI 卡 emit 是 `esc(val)` 纯文本(代码实证),健康数据下 `numbers.html` 已部署并线上回读
- [x] `ops/README.md` 数据口径(表首新增「采不到与真 0」行)+ `ops/config.json` notes 第 4 条 + 当日 `metrics.jsonl` 行 notes 落注(逐字节核对:仅 10-10 那一行变动,其余 28 行原样,CRLF 保形)
