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

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

- **立项依据**：owner 2026-09-03 在三条候选（D6.2 / DS-R1a / DS-R1b）中选定 D6.2。
- **⛔ 不是**在第 1 份预注册上调参重跑 —— 那是对着测试集迭代，`open-items §6.4` 已逐字禁止。
  本轮**换被测对象**（v3 是另一个器）、**换面**（H 面，见 §5）、**换 must-not-regress 口径**（逐节点）。
- **它回答哪个目前未知的问题**（⛔ 立项理由不得是「确定有产出」）：
  **把「什么时候不该信 frame 自己的 fill」做成分级（T1 直接信 / T2 不信、递归探子图层 / T3 兜底）
  之后，在一个从未被看过的面上，① light 侧的错判修不修得掉 ② T1 那半会不会被带坏。**
  ⚠️ 两个方向都可能，⛔ 不是构造保证 —— 见 §3 推演①。

---

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

| 对象 | 处置 |
|---|---|
| `probes/detect-frame-theme/detect-frame-theme.mjs`（v1，L1 真源落地）| 🔴 **⛔ 一个字节都不改。** 它同时是**基线**与 T1 档的**委托对象** |
| `probes/detect-frame-theme/detect-frame-theme-backdrop.mjs`（v2，D6.1 被测对象）| 🔴 **⛔ 一个字节都不改。** v2 的 11 臂注入读数与 D6.1 判定要保持可复核 |
| `probes/detect-frame-theme/detect-frame-theme-tiered.mjs`（**新**，v3）| D6.2 的被测对象。⛔ 另起文件，⛔ 不改 v1/v2 |
| 量具 `render-ground-truth.mjs` / `png-luma.mjs` | 🔴 **复用且 ⛔ 不改**（改了就不是同一把尺子，且其 `PIXEL_ANCHOR` 与故障注入读数会作废）|
| 量具 `scan-real-surface.mjs`（D6 用）| ⛔ **不改、也不复用**。它的 `SURFACE_PIN` 钉的是 6 个 target 文件；H 面另起量具，见 §6 |

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

**判定装置**：`render-ground-truth.mjs` 的口径 P（渲染像素 opaque median + 与被测对象**同一组阈值**
`>0.85`→light / `<0.25`→dark）。⛔ 不换阈值、⛔ 不换面、⛔ 不换 Figma version pin（§5）。

🔴 **本份的全部达标线都写成【相对基线】或【绝对零】，⛔ 没有一条依赖「先看一眼 H 面的分布」** ——
这是本份相对第 1 份的**方法学改动**：第 1 份的阈值是对着已取的基线定的（M1 基线 31 ⇒ 承诺 ≤3），
放在一个盲面上等于「先看牌再下注」。⇒ 本份取数前 **⛔ 不知道任何一条基线值**。

设 `v1_err` = v1 在 H 面上「判定 ≠ 像素真值」的**节点数**；`v1_rev` = v1 的**反向错判**数
（`v=light ∧ P=dark` 或 `v=dark ∧ P=light`）；`v1_unk` = v1 判 `unknown` 的节点数。

| # | 指标 | 🔴 承诺达标线 | 类型 |
|:-:|---|---|---|
| **M1** | **逐节点非回退**：`v1 判对 ∧ v3 判错` 的节点数 | **= 0** | 🔴 **must-not-regress（逐节点，⛔ 不是条件比例）** |
| **M2** | 绝对错判数 `v3_err` | **≤ 0.50 × `v1_err`** | 越小越好 |
| **M3** | 反向错判数 `v3_rev` | **≤ 0.10 × `v1_rev`** | 越小越好 |
| **M4** | `unknown` 数 `v3_unk` | **≤ 2 × `v1_unk`** | 分辨力地板 |

**达标线的先验出处（⛔ 不出自 H 面）**：M2 的 0.50 来自 D6.1 在 N 面实测的 58→27（降 53.4%）——
即「v3 至少要做到 v2 已经做到的幅度，且是在一个干净面上」；M3 的 0.10 来自 D6.1 实测 31→0；
M4 的 2× 逐字沿用第 1 份的 M5。⇒ 三条都是**已发表读数**，⛔ 不是看着 H 面定的。

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

### 2.1 🔴 功效地板（⛔ 取数前写死，⛔ 事后不得豁免）

`AGENTS §2.8`：MDE 大于效应量时，该指标在该 N 上**结构性地判不出改善**，这要显式写进报告。
H 面的 N 本份取数前**未知** ⇒ 用两条预先写死的地板接住：

