// [[INFRA-F138]] —— `scripts/audit-handoff-deliverable-sections.mjs` 的**整脚本**回归面。
// -----------------------------------------------------------------------------
// 与 `tests/audit-handoff-deliverable-sections.test.ts` 的分工（⛔ 别合并，两者测的不是一回事）：
//   · 那一份 `import { SECTIONS, findMissingSections, stripFences }` 直接调判据函数
//     ⇒ 覆盖判据逻辑，但**结构上看不见**：入口守卫 / 两个挂载层各自传什么参数 /
//     参数怎么解析成扫描面 / 判据 → 两条流 → 退出码这一整条接线。它连 `main` 都没导入。
//   · 本份 **spawn 整个脚本**（共享 harness `tests/lib/gate-fixture-root.ts`）⇒ 覆盖上述整条。
//     被测闸一行不改。
//
// 为什么选它（**爆炸半径**，⛔ 不是「挂载最深」也不是「最便宜」—— 它只 2 层
//   L4 `.husky/pre-commit` 的 `$staged_md` 块 + L5 `pr-checks.yml` 的「L4-only 闸接线 第 4 批」）。
//   五条都可核，其中 ⑤ 是 [[INFRA-F138]] entry **没有算进去**的因子：
//   ① **一条闸守四条 L1 规则** —— M37 / M42 / M-LIFECYCLE-CRUD / M35(=R13) 各自那份可落盘产物
//      （Reference probe 段 / clone source probe table / CRUD self-check 四答 / 3 行 affordance
//      trace）的**唯一存在性闸**（闸头注释逐字「判据早就设计好了，缺的只是一个存在性闸 ——
//      这些产物又恰好落在同一份 handoff .md 上，所以一个闸能同时守四条」）。
//      ⇒ 假绿 ⇒ **四条规则同时静默失去机械保障**，而那批产物正是无人会人眼核的那一类。
//   ② **它是 R13 三个落点里 handoff 那一个的唯一守卫** —— commit-message 那半由
//      `commit-affordance-trace` 各自守，**PR description 那一落点是 L1、本来就无闸**
//      （Gitea PR 正文不在仓库里）。⇒ 本闸假绿 ⇒ R13 只剩一条腿。
//   ③ **两个挂载层走两条不同的代码路径** —— L4 传**显式 staged 文件列表**（无 `--all`），
//      L5 传 **`--all`**（经 `git ls-files` 全仓扫描 + 命中 0 份时 fail closed）。
//      旧面两条都看不见。这正是「判据有几个调用面都要过」那条验收问。
//   ④ **它自己有 D1 挂载自声明**（2026-08-28 实测**不在** `gate-mount-declaration` 的
//      `KNOWN_SILENT` 表里）—— 这一格与上一轮的 prop-naming **相反**，如实记下，
//      它是本闸相对**弱**的一格，不是强项。
//   ⑤ 🔴 **entry 没算的因子：它有一份随包发的 consumer 镜像，而随包面判定结构上看不见。**
//      `templates/consumer-product/.githooks/audit-handoff-deliverable-sections.mjs` 是它的
//      零依赖自包含镜像，接在该模板的 `.githooks/pre-commit`（实测第 58 行）上；而
//      `templates` **在 `package.json` 的 `files[]` 里** ⇒ 随 npm 包发到每个 consumer。
//      棘轮的「随包面」只按**闸自身路径**匹配 `files[]`（`scripts/audit-mockup-*.mjs` 那条 glob）
//      ⇒ 本闸被判为「非随包」，而它实质上是随包的 —— 随的是副本。
//      ⚠️ 且 `tests/consumer-hook-mirror-parity.test.ts` 只按**判定函数**钉两边一致
//      （它自己的头注释逐字写明「不按 CLI 形态钉」）⇒ **两边的接线都零覆盖**：
//      DS 侧接线坏掉时，那份 parity 测试照样绿，镜像侧同型的接线也没有任何东西看得见。
//      ⇒ 假绿代价落在**每个 consumer 的 handoff 校验**上，与第五十 / 五十九 / 六十三轮
//      「守的对象随包而闸自己不随包」是同族，但**形态是新的**（随包的是闸的副本）。
//      ⛔ 本面**不动**镜像、不动棘轮口径 —— 改闸 / 改判定口径都是 [[INFRA-F138]] 逐字封的路，
//      如实登记、报 owner。
//
// 🔴 **harness 的 `realpathSync` 对本闸是承重的。** 本闸用**原始**入口守卫
//   （`process.argv[1] === fileURLToPath(import.meta.url)`），属 [[INFRA-F138]] 记的
//   「14 条非随包闸仍用原始守卫」那一档（⛔ 那不是缺陷 —— 它们只从仓库根跑）。
//   ⚠️ 但请注意 ⑤：它的**镜像**在 consumer 侧同样是原始守卫，那一份的 symlink 安全性
//   不由本面、也不由 [[INFRA-F140]] 覆盖（`is-cli-entry` 的治理面只含 `files[]` 里的
//   **脚本**，不含 `templates/` 下的镜像）。如实登记，⛔ 不在本面修。
//
// 🔴 **⛔ 本轮与前十一轮不同：共享 harness 改了一行（被测闸仍是一行没改）。** 如实登记：
//   本闸是 harness 的**第一个带 git 依赖的用户**（35 份 harness 用户里唯一跑 git 的），
//   于是撞到 `runGate` 直接继承 `process.env` ⇒ pre-commit 注入的绝对路径 `GIT_DIR` /
//   `GIT_INDEX_FILE` 会让 fixture 里的 git 去操作**真仓库正在提交的那份 index**。
//   实测代价（本轮真撞了一次）：首次 `git commit` 时同一轮 vitest 里另外三份**本轮一个字
//   没改**的测试一起转红（`deliverable-open-questions` / `deploy-marker` /
//   `shipped-import-closure` 的真仓库非空过钉）—— 它们读的是被污染的 index。
//   ⇒ 收进 harness 的理由与 2026-08-27 的 `realpathSync` **逐字同构**：
//   `tests/audit-deliverable-open-questions.test.ts:114` 早就记过同一条并**就地**建了个
//   `cleanEnv`，但没进共享 harness ⇒ 走 harness 的下一个人（本轮）又付了一次。
//   ⛔ **这不是「为了让 harness 能用去改活闸」**（那才是 entry 逐字封的路）—— 改的是
//   harness 自己，且对 34 份不跑 git 的既有用户是零行为变化。C 组配了回归钉。
//
// 🔴 **本轮给 [[INFRA-F138]] 论点补的形态：相邻两条闸的红走相反的流。**
//   上一轮的 prop-naming **判据红全走 stdout、stderr 零字节**；本闸恰好相反 ——
//   **判据红全走 stderr、stdout 零字节**（显式列表形态下），而 `--all` 形态又会先往
//   stdout 印一行分母。⇒ 任何跨闸统一的「grep 哪条流判红绿」都会在其中一条上读反。
//   前几轮撞的是「退出码变输出不变」「输出变退出码不变」「退出码对而红的原因全变」
//   「红与崩溃各占一条流」，这是第五种：**同一族相邻两条闸，流向互为镜像**。K 组按流分别钉。
//
// 🔴 **本轮如实登记三处缺陷，⛔ 本面一律钉「现行行为」而非背书；改闸是 entry 封的路 ⇒ 报 owner：**
//   **(a) 显式列表形态下没有任何分母 —— 且比同族前三例更弱。** 无参（L4 的 staged 里没有
//      .md 交付物时）⇒ `files=[]` ⇒ 循环不跑 ⇒ 直接印 `OK — 0 findings.` + `EXIT=0`。
//      独立探针实测：**「无参」「一份合规」「一份非交付物」「一份不存在的文件」四种情形的
//      stdout 逐字相同（53 B）、stderr 均零字节、退出码均 0** ⇒ 连人眼都分不出。
//      同族前三例（mockup-library-binding「绿档没有任何分母」/ product-code 的 `--dir` 空分母 /
//      prop-naming 的 `0 components scanned`）里，prop-naming 至少**印了**分母 ⇒ 人眼可见；
//      本闸这一档**连印都没印**，是同族第四例、也是最弱的一档。
//      ⚠️ 如实说清它**不**是什么：`--all` 那一侧**有** fail-closed（命中 0 份即红），
//      所以 L5 那条路径是被守住的；塌的只有 L4 那条显式列表路径。
//   **(b) `--all` 与显式列表同时给时，自印在撒谎。** `resolveDeliverableTargets` 对
//      `--all a.md` 返回 `{files:['a.md'], all:true}` ⇒ 闸照印
//      `--all 全仓扫描，交付物 1 份`，而它**根本没有扫全仓**（`listDeliverables` 未被调用）。
//      形态同第五十七轮「自印把 B 的来源写成 prepublishOnly」、第五十九轮「印成
//      `undefined 条具名豁免`」。修法是一行措辞，未实施。
//   **(c) 读不到的文件被静默跳过。** `try { readFileSync } catch { continue }` ⇒ 传一个
//      不存在的 `.md` 既不报错也不计数，与「读到了且合规」逐字同形（见 (a) 的四同形）。
//
// ⛔ 本面覆盖不到的一支（**如实登记的边界，不是待补 TODO**，与前几轮的 `EXEMPTIONS` 空表 /
//   `STRUCTURAL_L4_ONLY` / `KNOWN_COLLISIONS` / prop-naming 的 `Node<22` skip 分支同型）：
//   **`listDeliverables` 里 `git ls-files` 自身失败那一支的「非崩溃」形态**。fixture 侧能造的
//   只有「root 不是 git 仓库」（H 组钉住 ⇒ 抛错 ⇒ 崩溃 exit 1，闸头注释逐字声明的 fail closed
//   ①），而「git 可用但返回非零」需要伪造一个 git 二进制，那要动 PATH、不属零改动 fixture。
//   H 组那条自钉判据把前提钉住（真仓库跑得动 `git ls-files`），前提一破本面的绿会变成
//   那一支的绿，该判据当场转红并把人指到这里。
//
// ⛔ 污染面纪律（[[INFRA-F138]] 逐字）：fixture 假名统一 `dworp`（落地前实测全仓 0 文件命中，
//   含 30 个历轮 worktree；独立探针另用 `flenk`，**刻意不同以免自污染**）。散文里提到别的闸
//   一律**去 `audit-` 前缀、去 `.mjs` 后缀、去 `audit:` 冒号**。
//   ⚠️ **一处必须留全名的例外**：`copyFiles` 里 scope 脚本的真实路径 —— 被测闸 `import` 它，
//   harness 要照这个路径把它拷进 fixture，改一个字 fixture 就建不起来。判定它安全的依据是
//   量具的 E 类判据实现（`HARNESS_CALL_RE = /\brunGate\s*\(/`、窗口 = 命中行往下 3 行）：
//   那一行离任何 `runGate(` 都远超 3 行，且该 scope 闸**本来就已是 `E-fx`**（有自己的整脚本
//   回归面）⇒ 即便被误命中也不改变它的分类。落地前已用程序化污染面核逐条比对过。
//
// ⚠️ **本闸绿档的 stdout 在显式列表形态下不含任何 fixture 自有读数**（就一句固定的 OK）
//   ⇒ 非空过凭据按第五十轮 `doc-de-mirror` 的既有范式补齐：**红绿配对**（跑错了树的话红档
//   根本红不了）+ **红档钉 fixture 自己的文件路径**（含 `dworp`）+ **`--all` 形态钉 fixture
//   自己的分母**（fixture 3 份 / 真仓库 1 份，两侧都从活源现算、⛔ 不写死）。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
  gitCleanEnv,
  REPO_ROOT,
  type GateRun,
} from './lib/gate-fixture-root'
import { listDeliverables } from '../scripts/audit-deliverable-open-questions.mjs'

