# 触发器 T 判读器 · 去案例化阶梯 —— 实施计划

> **For agentic workers:** 本计划按 `superpowers:writing-plans` 写就。步骤用 `- [ ]` 勾选。
> ⛔ **本计划含预注册常量，必须「先 commit 再跑」**（§30.5 N80.1 纪律）。
> ⛔ **扰动算子自己的 must-hit 不全绿，不许发任何真实调用**（fail closed，见 Task 2）。

**Goal:** 检验「判据文本里带着它自己的实证案例」是不是 v2 已发表读数的混淆源 —— 用**三档单调剥离**
读趋势，判定 `reports/2026-08-31-trigger-t-llm-judge.md` §2「判读横切是 LLM 能稳定做对的事」这条结论
是否成立；并据结果决定 v3 该不该建在这批读数上。

**Architecture:** 不新建量具。在 [`adapters/llm-judge-container.mjs`](../adapters/llm-judge-container.mjs)
上加一个**纯函数扰动算子** `makeRuleVariant(text, rung)`，把喂给判读器的规则原文按三档剥离，
每档配 fail-closed 断言。三档在**同一 session、同一模型、同一 N** 下各跑一遍，读**档间趋势**。

**Tech Stack:** 纯 Node ESM、零依赖（D4）。只读 DS pin。判读通道复用既有 `judgeOnce()`。

**Spec:**
- 上游读数：[`reports/2026-08-31-trigger-t-llm-judge.md`](../reports/2026-08-31-trigger-t-llm-judge.md) §2 / §3
- 缺陷发现：本 session（2026-08-31）—— `extractRuleT()` 抽取范围含 DS `docs/meta-rules.md:975` 实证行
- owner 拍板：(a) lab 侧剥离 + (b) 递 DS 侧处方，两件都做

---

## 0. 这个计划要回答的那个问题