- **F1**：若 `v1_err < 20` ⇒ **M2 / M3 判「N 不足、⛔ 不可判读」**，⛔ 不得改判达标。
  （20 个错判以下，「降 50%」与「降 0%」在配对精确检验下分不开。）
- **F2**：若 H 面内落进 **T2 档**的节点数 `< 20` ⇒ **整轮判「该面不含被测形态、⛔ 不可判读」**，
  ⛔ 不得据此宣称 v3 有效或无效。
- ⚠️ **M1 ⛔ 不受地板豁免** —— 它是「= 0」的绝对断言，任何 N 上都可判（N 小只是让它更容易过，
  这一点必须在报告里连同 N 一起报，⛔ 不得单独引用 M1 过关）。

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

- ⛔ 靠把判不准的退成 `unknown` 来抬 M1/M2/M3 —— 由 M4 堵。
- ⛔ 拿副臂（`v3-strict`，见 §4.4）的数替主臂报。承诺指标**只判主臂**。
- ⛔ 拿 W 面（6 个 target 文件）的任何数替 H 面报。W 面已按诊断报告 §6 登记为**已污染、降级为推导面**。
- ⛔ 只报 M2/M3 不报 M1。
- ⚠️ **T1 档「v3 与 v1 逐节点相同」⛔ 不列为承诺指标** —— 若 v3 的 T1 分支直接委托 v1（§4.2 正是这么写的），
  它就是**构造保证、零信息量**。它的信息量在**故障注入臂**里（把分级拆掉 ⇒ 该断言必须红），
  ⇒ 它作为 **MH 控制**登记在 §6，⛔ 不作为承诺指标。这条是本份自己给自己堵的
  `M-GATE-FALSIFIABILITY` 同族病。

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

**① 这个器的输出有没有可能不是我预期的那个值？—— ✅ 可能，且有一个方向是我预期的反面。**
- **T2 档探不到铺底时的处置会直接决定 M1 能不能过**：一个**真的是浅色页**、设计师就是留了默认白、
  且没有任何子图层铺底的 frame —— v1 判 `light`（对），v3 若按诊断表字面退成 `unknown` 就是**判错**
  ⇒ **M1 当场破零**。⇒ 这不是假想，诊断实测「纯白未绑」桶里 **28.7%** 是真浅色。
  ⇒ 因此 §4.4 把 T3 的处置**拆成两臂并排测**，⛔ 不预设哪个对。
- 递归深了会**探到不该探的层**（卡片、弹窗、遮罩铺满整屏），把浅色页判成深色 ⇒ 反向错判可能不降反升。
- H 面有 3 个文件是 2023–2026-05 的旧稿（§5），**可能大面积没有变量绑定** ⇒ T2 桶异常大，
  或反过来结构简单到 T2 桶为空 ⇒ F2 触发、整轮不可判读。
⇒ 有信息量，⛔ 不是构造保证。

**② 交接引的原文有没有说过相反的话？—— ⚠️ 有两处，且都改变了做法。**
- **诊断表 §5 的 T3 逐字是 `unknown → navy 兜底`**。那句话的语境是**给 AI 生成 mockup 用的规则**
  （不知道就用 navy，是安全侧），⛔ **不是**「探测器准确率」这个口径下的最优处置。
  ⇒ 照搬会让 M1 结构性地过不了（见推演①）。⇒ **§4.4 两臂并排**，主臂取回落、副臂取字面，
  **让「哪个对」变成一个读数，⛔ 不是一个假设**。代价登记：主臂偏离了诊断表字面。
- **v2 头注释的边界①逐字**：「⛔ 只看直接子节点，⛔ 不递归。`page_bg` 若藏在 group 里就探不到 ——
  **那会直接体现在 M4 上，⛔ 那是读数，不是借口**」。D6.1 实测兑现：剩余 27 条错里 **18 条**
  `backdropFrom = none`。⇒ 递归正是本份要补的那一格，**它的收益上界已被 D6.1 量出来了**。

**③ 交接判为「病」的东西，会不会正是唯一信号源？—— ⚠️ 查过，且这次答案是「有条件地是」。**
被判为「病」的是**纯白且未绑变量的 `fills[0]`**。诊断实测它的 precision = **28.7%**
⇒ 它 **28.7% 的情况下是对的**，⛔ 不是纯噪声。⇒ 这正是 §4.4 主臂**保留 v1 回落**的实质理由：
把一个「三成对」的信号直接扔掉换成 `unknown`，是拿 M1 去换 M2。
⇒ ⚠️ 与第 1 份的③不同：那次白 fill 在 31 个节点上**被完全盖住、视觉不可见**（已目视坐实）⇒ 真的无信号；
这次的面更宽，⛔ 不得把那个结论搬过来。

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