const GATE = 'scripts/audit-handoff-deliverable-sections.mjs'

/**
 * 被测闸 `import` 的同仓脚本 —— harness 必须**拷**（不能软链，见 harness 头注释：
 * ESM 走 realpath 会把它的 `import.meta.url` 解回真仓库）。
 * ⚠️ 这一行是上方污染面纪律说的那个「必须留全名的例外」，⛔ 别改写成拼接来「脱敏」——
 *    改了 harness 就找不到文件，而它离任何 `runGate(` 都远超 3 行窗口。
 */
const COPY_FILES = ['scripts/audit-deliverable-open-questions.mjs']

const MARKER = '<!-- mockup-handoff -->'

// ── fixture 自己的读数：**纯 ASCII 手写的设计值**，⛔ 不用被测代码算 ──────────────
const FIX_TRACE = 'Affordance: dworp-directional (intent: 展开)\nVocabulary scan: [Arrow/Down]\nChosen: Arrow/Down — 同几何\n'
/** 全段齐全的合规交付物（2026-09-07 四→七段 Q1 第一批；2026-09-08 七→十一段 Q1 B1 批）。 */
const FIX_FULL = `${MARKER}\n\n## Reference probe\n| dworp |\n\n## clone source probe table\nN/A — 本轮无 clone\n\n## CRUD self-check\nN/A — view-only\n\n## Affordance trace\n${FIX_TRACE}` + `\n\n## PRD examples scoped\nN/A — PRD 无 e.g. 列表\n\n## Cardinality self-check\nN/A — 单对象无跨页关系\n\n## IA 树\nN/A — 单 entity\n\n## Selection model\nN/A — 无选中态` + `\n\n## derive from \`1:23\`\n\n## sectionInternalOverlap = 0\nsectionToSectionOverlap = 0\n\n## Buttons used: Button variants [primary]\n\n## reused file-local components\n| m | 2 | yes |`
/** 带标记但四段全缺。 */
const FIX_EMPTY = `${MARKER}\n\n## 1. dworp 现状\n`
/** 不带标记、也不在 docs/handoffs/ 下 ⇒ 不是交付物。 */
const FIX_NOT_DELIVERABLE = '# dworp 普通文档\nReference probe 这几个字在这里也不算数\n'