`extractRuleT()`（[llm-judge-container.mjs:65-84](../adapters/llm-judge-container.mjs#L65-L84)）
从 DS pin 抽 T 原文喂给模型。抽取范围**包含 `meta-rules.md:975` 那一整行实证**，它逐字写着：

> 按**单个 feature 交付卡**的范式建——**文案一张卡、规则一张卡、出处一张卡**。加第二类场景就要同时改三处

而 `must-hit-0` 样本的段落名正是 **文案 / 通用规则 / 出处**，`must-hit-1`（合成样本）
[llm-judge-container.mjs:200-224](../adapters/llm-judge-container.mjs#L200-L224) 的三段名**也是**这三个。
两条 must-hit 都拿了 100%，而段落名对不上的 `must-not-hit-0` 拿了 0%。

⇒ **两个解释同时拟合这组读数**：
- **H1（已发表）**：`appendCost = A + B`，B 恒为正稀释信号（报告 §3）
- **H2（未排除）**：模型在拿样本去匹配 prompt 里给它的那个已解答案例，不是在按规则判

⛔ lab 只检验了 H1。本计划检验 H2。

### 0.1 ⛔ 这个检验不吃训练集污染 —— 为什么

N82.2 定了那四份样本只能当训练集。**但本计划问的不是「v3 对不对」**，问的是
「**这个读数依不依赖案例被写进 prompt**」—— 同一份样本、三版判据文本，读的是**档间差**。
样本读没读过不影响这个差值。⇒ 本计划的产出**可以**进结论栏，⛔ 但只限于「H2 成不成立」这一条，
⛔ 不得外推成「v3 成立」。

---

## Global Constraints

- **lab 只审查、不裁定**（`AGENTS.md` §8）⇒ 出读数与处方，⛔ 不代 DS 拍板改不改 `meta-rules.md`。
- **预注册先行**：`PREREG_LADDER` 写死进源码，**先 commit 再跑**；⛔ 不许跑完调（N80.1）。
- **⛔ 不引用已发表的 v2 读数当对照臂。** `templateHash = sha256(TEMPLATE_VERSION + ruleText)`
  ([llm-judge-container.mjs:117-118](../adapters/llm-judge-container.mjs#L117-L118))
  ⇒ 改规则文本就换了主键，按覆盖边界 ④ 那是**另一个量**。三档必须**同 session 全部重跑**，
  R1 档也要重跑，⛔ 不许拿 `runs/judge-cal-6ac56de2/` 顶替。
- **扰动算子自己要配 must-hit**（N83.1）—— 剥离函数必须有 fail-closed 断言证明
  「案例真剥掉了」且「判据真没剥掉」，⛔ 否则「命中变化」和「剥错了」在读数上长得一模一样。
- **⛔ 事先不知道的量不设期望值**：三档各自的绝对命中率 lab 事先不知道 ⇒ 预注册的是**判读规则**
  （档间差怎么读），⛔ 不是期望值。
- **⛔ 二分类判读低基数，强制 N≥5、报逐次完整分布**（覆盖边界 ③）。⛔ `sd≈0` 不得读作无噪声（N82.3）。
- **取退出码 ⛔ 不接管道** ⇒ 一律 `cmd; echo "exit=$?"`，让被测进程是整条命令最后一步（N82.3）。
- **只覆盖 `.md` 面**，⛔ 不得外推到 Figma 面（覆盖边界 ⑥，本轮仍未消除）。
- **DS pin 只读**。跑完验 `git -C <pin> status` 为空。

### ⛔ 本计划不碰的文件

- ⛔ **不改 DS 任何字节** —— (b) 只产出 `proposals/` 下的处方，DS 侧自己执行。
- ⚠️ **TVU Pack 有一个 dirty 文件** `docs/specs/2026-06-09-v4-1864-2326-roaming-access-*.md`，
  **不是 lab 改的**，⛔ 别顺手提交。本计划只 `git show` 读 TVU Pack，不写。
- ⛔ **不改 [`docs/round2-status.md`](./round2-status.md) 以外的既有报告** —— 已发表读数逐字不动，
  更正走**新增登记条目**，⛔ 不改旧报告正文（改了就无法追溯当时发表的是什么）。

---

## File Structure

| 文件 | 责任 | 动作 |
|---|---|---|
| [`adapters/llm-judge-container.mjs`](../adapters/llm-judge-container.mjs) | 判读器。新增扰动算子 + 三档 CLI + 预注册常量 | 修改 |
| `runs/judge-ladder-<sha>/{R1,R2,R3}/` | 三档逐次原始记录 | 新建（产物） |
| `reports/2026-08-31-trigger-t-decase-ladder.md` | 三档读数 + H2 判定 | 新建 |
| `docs/criterion-design-conventions.md` | **产品无关**的判据设计约定页（Q2 固化件） | 新建 |
| `proposals/2026-08-31-rule-text-decasing.md` | 递给 DS 的处方（(b)） | 新建 |
| [`docs/round2-status.md`](./round2-status.md) | 登记 N84 系列 | 修改（仅追加行） |

---

## Task 1: 扰动算子 `makeRuleVariant` + 三档定义

**Files:**
- Modify: [`adapters/llm-judge-container.mjs`](../adapters/llm-judge-container.mjs)（在 `extractRuleT` 之后，`buildPrompt` 之前插入）

**Interfaces:**
- Consumes: `extractRuleT(pin) -> string`（已存在，不改）
- Produces: `makeRuleVariant(text: string, rung: 'R1'|'R2'|'R3') -> string`
  · `RUNGS: readonly ['R1','R2','R3']`
  · `RUNG_WHY: Record<rung, string>`

### 三档是什么（⛔ 剥的边界要对，剥错了就把判据剥没了）

实测确认过的两处（DS `docs/meta-rules.md`，pin 内行号可能漂，按内容匹配、⛔ 不按行号）：

| 位置 | 原文 | 性质 |
|---|---|---|
| **968 行** 硬要求单元格 | `⛔ 禁按层横切（内容 / 规则 / 出处各一段）` | 🟢 **判据本身**（抽象反例） |
| **975 行** 独占一行 | `**实证 2026-08-31 TVU Pack LCD 提示文案总表**：…文案一张卡、规则一张卡、出处一张卡…` | 🔴 **实证案例**（含产品名） |

⇒ 三档**单调剥离**，每档只比上一档少一样东西：

| 档 | 含什么 | 剥掉什么 |
|---|---|---|
| **R1 `cased`** | 具体案例 **+** 抽象反例 **+** 硬要求 | —（＝现状，⛔ 不是拿旧读数，是重跑） |
| **R2 `decased`** | 抽象反例 **+** 硬要求 | 实证整行（产品名随之消失） |
| **R3 `minimal`** | 只剩硬要求 | 再剥 `（内容 / 规则 / 出处各一段）` 这个括号 |

⚠️ **为什么必须有 R3**：R2 剥完实证后，`规则` / `出处` 这两个词**仍在 prompt 里**
（在 968 行的抽象反例中）。若只做两档，R2 仍命中时无法区分「判据成立」与「抽象反例仍在起匹配作用」。
R3 把最后一个形态提示也剥掉，只留 `加一个条目 = 追加一个单元、不动任何已有单元`。
**R3 才是「提炼成通用规则」那个形态的可测版本。**

- [ ] **Step 1: 写扰动算子（含 fail-closed 断言）**

在 [llm-judge-container.mjs:84](../adapters/llm-judge-container.mjs#L84)（`extractRuleT` 的 `}` 之后）插入：

```js
// ════════════════════════════════════════════════════════════════════
// 去案例化扰动算子（N83.1：扰动算子自己也要配 must-hit）
//
// 检验的是 H2：「判据文本里带着它自己的实证案例」是不是 v2 读数的混淆源。
// ⛔ 三档必须单调：R1 ⊃ R2 ⊃ R3，每档只比上一档少一样东西。
// ⛔ 剥的是**实证**，不是**判据** —— 968 行「禁按层横切（内容/规则/出处各一段）」
//    是判据的抽象反例、属于判据；975 行「实证 … 文案一张卡、规则一张卡、出处一张卡」才是案例。
// ════════════════════════════════════════════════════════════════════
export const RUNGS = Object.freeze(['R1', 'R2', 'R3'])

export const RUNG_WHY = Object.freeze({
  R1: 'cased —— 现状：具体案例 + 抽象反例 + 硬要求。⛔ 同 session 重跑，不引用已发表读数',
  R2: 'decased —— 剥掉实证整行（产品名随之消失），只剩抽象反例 + 硬要求',
  R3: 'minimal —— 再剥掉「（内容 / 规则 / 出处各一段）」，只剩硬要求。这是「提炼成通用规则」的可测形态',
})

// 判据的骨架 —— 三档都必须留着它，剥没了就不是去案例化而是毁判据
const CRITERION_CORE = '加一个条目 = 追加一个单元、不动任何已有单元'

// 案例专属词 —— R2/R3 里一个都不许剩
const CASE_TOKENS = ['TVU Pack', 'LCD', '文案一张卡', '交付卡', 'mockup-conventions', '五轮']

// 抽象反例 —— R3 里不许剩，R2 里必须还在
const ABSTRACT_FOIL = '（内容 / 规则 / 出处各一段）'

export function makeRuleVariant(text, rung) {
  if (!RUNGS.includes(rung)) throw new Error(`未知档位：${rung}`)

  let out = text
  if (rung === 'R2' || rung === 'R3') {
    // 实证是**独占一行**的段落 ⇒ 整行删。⛔ 不按行号（pin 内会漂），按行首标记匹配。
    const before = out
    out = out.replace(/^\*\*实证[^\n]*\n/m, '')
    if (out === before) {
      throw new Error('[fail-closed] 没找到「**实证…」那一行 ⇒ 规则原文形态变了，⛔ 不许静默按原样跑')
    }
  }
  if (rung === 'R3') {
    if (!out.includes(ABSTRACT_FOIL)) {
      throw new Error(`[fail-closed] R3 找不到抽象反例「${ABSTRACT_FOIL}」⇒ 原文形态变了`)
    }
    out = out.replace(ABSTRACT_FOIL, '')
  }
  return out.trim()
}

/**
 * 扰动算子的 must-hit（N83.1）。
 * ⛔ 不全绿就抛 —— 「命中率变了」和「剥错了」在读数上长得一模一样，
 *    没有这层断言，整批读数说明不了任何事。
 */
export function assertLadder(variants) {
  const { R1, R2, R3 } = variants
  const fail = (m) => { throw new Error(`[fail-closed] 扰动 must-hit 失败：${m}`) }

  // ① 判据骨架三档都在（剥的是案例，不是判据）
  for (const [k, v] of Object.entries(variants)) {
    if (!v.includes(CRITERION_CORE)) fail(`${k} 里判据骨架「${CRITERION_CORE}」没了 ⇒ 剥过头了`)
  }
  // ② 案例词：R1 至少有一个，R2/R3 一个都没有
  const hit = (v) => CASE_TOKENS.filter((t) => v.includes(t))
  if (hit(R1).length === 0) fail('R1 里一个案例词都没有 ⇒ 原文形态变了，本次扰动无对照意义')
  if (hit(R2).length) fail(`R2 仍残留案例词 ${JSON.stringify(hit(R2))} ⇒ 去案例化没做干净`)
  if (hit(R3).length) fail(`R3 仍残留案例词 ${JSON.stringify(hit(R3))} ⇒ 去案例化没做干净`)
  // ③ 抽象反例：R1/R2 在，R3 不在
  if (!R1.includes(ABSTRACT_FOIL)) fail('R1 缺抽象反例 ⇒ 原文形态变了')
  if (!R2.includes(ABSTRACT_FOIL)) fail('R2 缺抽象反例 ⇒ R2 剥过头，变成 R3 了')
  if (R3.includes(ABSTRACT_FOIL)) fail('R3 仍含抽象反例 ⇒ R3 没剥干净')
  // ④ 严格单调递减（⛔ 等长说明某一档没剥动）
  if (!(R1.length > R2.length && R2.length > R3.length)) {
    fail(`三档长度非严格递减：R1=${R1.length} R2=${R2.length} R3=${R3.length}`)
  }
  return true
}
```

- [ ] **Step 2: 加 `--verify-ladder` 自检模式（这就是本仓的「测试」）**

本仓无测试框架，既有惯例是 `.mjs` 内置 fail-closed 自检（`--dry-run` 的首尾核查）。照办。
在 [llm-judge-container.mjs:381](../adapters/llm-judge-container.mjs#L381) 的 `} else {` 之前插入分支：

```js
} else if (argv.includes('--verify-ladder')) {
  const pin = arg('--pin')
  if (!pin) { console.error('缺 --pin'); process.exit(2) }
  const base = extractRuleT(pin)
  const variants = Object.fromEntries(RUNGS.map((r) => [r, makeRuleVariant(base, r)]))
  assertLadder(variants)          // ⛔ 不全绿直接抛
  for (const r of RUNGS) {
    const v = variants[r]
    console.log(`\n══ ${r} ══  ${v.length} 字  templateHash=${templateHash(v).slice(0, 16)}`)
    console.log(`  why: ${RUNG_WHY[r]}`)
    console.log('  ┌─ 首 160 字 ──────────────')
    console.log('  ' + v.slice(0, 160).replace(/\n/g, '\n  '))
    console.log('  ├─ 尾 160 字 ──────────────')
    console.log('  ' + v.slice(-160).replace(/\n/g, '\n  '))
    console.log('  └──────────────────────────')
  }
  console.log('\n🟢 扰动 must-hit 全绿：判据骨架三档俱在 · 案例词 R2/R3 清零 · 抽象反例仅 R3 剥掉 · 长度严格递减')
  process.exit(0)
} else {
```

- [ ] **Step 3: 跑自检，肉眼核三档首尾**

```bash
node adapters/llm-judge-container.mjs --verify-ladder --pin <DS-pin>; echo "exit=$?"
```

期望：`exit=0` + 三档首尾打印。**必须肉眼核**：R2 尾部不该再出现 `文案一张卡`；
R3 的硬要求行应读作 `⛔ 禁按层横切，那是「一次性」的形态`。

⚠️ 若断言抛错 ⇒ **停下**，先看是 pin 里的 T 原文形态变了（DS 改过）还是剥离正则错了。
⛔ 不许放宽断言让它过（N80.1）。

- [ ] **Step 4: Commit（此时还没发任何真实调用）**

```bash
git add adapters/llm-judge-container.mjs
git commit -m "feat(adapters): 触发器 T 判读器去案例化扰动算子（三档梯子）+ fail-closed 自检

发现：extractRuleT 的抽取范围含 DS meta-rules.md 的实证整行，
而该行逐字点名了 must-hit 样本的三个段落名 ⇒ 已发表的 100% 存在未排除的混淆（H2）。
本 commit 只加算子与自检，⛔ 未跑任何真实调用。"
```

---

## Task 2: 预注册判读规则（⛔ 先 commit 再跑）

**Files:**
- Modify: [`adapters/llm-judge-container.mjs`](../adapters/llm-judge-container.mjs)（在 `PREREG` 之后插入）

**Interfaces:**
- Produces: `PREREG_LADDER`（常量，供报告逐字引用）

- [ ] **Step 1: 写预注册常量**

在 [llm-judge-container.mjs:58](../adapters/llm-judge-container.mjs#L58)（`PREREG` 的 `}` 之后）插入：

```js
// ════════════════════════════════════════════════════════════════════
// 去案例化阶梯的预注册（N80.1：先 commit 再跑）
//
// ⛔ 这里预注册的是**判读规则**，不是期望值 ——
//    三档各自的绝对命中率 lab 事先不知道，设任何值都是编的。
// ════════════════════════════════════════════════════════════════════
export const PREREG_LADDER = {
  n: 5,
  rungs: ['R1', 'R2', 'R3'],
  // 主判别项：两条 must-hit 的方向正确率在 R1 → R3 上怎么走
  discriminator: ['must-hit-0/lcd-before', 'must-hit-1/synthetic-attribute-split'],
  // 预注册的读法（⛔ 跑完不许改）
  decisionRule: {
    'H2 获强支持': 'R1 ≥ 4/5 且 R3 ≤ 2/5 ⇒ 命中依赖案例被写进 prompt，v2 §2 结论需更正',
    'H2 未获支持': 'R3 ≥ 4/5 ⇒ 判据脱离案例仍站得住，§2 结论从「未排除混淆」升为「已排除这一条」',
    '不下结论': 'R3 == 3/5（中间带）⇒ N 不足，触发下面的条件补跑，⛔ 不许在 n=5 上硬读',
  },
  // ⛔ 条件动作也是预注册的一部分，⛔ 不是事后加的
  conditionalTopUp: {
    when: '任一 discriminator 在 R3 落进中间带 3/5',
    then: '该样本 R1 与 R3 两档各补跑至 n=10，⛔ 只补这两档、⛔ 不补 R2、⛔ 不改 decisionRule',
  },
  // ⛔ must-not-hit-0 不是判别项 —— 它的段落名本就不匹配案例，
  //    H1 与 H2 都预测它三档全低 ⇒ 它区分不了两个假说。列为观察项。
  observeOnly: ['must-not-hit-0/lcd-after', 'observe/oneshot-plan'],
}
```

- [ ] **Step 2: Commit 预注册（⛔ 这一步必须在跑之前）**

```bash
git add adapters/llm-judge-container.mjs
git commit -m "chore(adapters): 预注册去案例化阶梯的判读规则 + 条件补跑（N80.1 先 commit 再跑）

⛔ 预注册的是判读规则不是期望值 —— 三档绝对命中率 lab 事先不知道。
must-not-hit-0 显式列为观察项：H1/H2 都预测它三档全低，它区分不了两个假说。"
```

---

## Task 3: 三档 CLI + dry-run

**Files:**
- Modify: [`adapters/llm-judge-container.mjs`](../adapters/llm-judge-container.mjs)（`runCalibrate` 加 `rung` 参数；CLI 加 `--ladder`）

**Interfaces:**
- Consumes: `makeRuleVariant`、`assertLadder`、`PREREG_LADDER`、既有 `judgeOnce`/`CONTROLS`/`checkExpect`
- Produces: `runLadder({pin, model, n, out, dry, cliPath}) -> 0|1`

- [ ] **Step 1: 加 `runLadder`**

在 [llm-judge-container.mjs:365](../adapters/llm-judge-container.mjs#L365)（`runCalibrate` 的 `}` 之后）插入：

```js
function runLadder({ pin, model, n, out, dry, cliPath }) {
  const base = extractRuleT(pin)
  const variants = Object.fromEntries(RUNGS.map((r) => [r, makeRuleVariant(base, r)]))
  assertLadder(variants)  // ⛔ 扰动 must-hit 不全绿，一次调用都不发
  const subjectSha = execFileSync('git', ['-C', pin, 'rev-parse', '--short', 'HEAD'],
    { encoding: 'utf8' }).trim()

  if (dry) {
    let total = 0
    for (const r of RUNGS) {
      console.log(`\n[dry-run] ${r}  ${variants[r].length}字  hash=${templateHash(variants[r]).slice(0, 16)}`)
      for (const c of CONTROLS) {
        const bytes = Buffer.byteLength(buildPrompt(variants[r], c.load()))
        total += bytes * n
        console.log(`   ${c.id.padEnd(38)} prompt=${String(bytes).padStart(6)}B ×${n}`)
      }
    }
    console.log(`\n[dry-run] 合计 ${RUNGS.length * CONTROLS.length * n} 次调用，输入约 ${(total / 1024).toFixed(1)} KB`)
    console.log('[dry-run] ⛔ 未发出任何真实调用')
    return 0
  }

  const summary = { auditId: 'llm-judge-decase-ladder', subjectSha, modelId: model,
    promptTemplateVersion: TEMPLATE_VERSION, prereg: PREREG_LADDER, rungs: {} }

  for (const r of RUNGS) {
    const ruleText = variants[r]
    const tHash = templateHash(ruleText)
    const dir = join(out, r)
    mkdirSync(dir, { recursive: true })
    summary.rungs[r] = { why: RUNG_WHY[r], ruleChars: ruleText.length, templateHash: tHash, samples: [] }
    console.log(`\n══ ${r} ══ ${RUNG_WHY[r]}`)
    for (const c of CONTROLS) {
      const doc = c.load()
      const prompt = buildPrompt(ruleText, doc)
      const costs = [], units = []
      let nulls = 0
      for (let i = 1; i <= n; i++) {
        const res = judgeOnce(cliPath, model, prompt)
        const appendCost = res.parsed ? res.parsed.appendCost : null
        if (appendCost === null) nulls++
        costs.push(appendCost)
        units.push(res.parsed?.unitsToModify ?? null)
        writeFileSync(join(dir, `${c.id.replace(/\//g, '_')}-${i}.json`), JSON.stringify({
          subjectSha, modelId: model, promptTemplateVersion: TEMPLATE_VERSION,
          rung: r, templateHash: tHash, docHash: sha256(doc),
          sampleId: c.id, iteration: i, appendCost,
          unitsToModify: res.parsed?.unitsToModify ?? null,
          wallMs: res.wallMs, exitCode: res.exitCode, raw: res.raw,
        }, null, 2))
        const ok = checkExpect(c.expect, appendCost)
        console.log(`  ${c.id.padEnd(38)} #${i} appendCost=${String(appendCost).padStart(4)} ` +
          `${ok === null ? '·' : ok ? '🟢' : '🔴'} ${res.wallMs}ms`)
      }
      const judged = costs.map((x) => checkExpect(c.expect, x)).filter((x) => x !== null)
      summary.rungs[r].samples.push({ id: c.id, expect: c.expect, costs, units, nulls,
        agreement: judged.length ? judged.filter(Boolean).length / n : null })
    }
  }

  mkdirSync(out, { recursive: true })
  writeFileSync(join(out, 'summary.json'), JSON.stringify(summary, null, 2))

  // ⛔ 报完整分布 + 档间趋势，⛔ 不下 H2 判定（判定按 PREREG_LADDER.decisionRule 在报告里做）
  console.log('\n══ 三档趋势（主判别项）══')
  for (const id of PREREG_LADDER.discriminator) {
    const row = RUNGS.map((r) => {
      const s = summary.rungs[r].samples.find((x) => x.id === id)
      return `${r}=[${s.costs.join(',')}] ${s.agreement === null ? '—' : (s.agreement * 100).toFixed(0) + '%'}`
    })
    console.log(`  ${id}\n      ${row.join('   ')}`)
  }
  console.log('\n  观察项（⛔ 不参与 H2 判定）：')
  for (const id of PREREG_LADDER.observeOnly) {
    const row = RUNGS.map((r) => {
      const s = summary.rungs[r].samples.find((x) => x.id === id)
      return `${r}=[${s.costs.join(',')}]`
    })
    console.log(`  ${id}\n      ${row.join('   ')}`)
  }
  console.log('\n⚠️ 每档 templateHash 不同 ⇒ 按覆盖边界 ④ 三档是三个量；' +
    '本器读的是**同 session 同模型下的档间差**，⛔ 不得与 runs/judge-cal-6ac56de2/ 跨读数比较')
  return 0
}
```

- [ ] **Step 2: 接 CLI**

在 [llm-judge-container.mjs:369](../adapters/llm-judge-container.mjs#L369) 的
`} else if (argv.includes('--calibrate')) {` **之前**插入：

```js
} else if (argv.includes('--ladder')) {
  const pin = arg('--pin')
  if (!pin) { console.error('缺 --pin'); process.exit(2) }
  process.exit(runLadder({
    pin,
    model: arg('--model') ?? 'claude-opus-5',
    n: Number(arg('--n') ?? PREREG_LADDER.n),
    out: arg('--out') ?? 'runs/judge-ladder',
    dry: argv.includes('--dry-run'),
    cliPath: resolveCli(arg('--cli')),
  }))
```

同时把最后的 usage 行补上 `--ladder`。

- [ ] **Step 3: dry-run 核规模**

```bash
node adapters/llm-judge-container.mjs --ladder --dry-run --pin <DS-pin>; echo "exit=$?"
```

期望：`exit=0`，合计 **60 次调用**（3 档 × 4 样本 × 5）。三档 `hash` 两两不同。

- [ ] **Step 4: Commit**

```bash
git add adapters/llm-judge-container.mjs
git commit -m "feat(adapters): 三档梯子跑法 --ladder（同 session 重跑 R1，⛔ 不引用已发表读数）"
```

---

## Task 4: 跑三档（⛔ 跑之前先核 DS pin 干净）

**Files:** 产物 `runs/judge-ladder-<sha>/`

- [ ] **Step 1: 核 pin 干净 + 记 DS HEAD**

```bash
git -C <DS-pin> status --porcelain; echo "exit=$?"
git -C <DS-pin> rev-parse --short HEAD
```

期望：`status` 输出为空。⚠️ 非空 ⇒ **停**（fail closed，⛔ 不许 `allowDirty`）。

- [ ] **Step 2: 跑（≈60 次 × 40s ≈ 40 分钟，后台跑）**

```bash
node adapters/llm-judge-container.mjs --ladder --pin <DS-pin> \
  --model claude-opus-5 --n 5 --out runs/judge-ladder-<sha>; echo "exit=$?"
```

- [ ] **Step 3: 跑完复核 pin 仍干净**

```bash
git -C <DS-pin> status --porcelain; echo "exit=$?"
```

- [ ] **Step 4: 按预注册判条件补跑**

看 `summary.json` 里两条 discriminator 在 **R3** 的 `agreement`：
- 有任一 `== 0.6`（3/5）⇒ 按 `PREREG_LADDER.conditionalTopUp` 补跑该样本 R1/R3 至 n=10。
- 否则 ⇒ ⛔ 不补跑（补了就是拿数据换结论）。

- [ ] **Step 5: Commit 原始记录**

```bash
git add runs/judge-ladder-*/
git commit -m "data(runs): 去案例化阶梯三档读数（R1/R2/R3 × 4 样本 × n=5）"
```

---

## Task 5: 报告 + status 登记

**Files:**
- Create: `reports/2026-08-31-trigger-t-decase-ladder.md`
- Modify: [`docs/round2-status.md`](./round2-status.md)（§30.5 追加 N84 / N84.1）

- [ ] **Step 1: 写报告**

必须逐字含这几节：
- §0 一句话结论 —— 按 `PREREG_LADDER.decisionRule` 三选一，⛔ 不许自创第四种读法
- §1 缺陷从哪来（`extractRuleT` 抽取范围 + DS `meta-rules.md:975`），含**为什么两个假说都拟合旧读数**
- §2 三档定义 + 扰动 must-hit 全绿证据（`--verify-ladder` 输出）
- §3 三档完整分布（⛔ 逐次，不是一致率）+ 档间趋势
- §4 **对已发表读数的处置** —— 若 H2 获支持，明写
  「`reports/2026-08-31-trigger-t-llm-judge.md` §2 的结论撤回；§3 的 A/B 拆分因建立在该读数上，⛔ 同样不得再引用」
- §5 **⛔ 本报告没有产出什么**：没有 v3、没有 Figma 面、⛔ 没有证明 H1 错（H1 与 H2 可同时成立）
- §6 对 v3 的输入（⛔ 不代拍）

⚠️ **报告里 ⛔ 不得出现产品名当判据**：讲实证时点名可以（读数要可复核），
但**结论句必须是产品无关的**。

- [ ] **Step 2: status 追加 N84 / N84.1**（⛔ 只追加行，不改既有行）

- [ ] **Step 3: Commit**

```bash
git add reports/2026-08-31-trigger-t-decase-ladder.md docs/round2-status.md
git commit -m "feat(metrics): 去案例化阶梯读数 —— 判据文本带实证案例是否混淆 v2 读数"
```

---

## Task 6: 判据设计约定页（Q2 固化件，**产品无关**）

**Files:**
- Create: `docs/criterion-design-conventions.md`

**这一页是闸口，不是案例记录。** 硬约束：

- ⛔ **全文不得出现任何产品名、案例名、仓库名、人名**（`TVU`/`LCD`/`Pack`/`DS` 皆不许）。
  自检：`grep -nE 'TVU|LCD|Pack|Figma|触发器 T' docs/criterion-design-conventions.md` 必须**零命中**。
- ⛔ **不得内嵌实证正文** —— 实证留在 `reports/` 与 `round2-status.md`，本页只用**不含产品名的指针**
  （如「实证登记见 round2-status §30.5 N83.1」）。
- 每条规则的形态：**一句可执行的祈使句 + 为什么（一句） + 怎么自查（一句）**。

- [ ] **Step 1: 写四条已成立的规则**

按同族顺序（这四条是同一条纪律在四个位置上的出现）：

1. **每个绿都要回答「它和谁比，那个『谁』是不是同源」**（N81.1）
2. **扰动/合成样本照真实病灶造，⛔ 不照判据造**（N82.1）
3. **搜寻器/扰动算子和判据一样要配 must-hit，且 fail-closed**（N83.1）
   —— 因为 recall 未知时「命中 0」和「口径瞎了」在读数上长得一模一样
4. **判据文本交给判读器时，⛔ 不许把它自己的实证案例一起喂进去**（本轮新增）
   —— 否则判读器可能在照案例匹配而不是照规则判，而这两种行为在读数上长得一模一样

加两条已成立的横向约束：

5. **凡数一个 count 当判据，先问里面有几格是任何样本都会有的**（N82.2）
6. **判据库每条必须显式标注「可机检 / 需 LLM 判读 / 需人判」，⛔ 不默认判据都能落成闸**
   （来自触发器 T 探针 §5 立项问 ② 的负向答案）

- [ ] **Step 2: 跑产品名自检**

```bash
grep -nE 'TVU|LCD|Pack|Figma|触发器 T|meta-rules' docs/criterion-design-conventions.md; echo "exit=$?"
```

期望：无输出、`exit=1`（grep 无命中）。⚠️ 有命中 ⇒ 改到零。

- [ ] **Step 3: Commit**

```bash
git add docs/criterion-design-conventions.md
git commit -m "docs(conventions): 判据设计约定页（产品无关）—— 同族四条 + 两条横向约束

闸口页 ⛔ 零产品名；实证留在 reports/ 与 status，本页只用不含产品名的指针。"
```

---

## Task 7: 递给 DS 的处方（(b)）

**Files:**
- Create: `proposals/2026-08-31-rule-text-decasing.md`

按 [`proposals/2026-08-28-claude-design-real-components.md`](../proposals/2026-08-28-claude-design-real-components.md) 的既有格式：
日期 / 目标 sha / 执行方（DS 侧，lab ⛔ 未改 DS 任何字节）/ 一句话 / 逐条改法 / 验证物。

- [ ] **Step 1: 写处方**

主张（⛔ 依赖 Task 5 的读数，**必须等读数出来再写**，⛔ 不许先写结论）：

> DS `docs/meta-rules.md` 的触发器 T（及同构的其他触发器）把**实证案例**与**判据**写在同一节里。
> 任何程序化抽取该节当判据的下游消费者，都会连案例一起吃进去。
> ⇒ 建议把**实证**移出判据节（挪到文末「本规则的实证沉淀」附录，该附录已存在），
> 判据节只留：机械触发三行块 + 性质→结构要求表 + 三条边界。

必须写清的两件事：
- **这不是文风建议，是可测的**：附上 Task 4 三档读数作为证据（含 R3 那档的实际表现）。
- **⛔ lab 不裁定**：改不改由 DS 拍；lab 侧已在自己的判读器里做了剥离（Task 1），
  ⇒ **DS 不改也不阻塞 lab**，改了则下游都受益。

⚠️ 若 Task 5 判定「H2 未获支持」（R3 ≥ 4/5），处方**仍然要写**，但主张降档为
「判据脱离案例仍站得住 ⇒ 移出实证是**零风险**的可读性改进」，⛔ 不得把它写成「必须改」。

- [ ] **Step 2: Commit**

```bash
git add proposals/2026-08-31-rule-text-decasing.md
git commit -m "docs(proposals): 处方 —— 建议 DS 把实证移出判据节（下游程序化抽取会连案例一起吃）"
```

---

## Self-Review（写完计划后的自查）

**1. Spec 覆盖**
- (a) lab 侧剥离 → Task 1–5 ✅
- (b) DS 侧处方 → Task 7 ✅
- Q2「暂不建，只固化那一条」→ Task 6 ✅
- 「闸口不写产品名」→ Task 6 硬约束 + `grep` 自检 ✅

**2. Placeholder 扫描** —— Task 5/6/7 的正文内容在执行时才写，但**每条都给了必须含的小节清单与硬约束**，
⛔ 无 "TBD"。Task 1–4 全部给了可直接粘贴的代码与命令。

**3. 类型一致性** —— `makeRuleVariant(text, rung)` / `assertLadder(variants)` / `RUNGS` / `RUNG_WHY` /
`PREREG_LADDER` / `runLadder({...})` 在 Task 1/2/3 定义、Task 3/4 使用，签名一致 ✅。

**4. ⚠️ 已知未解决**
- **N=5 分辨率粗** —— 已用预注册的中间带 + 条件补跑处理，⛔ 但 n=10 仍是小 N，报告里必须显式写出。
- **Figma 面仍未测** —— 本计划不消除这个缺口，报告 §5 必须逐字重申。
- **H1 与 H2 可同时成立** —— 本计划只检验 H2，⛔ 不得读成「H1 被推翻」。