### 4.1 分档谓词（🔴 逐字沿用既有量具，⛔ 不重新定义）

- **精确纯白** `isExactWhite(c)` = `c.r === 1 && c.g === 1 && c.b === 1`
  —— 逐字取自 `diagnose-light-side.mjs:33`，⛔ 不做近似（`#fffffe` 不算纯白）。
- **绑了变量** `bound(node, p0)` = `Boolean(p0.boundVariables?.color) || Boolean(node.boundVariables?.fills?.length)`
  —— 逐字取自 `diagnose-light-side.mjs:116`。
- `p0` = `node.fills?.[0]`，**⛔ 不筛 `visible`**（v1 就是这么读的，分档必须与基线同源）。

### 4.2 三档（⛔ 分档只看 frame 自身，⛔ 不看子树）

| 档 | 条件 | 动作 |
|:-:|---|---|
| **T1** | `p0` 存在 ∧ `p0.type === 'SOLID'` ∧ 有 `color` ∧（**非**精确纯白 **∨** 绑了变量）| 🔴 **直接委托 `detectFrameTheme(node)`（v1）**，⛔ 不探子图层 |
| **T2** | （`p0` 存在 ∧ SOLID ∧ 有 color ∧ 精确纯白 ∧ **未**绑变量）**∨**（`p0` 缺失 / 非 SOLID / 无 color）| ⛔ 不信 `fills[0]`，走 §4.3 递归探铺底 |
| **T3** | T2 探完仍无合格铺底 | 见 §4.4（两臂）|

⚠️ **T2 的第二支（`fills[0]` 取不到）是本份补的格，诊断表 §5 ⛔ 未覆盖它**（那张表只谈「有 fills[0].color」的情形）。
补它的理由：v1 对这类判 `unknown`，探子图层**只可能改善或持平**；且 D6.1 剩余错判里 18 条正是这一类。
**登记为本份的显式扩张，⛔ 不掩盖。**

### 4.3 递归探铺底（🔴 这是 v3 相对 v2 的唯一机制改动）

自 frame 起，按**画序从最顶向下**扫其 `children`（`children[n-1]` 最顶 → `children[0]` 最底）：

- **「合格铺底」** = `visible !== false` ∧ `(opacity ?? 1) === 1` ∧
  `absoluteBoundingBox` **几何包含** frame 的（容差 **0.5px**）—— 逐字沿用 v2 的定义。
- 合格 ∧ **自身有最顶层不透明 SOLID paint** ⇒ 取该 paint 的 `color` 作有效底色，**停**。
- 合格 ∧ **自身无不透明 SOLID paint** ⇒ 🔴 **递归进它的 `children`**（v2 边界①缺的正是这一格）。
- **递归深度上限 = 3 层**（frame 的 children 记第 1 层）。超出 ⇒ 停，记 T3。
- 扫完全部候选无所得 ⇒ T3。

得到有效底色后，**阈值与三态判定沿用真源**（`0.299/0.587/0.114`、`>0.85`/`<0.25`）——
🔴 **直接 import v1 的公开常量，⛔ 不复制字面量**（复制了会在真源改动时静默分岔）。

### 4.4 🔴 T3 的处置拆成两臂并排测（⛔ 不预设哪个对）

| 臂 | T3 处置 | 地位 |
|---|---|---|
| **`v3-fallback`（🔴 主臂）** | **回落 v1 的判定**（即 T2 只在**探到**铺底时才覆盖 `fills[0]`）| 🔴 **承诺指标只判这一臂** |
| **`v3-strict`（副臂）** | 判 `unknown`（诊断表 §5 字面）| 并排报，⛔ 不用于改判、⛔ 不得替主臂报数 |

⚠️ 两臂**在同一次取数、同一个面、同一份像素真值上**算出，⛔ 不分两次跑（分两次跑面会漂）。

### 4.5 覆盖边界（`§3.4`：这是边界，⛔ 不是 TODO）

① **递归上限 3 层** —— 更深处的铺底探不到，这会直接体现在 M2/M3 上，⛔ 那是读数不是借口。
② ⛔ 不看 `clipsContent`；⛔ 不做真正的合成（半透明叠加）；⛔ 不做颜色空间转换。
③ ⛔ 不判 §M23.18「一个 Section 内混有深浅 ⇒ unknown」那条合并规则（属调用方逻辑）。
④ 「产品 frame」是自然语言定义，本份用**单一可执行近似**（W 口径，见 §5），⛔ 它不是那句话本身。
⑤ 🔴 **树深度必须逐 frame 补齐**（§6.1）—— 否则递归预算会随 frame 在文件里的位置而不同
（page 直属 frame 比 SECTION 内 frame 多一层），那会把「递归有没有用」和「我们手上有多少树」搅在一起。