const FIX_OK_PATH = 'docs/handoffs/dworp-ok.md'
const FIX_BAD_PATH = 'docs/handoffs/dworp-bad.md'
/** `--all` 绿档的 fixture 分母 —— 设计成 3，真仓库现算是另一个数（B 组 fail-closed 自证）。 */
const FIX_ALL_COUNT = 3

type FixtureOpts = { files?: Record<string, string>; git?: boolean }

function makeRoot(prefix: string, opts: FixtureOpts = {}): string {
  const root = createGateFixture({
    gate: GATE,
    prefix,
    files: opts.files ?? {},
    copyFiles: COPY_FILES,
    // ⛔ 显式关掉 harness 默认的 `scripts/lib` 软链 —— 被测闸与它 import 的 scope 脚本
    //    都只用 node 内建（实测：两份的 import 行全是 `node:` 前缀），不碰那个目录。
    linkDirs: [],
  })
  if (opts.git) {
    // `--all` 走 `git ls-files` ⇒ 这条路径要求 fixture 是个 git 仓库。
    // ⚠️ 只 `init` + `add`（不 commit）：`git ls-files` 读的是 index。
    // 🔴 `gitCleanEnv()` **不可省**（本轮实测踩过）：pre-commit 会往环境里注绝对路径的
    //    `GIT_DIR` / `GIT_INDEX_FILE`，不剥的话这两条命令会落到**真仓库正在提交的那份
    //    index** 上 —— 不只是测试红，是往别人的提交里写文件。理由与实测见 harness 头注释。
    execFileSync('git', ['init', '-q'], { cwd: root, env: gitCleanEnv() })
    execFileSync('git', ['add', '-A'], { cwd: root, env: gitCleanEnv() })
  }
  return root
}

function run(prefix: string, args: string[], opts: FixtureOpts = {}): GateRun {
  return runGate(makeRoot(prefix, opts), GATE, args)
}

/** 取「程序自己最后那句话」—— 终态事实，见 K 组。 */
function lastLine(s: string): string {
  const lines = s.replace(/\s+$/, '').split('\n')
  return lines[lines.length - 1]
}

