# D6.1 预注册 —— 承诺指标写在改动之前（`lab:D6.1`）

> 🔴 **本文件必须在写任何 v2 代码之前 commit。** 依据 `AGENTS §1.1` 订正段：
> 元指标 = 因子 2（采纳后指标实际改善的命中率），**其可判读的前提是承诺指标写在改动之前** ——
> 否则可事后挑一个必然会动的指标，那才真是一条不能失败的控制。
> ⛔ 取数后本文件一字不改（要改只能另起一份并留痕）。

- **立项依据**：owner 2026-09-02 采纳 [`reports/2026-09-02-detect-frame-theme-D6.md`](../reports/2026-09-02-detect-frame-theme-D6.md) §6.2 的推荐值。
- **它回答哪个目前未知的问题**（⛔ 立项理由不得是「确定有产出」）：
  **把真源伪代码点名却没实现的那条回退路（「或探 Top bar / page bg 图层」）补上之后，
  `light` 侧的准确率上不上得去、以及会不会把现在 99.6% 正确的 `dark` 侧带坏。**
  ⚠️ 两个方向都可能，⛔ 不是构造保证 —— 见 §3 推演①。

---

## 1. 被测对象与不动的东西

| 对象 | 处置 |
|---|---|
| `probes/detect-frame-theme/detect-frame-theme.mjs`（L1 真源落地）| 🔴 **⛔ 一个字节都不改。** 改了 L1 的 26 条钉子与 11 臂注入读数就全部作废 |
| `probes/detect-frame-theme/detect-frame-theme-backdrop.mjs`（**新**）| D6.1 的被测对象。v2 探测器，⛔ 另起一个文件 |
| 量具 `png-luma.mjs` / `render-ground-truth.mjs` | 复用，⛔ 不改判据（改了就不是同一把尺子） |

## 2. 🔴 承诺指标（⛔ 取数后不得增删、不得换）

**判定装置**：`render-ground-truth.mjs` 的口径 P（渲染像素 opaque median + 与被测对象**同一组阈值**
`>0.85`→light / `<0.25`→dark），面 = **同一个 pin 上的 N 面**（严口径，332 个主产品 frame，
其中 331 个量得到）。⛔ 不换面、⛔ 不换阈值、⛔ 不换 Figma version pin。

**基线（口径 B = 现行 `detectFrameTheme`，已取数，见 `runs/detect-frame-theme/ground-truth-N.json`）**：

| # | 指标 | 基线 | 🔴 承诺达标线 | 方向 |
|:-:|---|--:|--:|---|
| **M1** | **反向错判数**（`B=light ∧ P=dark`）+（`B=dark ∧ P=light`）| **31** | **≤ 3** | 越小越好 |
| **M2** | `dark` 侧准确率 `P(P=dark \| v=dark)` | **262/263 = 99.62%** | **≥ 99.00%** | 🔴 **must-not-regress** |
| **M3** | 总体一致率 `P(P = v)` | **273/331 = 82.48%** | **≥ 95.00%** | 越大越好 |
| **M4** | `light` 侧准确率 `P(P=light \| v=light)` | **6/45 = 13.33%** | **≥ 80.00%** | 越大越好 |

**判否规则（🔴 这条能失败，且必须能失败）**：
**M1–M4 四条中任何一条不达标 ⇒ D6.1 判「未达成」**，按实际读数登记，
⛔ 不许事后放宽达标线、⛔ 不许换指标、⛔ 不许换面。

⚠️ **M2 是 must-not-regress，⛔ 不是凑数的**：v2 的整个机制就是「让子图层压过 frame 自身 fill」，
它**最可能的副作用**正是把一个深底 frame 上盖着的大块浅色卡片误当成底 ⇒ 把现在对的 `dark` 判坏。
只报 M1/M4 会让「改好了」这个结论逃过检验。

### 2.1 ⛔ 不算达标的几种形态（提前堵掉）

- ⛔ 把判不准的一律退成 `unknown` 来抬 M1/M2/M3 —— **因此加一条硬约束**：
  **M5（分辨力地板）：v2 在 N 面上判出的 `unknown` 数 ⛔ 不得超过基线的 2 倍（23 → ≤ 46）。**
  否则「反向错判归零」可以靠「什么都不判」达成，那是一条不能失败的控制。
- ⛔ 拿 W 面（宽口径）的数替 N 面报 —— 面在本文件里已写死。
- ⛔ 只报改善不报 M2。

## 3. 动手前的推演三问（⛔ 不许跳过）

**① 这个器的输出有没有可能不是我预期的那个值？—— ✅ 可能，两个方向都可能。**
- 「哪个子图层算底」本身有歧义：一个 frame 可能有多个铺满的子节点（卡片、遮罩、overlay），
  取错一层就判反。