## 5. 🔴 面（H 面）与 pin —— ⛔ 取数后不得更换

**H 面 · W 口径** = 下列 **8 个 hold-out 产品文件**中，**page 或 SECTION 的直接子 `FRAME`**。

- ⚠️ **口径名必须带**（`AGENTS §2.4`）：本份全部读数都是【**H 面 · W 口径**】。
  ⛔ 不得与【lab W 面】（6 个 target 文件，2253/2275 节点，**已污染**）或
  【DS 闸口径】（全深度，2435 扫描 / 446 违反）并排比 —— 三者是三个不同的数。
- **为什么是这 8 个**：`ds-health-mockup-targets.json` 的 `sourceLabels` 登记它们为 `product-file`，
  但 `targets` 只含 6 个。实测 `holdout ∩ targets = []`、`holdout ∩ default-white-bg-baseline.byFile = []`，
  `figma-data/mockup/` 无其 payload，lab `runs/` 内唯一命中是 `r009/transcript.jsonl`（2026-08-13 取名那次，
  ⛔ 不是读它的内容）⇒ **「从未进过任何读数」被结构性坐实，⛔ 不只是交接自陈。**

| fileKey | 名称 | `version`（= subjectSha，`§2.1`）| lastModified | pages |
|---|---|---|---|--:|
| `5VVlCTtQ0WRcJdvwUA6bJ5` | Config T ( Local UI ) V7.7&v8.0 20210909 | `2373009894472478653` | 2026-07-06T07:36:41Z | 76 |
| `ZXm4ClRDgGm2M3pU64NC4A` | MH-Lora | `2394437567926738907` | 2026-09-02T02:00:57Z | 13 |
| `lqo3mopIoEs27YcJ6R4a0I` | PP-Lora | `2391118721625433918` | 2026-08-24T03:25:27Z | 34 |
| `nhZkYb9qQdEngal7iOa5CL` | Sandra-TVU One 20210526 | `2384516030278151267` | 2023-03-23T06:00:18Z | 10 |
| `qkxgyVfbPDB9fc2rdbeXOh` | PL_3 | `2351383927235829082` | 2026-05-06T09:14:49Z | 44 |
| `daLUTwSPemAbE0bMpn9s8n` | Producer Admin | `2340389986709405741` | 2026-04-09T09:30:54Z | 9 |
| `HAL8gzATJx58kynXesipEb` | Producer 2.0 | `2352533676537086777` | 2026-05-12T03:43:40Z | 47 |
| `G9JxiOc3V4fVq0DH3GjfTw` | TVU Drive | `2373038443713949669` | 2026-07-06T09:13:25Z | 7 |

原始读数：[`runs/detect-frame-theme/holdout-pins.json`](../runs/detect-frame-theme/holdout-pins.json)
（本份 commit 前取，只拉 `depth=1` 元数据 ⇒ **⛔ 未看到任何 frame、⛔ 零污染**）。

🔴 **pin fail-closed**：取数时 `version` 对不上就**抛**，⛔ 不许静默换样本。
⚠️ `MH-Lora` 的 lastModified 是 2026-09-02（最近）⇒ 它最可能在取数前漂移；漂了就**显式重新 pin 并登记**，
⛔ 不许假装没看见。

### 5.1 🔴 渲染面抽样规则（⛔ 取数前写死）

像素真值要逐节点调 Figma 渲染，H 面可能有数千个节点。⇒ 预先写死：

- 若 H 面 · W 口径节点数 **≤ 1500** ⇒ **全量渲染**。
- 若 **> 1500** ⇒ 以**固定种子 `20260903`** 做**无放回均匀随机抽样**取 1500 个作渲染面。
  🔴 抽样在**任何判定读数产生之前**完成，抽样清单入 `runs/`，
  ⛔ **不得按 v1/v3 的判定结果分层、⛔ 不得按文件配额、⛔ 不得剔除任何节点。**
- 渲染失败 / 无不透明像素的节点**单独记一层**，⛔ 不并进三态、⛔ 不并进分母
  （沿用 `render-ground-truth.mjs` 边界③）。