const OK_LINE = 'audit-handoff-deliverable-sections: OK — 0 findings.'
const HELP_LAST = '   ④ 段名逐字照抄，别改写 —— 判据就是规则原文引号里的那几个字。'

// ── 真仓库读数：⛔ **不写死**，当场从活源取，否则真仓库一改反向钉就静默变空 ──────────
const REAL_DELIVERABLES: string[] = listDeliverables()
const REAL_ALL_COUNT: number = REAL_DELIVERABLES.length

afterAll(cleanupGateFixtures)

// =============================================================================
describe('A 绿档 —— 非空过凭据 + 常驻负向对照', () => {
  it('显式列表 · 一份合规交付物 → 绿，收尾句照印', () => {
    const r = run('dworp-green-explicit', [FIX_OK_PATH], { files: { [FIX_OK_PATH]: FIX_FULL } })
    expectGateGreen(r, { contains: [OK_LINE] })
  })

  it('`--all` · fixture 自己的分母印出来（这是本闸唯一的 fixture 自有读数）', () => {
    const files: Record<string, string> = {}
    for (let i = 0; i < FIX_ALL_COUNT; i++) files[`docs/handoffs/dworp-${i}.md`] = FIX_FULL
    const r = run('dworp-green-all', ['--all'], { files, git: true })
    expectGateGreen(r, {
      contains: [
        `audit-handoff-deliverable-sections: --all 全仓扫描，交付物 ${FIX_ALL_COUNT} 份`,
        OK_LINE,
      ],
    })
  })

  it('绿档是真绿：stderr 零字节、无任何违例标记（must-not-hit 配正向锚点）', () => {
    const r = run('dworp-green-neg', [FIX_OK_PATH], { files: { [FIX_OK_PATH]: FIX_FULL } })
    expect(r.status).toBe(0)
    expect(r.stderr).toBe('')
    expect(r.stdout).not.toContain('❌')
    expect(r.stdout).not.toContain('强制段落')
    expect(r.stdout).not.toContain('[M37]')
    // 正向锚点：闸确实跑到了最后一句（否则「压根没跑」也满足上面全部 must-not-hit）
    expect(r.stdout).toContain(OK_LINE)
  })

  it('每条判据在绿档里各有一个**真的走进去又落空**的样本 ⇒ 绿不是「一条判据都没碰到」', () => {
    // 钉的是 fixture 的设计意图：合规样本必须逐条含全部锚点短语，且 M35 第二层的两行也在。
    for (const phrase of ['Reference probe', 'clone source probe table', 'CRUD self-check',
      'PRD examples scoped', 'Cardinality self-check', 'IA 树', 'Selection model', 'Affordance:',
      // ↓ 2026-09-08 Q1 B1 批新增的 4 条判据（覆盖 5 段）
      'derive from', 'sectionInternalOverlap', 'sectionToSectionOverlap',
      'Buttons used:', 'reused file-local components']) {
      expect(FIX_FULL).toContain(phrase)
    }
    expect(FIX_FULL).toContain('Vocabulary scan:')
    expect(FIX_FULL).toContain('Chosen:')
    // 且它**不是**走 N/A 豁免进来的（否则 M35 第二层的绿是豁免的绿，不是判据的绿）
    expect(/affordance:\s*n\/a/i.test(FIX_FULL)).toBe(false)
  })
})

// =============================================================================
describe('B 反向钉 —— 绿档没有退回真仓库读', () => {
  it('反向钉本身有意义（fail-closed 自证：真仓库份数 ≠ fixture 份数，且真侧非空）', () => {
    // ⛔ 若哪天真仓库的交付物份数恰好等于 fixture 的设计值，下面那条反向钉会静默失去意义。
    expect(REAL_ALL_COUNT).not.toBe(FIX_ALL_COUNT)
    // 分母非空：真侧得真的量到了东西（否则「不相等」是因为真侧是空的）
    expect(REAL_ALL_COUNT).toBeGreaterThan(0)
    // 真仓库那批交付物的路径里不含 fixture 假名 ⇒ 下面按名字做的反向钉不会互相干扰
    for (const p of REAL_DELIVERABLES) expect(p.toLowerCase().includes('dworp')).toBe(false)
  })

  it('`--all` 绿档 stdout 不含真仓库的份数（正向锚点：fixture 自己的份数确实印了）', () => {
    const files: Record<string, string> = {}
    for (let i = 0; i < FIX_ALL_COUNT; i++) files[`docs/handoffs/dworp-${i}.md`] = FIX_FULL
    const r = run('dworp-reverse-all', ['--all'], { files, git: true })
    expect(r.stdout).not.toContain(`交付物 ${REAL_ALL_COUNT} 份`)
    expect(r.stdout).toContain(`交付物 ${FIX_ALL_COUNT} 份`)
  })

  it('红档同样不含真仓库读数，且红的原因是 fixture 自己的文件（红绿配对）', () => {
    const r = run('dworp-reverse-red', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.status).toBe(1)
    for (const p of REAL_DELIVERABLES) expect(r.stderr).not.toContain(p)
    // 正向锚点：点名的是 fixture 自己那份路径
    expect(r.stderr).toContain(`❌ ${FIX_BAD_PATH}`)
  })
})