- **可能把现在对的 `dark` 带坏**（见 M2）。
- 22 个 `fills` 为空数组的 frame 会从 `unknown` 变成有判定 —— 变对变错都可能。
⇒ 有信息量，⛔ 不是构造保证。

**② 交接引的原文有没有说过相反的话？—— ⚠️ 有，而且这一条改变了做法。**
真源逐字是 `const bg = productFrame.fills?.[0]?.color; // 顶层 frame 自身 fill，**或**探 Top bar / page bg 图层`。
那个「**或**」把子图层写成了**回退**（`fills[0]` 取不到时才探）。
🔴 **但按字面做回退，本轮 31 条反向错判一条都修不掉** —— 它们的 `fills[0]` 全都取得到（是纯白）。
⇒ **D6.1 必须显式偏离真源字面**：子图层铺底若存在，**压过** frame 自身 fill（画序语义），
⛔ 不是只在 `fills[0]` 缺失时才用。
**代价（登记，⛔ 不掩盖）**：这是 lab 在替真源改语义。若 owner 判「必须守字面」，
则 D6.1 的正确结论是「按字面无法修复」，⛔ 那也是一个合法读数。

**③ 交接判为「病」的东西，会不会正是唯一信号源？—— ⚠️ 查过两处。**
- 被判为「病」的白色 `fills[0]`：在那 31 个节点上**完全被子图层盖住、视觉上不可见**
  （已目视 `1834:300` 截图坐实）⇒ 它 ⛔ 不携带主题信号。
- 被判为「无害」的 `unknown` 兜底：它**是**一个真信号源（「不知道就用 navy」）。
  ⇒ 因此 M5 明确禁止靠扩大 `unknown` 来刷分。

## 4. 🔴 判据形态在取数前写死（⛔ 取数后调参 = 另起一次预注册）

**v2 语义**：一个 frame 的有效底色 = **画序上最后一个「铺满且完全不透明」的 SOLID 涂层**，
候选来自两处，按画序合并：

1. frame 自身的 `fills`（画序：`fills[0]` 最底 → `fills[n-1]` 最顶，已由 D6 §1 实测确认）；
2. frame 的**直接子节点**（画序：`children[0]` 最底 → `children[n-1]` 最顶）。
   子节点在两处都在 frame 之上 ⇒ 任一合格子节点都压过 frame 自身的全部 fills。

**「合格」逐条写死（⛔ 这些值取数后不得调）**：

- `visible !== false` 且 `(opacity ?? 1) === 1`；
- 有一个**最顶层不透明 SOLID** paint（`type==='SOLID'`、`visible!==false`、`opacity===1`、`color.a===1`）；
- **铺满 = 几何包含**：子节点的 `absoluteBoundingBox` **包含** frame 的 `absoluteBoundingBox`，
  容差 **0.5px**（浮点取整）。⛔ 不用「面积占比 ≥ N%」那种可调阈值 —— 那会变成对着读数调参。
- **只看直接子节点（深度 1）**。⛔ 不递归。这是**边界**：若 `page_bg` 藏在 group 里就探不到，
  那会直接体现在 M4 上，⛔ 那是读数不是借口。
- ⛔ 不看 `clipsContent`、⛔ 不做真正的合成、⛔ 不处理 `blendMode !== 'NORMAL'`（登记为边界）。

**阈值与三态判定沿用真源**（`0.299/0.587/0.114`、`>0.85`/`<0.25`）—— ⛔ 一个数都不动。

## 5. 数据面

- 子节点数据走 **`GET /v1/files/:key/nodes?ids=…&depth=2`**（⛔ 不用 D6 那份 `depth=4` 缓存：
  「深度边界上返回空 children」与「真的没有子节点」在那份数据里长得一样 —— 这正是
  `scan-real-surface.mjs` 边界①自己写过的坑）。
- **交叉控制**：对缓存里也带 `children` 的节点，两处的子节点 id 序列必须逐个相同；不同 ⇒ 抛。
- 每个文件的 Figma `version` 必须仍等于 D6 的 pin；漂移 ⇒ 抛（⛔ 不静默换样本）。

## 6. 允许与不允许的返工（⛔ 提前定，防止对着读数迭代）

| 情形 | 允许 |
|---|---|
| **装置故障**（fetch 挂了、解码抛了、控制自己写错）| ✅ 修，需在报告里贴出「这是装置故障」的证据（§3.9）|
| 读数不达标 ⇒ 想调 0.5px 容差 / 改成递归 / 改成面积占比 | ⛔ **不许**。那是对着测试集调参 ⇒ 只能**另起一份预注册**并明写「这是第 2 次」|
| 读数不达标 ⇒ 想换面 / 换阈值 / 换指标 | ⛔ 不许 |