## 6. 量具与控制（`§3.2`：每条判据配 must-hit + must-not-hit，内建进脚本）

### 6.1 新量具 `probes/detect-frame-theme/scan-holdout-face.mjs`

⛔ 不改 `scan-real-surface.mjs`（它的 `SURFACE_PIN` 钉 6 个 target 文件，且带自己的注入读数）。
新量具做四件事，**⛔ 自身不含任何主题判定逻辑**（唯一例外是 MN 的独立重算）：

1. **枚举面**：`/v1/files/{key}?depth=4` → W 口径（page 或 SECTION 的直接子 FRAME）。
2. 🔴 **补齐树深度**：对面内每个 frame 批量 `/v1/files/{key}/nodes?ids=…&depth=4`
   ⇒ **每个 frame 下都拿到 4 层**，而递归上限是 3 ⇒ **永远在截断边界内侧 1 层**。
   ⚠️ 被截断的节点没有 `children` 键 —— ⛔ 不得把它读作「没有子节点」（D6 边界①的原话）。
3. **喂三个器**：v1、v3-fallback、v3-strict，逐节点记判定 + `tier` + `backdropFrom`（探到的层深）。
4. **发 `faceNodeIds`**，形状与 `real-surface-d6.json` 逐字相同
   （`{fileKey, nodeId, name, bottomReading, topReading}`）⇒ `render-ground-truth.mjs` **⛔ 不改**即可复用。

**内建控制**（签名一变当场抛）：

| 控制 | 断言 | 抓什么 |
|---|---|---|
| **MH1** | 三档计数之和 `= 面内节点总数`，且三档**互斥** | 分档漏格 / 重复计数 |
| **MH2** | 🔴 **T1 档上 `v3-fallback` 与 v1 的判定逐节点全等（差异 = 0）** | 分级塌掉（退化成 v2 那种一刀切）。⚠️ 正实现下是构造保证 ⇒ 它的信息量在注入臂，见 §2.2 |
| **MH3** | `v3-strict` 与 `v3-fallback` **仅在 T3 档**可能不同；T1/T2-探到 档上必须全等 | 两臂实现串味 |
| **MH4** | 每个文件的 `version` 与 §5 表中 pin 逐字相等 | 面漂移（fail closed）|
| **MN1** | ⛔ **不得**出现 `tier=T1 ∧ backdropFrom ≠ null` | T1 偷偷探了子图层 |
| **MN2** | 用**独立第二实现**重算 luma，与被测对象的判定不一致时抛 | 分类器与子分类器分岔（沿用 `scan-real-surface` MN2 的形态）|
| **MN3** | ⛔ **不得**出现 `backdropFrom > 3`（递归越界）| 深度上限失效 |

### 6.2 故障注入（`§3.8`：跑一遍绿不算验证，必须造故障验证它会拦）

取数后对 v3 造故障，**⛔ 少于 4 臂不算做过**，读数入 `runs/detect-frame-theme/tiered-fault-injection.json`：

| 臂 | 改坏什么 | 🔴 预期 |
|---|---|---|
| `p1-no-tiering` | 拆掉分档，所有节点都走递归（= v2 的一刀切）| **MH2 必红**（T1 被带坏）|
| `p2-no-recursion` | 递归上限 3 → 1（= v2 的边界①）| M2/M3 应劣化，且 `backdropFrom` 分布上移 |
| `p3-white-approx` | `isExactWhite` 改成近似（`> 0.99`）| 分档边界漂移，T1/T2 计数变 |
| `p4-literal-fallback` | T2 探到铺底也只作**回退**（真源那个「或」的字面）| **应逐字回到 v1 基线**（D6.1 `p2-own-fill-wins` 已证同形）|

**操作纪律（`§3.8`）**：① **先证故障态成立**（贴被改对象的实际输出，⛔ 不改完直接跑）；
② 用完 `git checkout` 还原并核 `git status` 为空；③ **注入本身也要验** —— marker 计数没涨就不算注入成功。

## 7. ⛔ 本份不承诺什么

- ⛔ 不承诺 D7（接进 `variant-axis` 闸）—— 那是 D6.2 出读数之后的独立决定。
- ⛔ 不承诺动 DS 真源。本轮 **lab 侧改动为主**；若要落 DS，另起处方 + 跑 DS 元闸。
- ⛔ 不承诺存量 446 条整改（DS-R1b，要设计师排期，lab 不代拍）。
- ⛔ 不承诺 §3.1 二级判据转 fail（DS-R1a）—— 本份产出的 precision 是它的**输入**，⛔ 不是它的执行。