// =============================================================================
describe('C 自钉判据 —— 本面的覆盖边界靠的是活源的哪几个事实', () => {
  it('闸与它 import 的 scope 脚本在真仓库里都存在（路径一改本面当场红）', () => {
    expect(existsSync(resolve(REPO_ROOT, GATE))).toBe(true)
    for (const f of COPY_FILES) expect(existsSync(resolve(REPO_ROOT, f))).toBe(true)
  })

  it('🔴 真仓库跑得动 `git ls-files` —— 否则本面的绿会变成「那一支 fail closed 的绿」', () => {
    // 这是文件头那条覆盖边界的**凭据**，不是一句声称：`listDeliverables()` 上面已经真跑过了。
    expect(Array.isArray(REAL_DELIVERABLES)).toBe(true)
    expect(() => execFileSync('git', ['ls-files', '-z', '*.md'], { cwd: REPO_ROOT, env: gitCleanEnv() }))
      .not.toThrow()
  })

  it('🔴 harness 剥掉了继承的 `GIT_*` —— 否则本面在 pre-commit 里测的是真仓库，不是 fixture', () => {
    // 这是 2026-08-28 收进 harness 那条能力的**回归钉**（谁把它删掉，本条当场红）。
    // 判据：人为注入一个指向别处的 `GIT_DIR`，闸在 fixture 里的 `--all` 读数必须**不受影响**。
    const files: Record<string, string> = {}
    for (let i = 0; i < FIX_ALL_COUNT; i++) files[`docs/handoffs/dworp-env-${i}.md`] = FIX_FULL
    const root = makeRoot('dworp-envleak', { files, git: true })
    const saved = process.env.GIT_DIR
    try {
      process.env.GIT_DIR = join(REPO_ROOT, '.git')
      const r = runGate(root, GATE, ['--all'])
      expect(r.stdout).toContain(`交付物 ${FIX_ALL_COUNT} 份`)
      expect(r.stdout).not.toContain(`交付物 ${REAL_ALL_COUNT} 份`)
    } finally {
      if (saved === undefined) delete process.env.GIT_DIR
      else process.env.GIT_DIR = saved
    }
    // 致败探针：这条钉必须真的在比两个**不同**的数，否则上面两句可以同时满足而毫无意义
    expect(REAL_ALL_COUNT).not.toBe(FIX_ALL_COUNT)
  })

  it('🔴 现行行为：显式列表形态下没有分母 —— 四种情形逐字同形（⛔ 钉住，不是背书）', () => {
    // 文件头 (a) + (c) 的凭据。修法方向 = 印出分母 + 显式逃逸口，**本轮未实施**
    // （改闸是 [[INFRA-F138]] 逐字封的路）⇒ 报 owner。谁哪天补上了，本条会红。
    const noArg = run('dworp-denom-noarg', [])
    const okOne = run('dworp-denom-ok', [FIX_OK_PATH], { files: { [FIX_OK_PATH]: FIX_FULL } })
    const notDeliv = run('dworp-denom-nd', ['x.md'], { files: { 'x.md': FIX_NOT_DELIVERABLE } })
    const ghost = run('dworp-denom-ghost', ['ghost.md'])
    for (const r of [noArg, okOne, notDeliv, ghost]) {
      expect(r.status).toBe(0)
      expect(r.stderr).toBe('')
    }
    // 四条 stdout **逐字**相同 —— 这正是该登记的那件事
    expect(new Set([noArg.stdout, okOne.stdout, notDeliv.stdout, ghost.stdout]).size).toBe(1)
    expect(noArg.stdout).toContain(OK_LINE)
  })

  it('🔴 现行行为：`--all` + 显式列表 ⇒ 自印说「全仓扫描」而它没扫（⛔ 钉住，不是背书）', () => {
    // 文件头 (b) 的凭据。阴阳对照就在同一条用例里：同一份 fixture，
    // 走 `--all` 无显式时分母是 git 扫出来的 2，带显式时分母是显式列表的 1。
    const files = { [FIX_OK_PATH]: FIX_FULL, 'docs/handoffs/dworp-second.md': FIX_FULL }
    const withExplicit = run('dworp-all-explicit', ['--all', FIX_OK_PATH], { files, git: true })
    expect(withExplicit.status).toBe(0)
    expect(withExplicit.stdout).toContain('--all 全仓扫描，交付物 1 份')

    const scanned = run('dworp-all-scanned', ['--all'], { files, git: true })
    expect(scanned.status).toBe(0)
    expect(scanned.stdout).toContain('--all 全仓扫描，交付物 2 份')
  })

  it('fixture 的非交付物样本确实不进扫描面（否则 G 组的绿是假绿）', () => {
    // 它既无标记、路径也不在 docs/handoffs/ 下 —— 两个入口条件都不满足。
    expect(FIX_NOT_DELIVERABLE).not.toContain(MARKER)
    // 但它**含**判据短语 ⇒ 一旦 scope 过滤被摘掉，它会立刻变成一条真违例（G 组用它当探针）
    expect(FIX_NOT_DELIVERABLE).toContain('Reference probe')
  })
})

// =============================================================================
describe('D main() 独有的接线 —— 旧面结构上覆盖不到（它连 main 都没导入）', () => {
  it('有真违例时退出码必须是 1（摘掉 process.exit(1) 这一档由本条抓）', () => {
    const r = run('dworp-exit-wired', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.status).toBe(1)
  })

  it('缺失数自印，且与逐条明细的行数一致', () => {
    const r = run('dworp-count-matches', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.stderr).toContain('handoff 交付物缺 11 个强制段落：')
    const detailLines = r.stderr.split('\n').filter((l) => /^\s+\[[^\]]+\] 缺 "/.test(l))
    expect(detailLines).toHaveLength(11)
  })

  it('多份交付物各自独立点名（钉住 failed 是累加的、不是 early-return）', () => {
    const files = { [FIX_BAD_PATH]: FIX_EMPTY, 'docs/handoffs/dworp-bad2.md': FIX_EMPTY }
    const r = run('dworp-multi-red', ['--all'], { files, git: true })
    expect(r.status).toBe(1)
    expect(r.stderr).toContain(`❌ ${FIX_BAD_PATH}`)
    expect(r.stderr).toContain('❌ docs/handoffs/dworp-bad2.md')
    // 分母那行照常印完（判据红 ≠ 程序半途死掉）
    expect(r.stdout).toContain('交付物 2 份')
  })

  it('红档 HELP 段照印（钉住 `if (failed > 0)` 之后那一整块接线）', () => {
    const r = run('dworp-help', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.stderr).toContain('这几段是**规则文本自己指定**的可落盘产物')
    expect(r.stderr).toContain(HELP_LAST)
    // 而绿档专有的结论句**不该**出现
    expect(r.stderr + r.stdout).not.toContain(OK_LINE)
  })

  it('🔴 红档 stdout 零字节 —— 判据红全走 stderr（文件头那条「流向与 prop-naming 互为镜像」）', () => {
    const r = run('dworp-red-stdout-empty', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.status).toBe(1)
    expect(r.stdout).toBe('')
    // 正向锚点：它真的红了，不是没跑
    expect(r.stderr).toContain('handoff 交付物缺 11 个强制段落：')
  })
})

// =============================================================================
describe('E 两个挂载层各自的调用形态 —— 走的是两条不同的代码路径', () => {
  it('L4 形态（显式列表、无 `--all`）：不印分母行，直接判', () => {
    const r = run('dworp-l4-shape', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.status).toBe(1)
    expect(r.stdout).not.toContain('全仓扫描')
    expect(r.stderr).toContain(`❌ ${FIX_BAD_PATH}`)
  })

  it('L5 形态（`--all`）：先印分母行再判，两行都在', () => {
    const r = run('dworp-l5-shape', ['--all'], { files: { [FIX_BAD_PATH]: FIX_EMPTY }, git: true })
    expect(r.status).toBe(1)
    expect(r.stdout).toContain('--all 全仓扫描，交付物 1 份')
    expect(r.stderr).toContain(`❌ ${FIX_BAD_PATH}`)
  })

  it('非 `.md` 参数被参数解析丢弃（钉住 `argv.filter(f => f.endsWith(".md"))`）', () => {
    // 传一个 .txt：它不进 files ⇒ 即使内容缺段也不会红。
    const r = run('dworp-nonmd-arg', ['dworp.txt'], { files: { 'dworp.txt': FIX_EMPTY } })
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(OK_LINE)
  })

  it('`--all` 与显式列表两条路径的分母来源不同（同一棵树、同一份闸，两个数）', () => {
    const files = { [FIX_OK_PATH]: FIX_FULL, 'docs/handoffs/dworp-x.md': FIX_FULL }
    const viaGit = run('dworp-two-sources-git', ['--all'], { files, git: true })
    const viaArgs = run('dworp-two-sources-args', ['--all', FIX_OK_PATH], { files, git: true })
    expect(viaGit.stdout).toContain('交付物 2 份')
    expect(viaArgs.stdout).toContain('交付物 1 份')
  })
})

// =============================================================================
describe('F 四条 L1 规则的判据 —— 逐条经整条接线（旧面只到判据函数为止）', () => {
  it('全段缺失 → 每条各自点名，且带各自的规则出处', () => {
    const r = run('dworp-all-four', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expectGateRed(r, {
      checks: [
        '[M37] 缺 "Reference probe" 段',
        '[M42] 缺 "clone source probe table" 段',
        '[CRUD] 缺 "CRUD self-check" 段',
        '[PRD-EG] 缺 "PRD examples scoped" 段',
        '[CARDINALITY] 缺 "Cardinality self-check" 段',
        '[IA-FIRST] 缺 "IA 树" 段',
        '[M35] 缺 "Affordance:" 段',
        '[M21.1] 缺 "derive from" 段',
        '[I2] 缺 "sectionInternalOverlap" 段',
        '[M32] 缺 "Buttons used:" 段',
        '[M36-REUSE] 缺 "reused file-local components" 段',
      ],
    })
  })

  it('只缺一段 → 只点名那一段（不连坐）', () => {
    const md = FIX_FULL.replace('CRUD self-check', 'CRUD 自查')
    const r = run('dworp-one-missing', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: md } })
    expectGateRed(r, { checks: ['缺 1 个强制段落', '[CRUD] 缺 "CRUD self-check" 段'] })
    expect(r.stderr).not.toContain('[M37]')
    expect(r.stderr).not.toContain('[M42]')
  })

  it('M35 第二层：锚点在但缺 Vocabulary scan → 单独点名那一行', () => {
    const md = FIX_FULL.replace(/Vocabulary scan: .*\n/, '')
    const r = run('dworp-m35-vocab', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: md } })
    expectGateRed(r, { checks: ['[M35:Vocabulary scan:] 缺 "Vocabulary scan:" 段'] })
    expect(r.stderr).not.toContain('[M35] 缺')
  })

  it('M35 第二层：缺 Chosen → 点名 Chosen 那一行', () => {
    const md = FIX_FULL.replace(/Chosen: .*\n/, '')
    const r = run('dworp-m35-chosen', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: md } })
    expectGateRed(r, { checks: ['[M35:Chosen:] 缺 "Chosen:" 段'] })
  })

  it('M35 的 N/A 档整段豁免第二层 ⇒ 绿（规则自带的 skip 形态，经整条接线）', () => {
    const md = FIX_FULL.replace(FIX_TRACE, 'Affordance: N/A — 本轮未新增 affordance 类元素\n')
    const r = run('dworp-m35-na', [FIX_OK_PATH], { files: { [FIX_OK_PATH]: md } })
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(OK_LINE)
  })

  it('围栏里的段不算数 ⇒ 照样全红（stripFences 经整条接线）', () => {
    const md = `${MARKER}\n\`\`\`\nReference probe\nclone source probe table\nCRUD self-check\nPRD examples scoped\nCardinality self-check\nIA 树\nSelection model\n${FIX_TRACE}derive from x\nsectionInternalOverlap\nsectionToSectionOverlap\nButtons used:\nreused file-local components\n\`\`\`\n`
    const r = run('dworp-fenced', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: md } })
    expectGateRed(r, { checks: ['缺 11 个强制段落'] })
    // 阴性对照：同样的文本拿掉围栏就是绿的 ⇒ 差别来自 stripFences 而非拼写
    const green = run('dworp-unfenced', [FIX_OK_PATH], {
      files: { [FIX_OK_PATH]: md.replace(/```/g, '') },
    })
    expect(green.status).toBe(0)
  })
})

// =============================================================================
describe('G 扫描面过滤（isDeliverable）—— 经整条接线，不是直接调函数', () => {
  it('无标记、非 handoffs 路径的 .md 不进判据（即使含判据短语）', () => {
    const r = run('dworp-scope-skip', ['x.md'], { files: { 'x.md': FIX_NOT_DELIVERABLE } })
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(OK_LINE)
    expect(r.stderr).toBe('')
  })

  it('docs/handoffs/ 路径入 scope（无标记也算）', () => {
    const r = run('dworp-scope-path', ['docs/handoffs/dworp-nomarker.md'], {
      files: { 'docs/handoffs/dworp-nomarker.md': '# dworp 无标记\n' },
    })
    expectGateRed(r, { checks: ['❌ docs/handoffs/dworp-nomarker.md', '缺 11 个强制段落'] })
  })

  it('标记必须独立成行（钉住扫描面那条正则，不是「文中出现即可」）', () => {
    const inline = `# dworp\n前面有字 ${MARKER} 后面有字\nReference probe\n`
    const r = run('dworp-scope-inline', ['x.md'], { files: { 'x.md': inline } })
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(OK_LINE)
    // 阴性对照：同一份内容把标记单独成行 ⇒ 立刻进扫描面并红
    const own = `# dworp\n${MARKER}\nReference probe\n`
    const red = run('dworp-scope-ownline', ['x.md'], { files: { 'x.md': own } })
    expect(red.status).toBe(1)
  })
})

// =============================================================================
describe('H `--all` 的 git 依赖与 fail-closed —— 量具 P2 判为 PASS 的那一格其实带 git', () => {
  // ⚠️ 顺带一条给量具的如实登记：本闸自身源码里没有 git/net/cred/proc 的痕迹（所以 P2=PASS），
  //    而 git 依赖藏在它 import 的 scope 脚本里 ⇒ 这是量具**诚实边界 ③「词法分类会低报」**
  //    的一个活实例。⛔ 不改量具（那是另一件事），如实记在这里。
  it('`--all` 命中 0 份 ⇒ fail closed，且说出「判定面塌了」而不是「干净」', () => {
    const r = run('dworp-all-zero', ['--all'], { files: { 'plain.md': '# dworp 不是交付物\n' }, git: true })
    expectGateRed(r, { checks: ['判定面塌了，不是「干净」'] })
    // 分母那行照印 0 —— 人眼与机械各有一条线索
    expect(r.stdout).toContain('交付物 0 份')
  })

  it('`--all` 在非 git 仓库里 ⇒ 抛错（闸头注释声明的 fail closed ①，形态是崩溃）', () => {
    const r = run('dworp-all-nogit', ['--all'], { files: { [FIX_OK_PATH]: FIX_FULL } })
    expect(r.status).toBe(1)
    // ⛔ 关键：这条走的是**崩溃**不是判据 ⇒ 不能用 expectGateRed 的 checks 当凭据
    //    （harness 判据 2 逐字：崩溃也是非零，只看退出码的红是假红）。
    //    区分靠终态事实：崩溃时 stdout 零字节、stderr 末行是 node 的崩溃栈尾。
    expect(r.stdout).toBe('')
    expect(lastLine(r.stderr)).toMatch(/^Node\.js v/)
  })

  it('`--all` 绿档确实走了 git 扫描（阴阳对照：同样的文件不 add 进 index 就扫不到）', () => {
    const files = { [FIX_OK_PATH]: FIX_FULL }
    const added = run('dworp-all-added', ['--all'], { files, git: true })
    expect(added.stdout).toContain('交付物 1 份')

    // 同一份 fixture，只 init 不 add ⇒ index 为空 ⇒ 命中 0 ⇒ fail closed
    const root = makeRoot('dworp-all-unadded', { files })
    execFileSync('git', ['init', '-q'], { cwd: root, env: gitCleanEnv() })
    const unadded = runGate(root, GATE, ['--all'])
    expect(unadded.status).toBe(1)
    expect(unadded.stdout).toContain('交付物 0 份')
  })

  it('scope 脚本是**拷**进 fixture 的，ENOENT 路径不指向真仓库（无 fallback 凭据）', () => {
    // 直接删掉拷进去的那份 ⇒ 闸 import 不到 ⇒ ERR_MODULE_NOT_FOUND，且路径落在 fixture 里。
    const root = createGateFixture({ gate: GATE, prefix: 'dworp-no-scope', linkDirs: [] })
    const r = runGate(root, GATE, [FIX_OK_PATH])
    expect(r.status).toBe(1)
    expect(r.stderr).toContain('ERR_MODULE_NOT_FOUND')
    expect(r.stderr).toContain(root)
    for (const f of COPY_FILES) expect(r.stderr).not.toContain(join(REPO_ROOT, f))
    expect(r.stdout).toBe('')
  })
})

// =============================================================================
describe('J 随包镜像 —— 文件头 ⑤ 那个 entry 没算的因子（如实登记，⛔ 本面不动镜像）', () => {
  const MIRROR = 'templates/consumer-product/.githooks/audit-handoff-deliverable-sections.mjs'
  const MIRROR_HOOK = 'templates/consumer-product/.githooks/pre-commit'

  it('镜像存在，且 `templates` 在 package.json 的 files[] 里 ⇒ 它随包发', () => {
    expect(existsSync(resolve(REPO_ROOT, MIRROR))).toBe(true)
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf-8')) as { files: string[] }
    expect(pkg.files).toContain('templates')
  })

  it('镜像接在模板自己的 pre-commit 上（⇒ consumer 侧真的会跑它）', () => {
    const hook = readFileSync(resolve(REPO_ROOT, MIRROR_HOOK), 'utf-8')
    expect(hook).toContain('audit-handoff-deliverable-sections.mjs')
  })

  it('🔴 棘轮的随包面看不见它 —— 判定按闸自身路径匹配 files[]，而随包的是副本', () => {
    // 这是文件头 ⑤ 的凭据：`files[]` 里没有任何一项能覆盖 `scripts/audit-handoff-…`，
    // 所以它被判为「非随包」；而上面两条已证明它实质随包。⛔ 不改棘轮口径（另一件事）。
    const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf-8')) as { files: string[] }
    const covered = pkg.files.some((f) => {
      const re = new RegExp('^' + f.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*') + '(/|$)')
      return re.test(GATE)
    })
    expect(covered).toBe(false)
    // 正向对照：同一段匹配逻辑对镜像路径是命中的 ⇒ 上面那个 false 不是逻辑写坏了
    const mirrorCovered = pkg.files.some((f) => {
      const re = new RegExp('^' + f.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*') + '(/|$)')
      return re.test(MIRROR)
    })
    expect(mirrorCovered).toBe(true)
  })

  it('🔴 镜像 parity 那份测试只按判定函数钉，两边的**接线**都没人看 —— 本面只覆盖 DS 侧', () => {
    // 如实登记的边界：本面 spawn 的是 DS 侧那份。镜像侧同型的接线仍零覆盖。
    const parity = readFileSync(resolve(REPO_ROOT, 'tests/consumer-hook-mirror-parity.test.ts'), 'utf-8')
    expect(parity).toContain('findMissingSections')
    // 它不 spawn 任何一侧的 CLI（⇒ 接线在它的视野外）
    expect(parity).not.toMatch(/spawnSync|execFileSync\(\s*['"]node['"]/)
  })
})

// =============================================================================
describe('K 终态事实 —— 按流分别钉（本闸与 prop-naming 的流向互为镜像）', () => {
  // 判据 = **闸自己的最后一句话必须是对应那条流的最后一行**。
  // 崩溃同样是非零退出码，只断言 status 的红是假红（harness 头注释判据 2 逐字预言过）。
  it('绿档：stdout 末行恰好是收尾句，stderr 空', () => {
    const r = run('dworp-terminal-green', [FIX_OK_PATH], { files: { [FIX_OK_PATH]: FIX_FULL } })
    expect(lastLine(r.stdout)).toBe(OK_LINE)
    expect(r.stderr).toBe('')
  })

  it('红档：**stderr** 末行是闸自己的 HELP 末句（不是崩溃栈），stdout 空', () => {
    const r = run('dworp-terminal-red', [FIX_BAD_PATH], { files: { [FIX_BAD_PATH]: FIX_EMPTY } })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toBe(HELP_LAST)
    expect(lastLine(r.stderr)).not.toMatch(/^Node\.js v/)
    expect(r.stdout).toBe('')
  })

  it('fail-closed 档：stderr 末行是闸自己那句提示，且 stdout 仍印了分母（与崩溃档可区分）', () => {
    const r = run('dworp-terminal-failclosed', ['--all'], { files: { 'plain.md': '# dworp\n' }, git: true })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toContain('_handoff.template.md')
    expect(r.stdout).toContain('交付物 0 份')
  })

  it('崩溃档：末行落在 stderr 且是崩溃栈尾 ⇒ 上三条的区分力是真的（阴性对照）', () => {
    const r = run('dworp-terminal-crash', ['--all'], { files: { [FIX_OK_PATH]: FIX_FULL } })
    expect(r.status).toBe(1)
    expect(r.stdout).toBe('')
    expect(lastLine(r.stderr)).toMatch(/^Node\.js v/)
  })
})
