// [[INFRA-F138]] —— `scripts/audit-doc-shape.mjs` 的**整脚本**回归面。
// -----------------------------------------------------------------------------
// 与 `tests/audit-doc-shape.test.ts` 的分工（⛔ 别合并，两者测的不是一回事）：
//   · 那一份 `import { evaluate, sliceStatus, findCompletedEntries }` 直接调判据函数
//     ⇒ 覆盖**判据逻辑**，看不见 `main()` 的接线。
//   · 本份 **spawn 整个脚本**（共享 harness `tests/lib/gate-fixture-root.ts`）⇒ 覆盖
//     入口守卫 / 读文件 / 判据 → 输出 → 退出码的**整条接线**。被测闸一行不改。
//
// 🔴 **这条闸就是 [[INFRA-F138]] 的立项实证对象本身。** entry 逐字记着：拿它做两次单侧
//   注入 —— 改判据逻辑 ⇒ 旧面 `1 failed / 19 passed`（判据逻辑有覆盖）；**摘接线**
//   （`main()` 把 `failures` 吞掉）⇒ 闸对真违例（顶部摘要区 3496 B > 上限 3000）改印
//   `✓ PASS` 且 exit 0，而**全量** vitest `1993 passed / EXIT=0`。`main` 不在旧面的
//   导入列表、全文 `main(` 命中 0 ⇒ **结构性的，不是漏写用例**。
//   ⇒ 立项那天被用来证明缺口的那条闸，此后 8 轮一直**还是**零接线覆盖。本文件补上它。
//
// 为什么选它（爆炸半径，⛔ 不是「挂载最深」也不是「最便宜」—— 它只 2 层：
//   L4 `.husky/pre-commit`（staged 命中 STATUS / backlog / 本脚本时）+ L5 `gate-chain`）：
//   它守的是 `docs/STATUS.md` 的**形态契约**，而 STATUS 是 onboarding L-core 的 #1。
//   假绿 ⇒ 顶部摘要区与 Active 段无上限膨胀 ⇒ L-core 体量失控 ⇒ **onboarding gate 被
//   默读架空**。这个代价**已经真实付过一次**（原 INFRA-F95：2026-07-15 起 3 周内 4 次
//   手动精简、最后一次删 50 316 B，全部在 2-4 天内被填平，术后增速反而 ×1.7~×2.2；
//   两处 onboarding 预算写着「~40KB」而实测已 90.5 KB，**失真 2.2×**）。
//   而它的假绿在本仓**没有第二道机械保障** —— `WRAP-UP.md:13/:20` 与 STATUS §SoT 归属表
//   那三条原本就是纯 L1、无人执行，本闸的全部存在意义就是把它们升到 L4+L5。
//
// 🔴 **本轮实测：harness 的 `realpathSync` 对本闸是承重的。** 本闸用的是**原始**入口守卫
//   （`resolve(argv[1]) === resolve(fileURLToPath(import.meta.url))`），属 [[INFRA-F138]]
//   记的「14 条非随包闸仍用原始守卫」那一档（⛔ 那不是缺陷 —— 它们不随包、只从仓库根跑）。
//   独立探针实测：fixture root 不 realpath ⇒ **全部场景 `EXIT=0` + 双流零字节**。
//
// ⛔ **本面覆盖不到豁免那三支**（`S1`/`S2` 的「豁免在位」「上涨即红」与 `EXEMPT-STALE`）：
//   `EXEMPTIONS` 是模块顶层 const 且**现在是空表**（闸自己的注释逐字写「表空着是终态，
//   不是待办」），CLI 走 `evaluate({ exemptions: EXEMPTIONS })` ⇒ 不改闸就造不出非空表，
//   而改闸是 [[INFRA-F138]] 逐字封的路。那三支的判据逻辑由旧面（传 `exemptions` 参数）覆盖。
//   **如实登记的边界，不是待补 TODO** —— 与前几轮的 `STRUCTURAL_L4_ONLY` /
//   `KNOWN_COLLISIONS` / `EXEMPTIONS` 同型。下方 C 组那条自钉判据是这条边界的**凭据**。
//
// ⚠️ **S4 是一个 report-only 的 fail-open 出口**（`try { … } catch { /* 不阻塞 */ }`）：
//   backlog 读不到时，`S4 体量` 那一行**整段消失**、其余照常、退出码仍 0，**没有任何
//   警告**。这与 `exports-types-contract` 的 S3（缺产物时大声印 `S3 部分 SKIPPED`）方向
//   相反。⛔ 本文件 I 组**钉住现行行为**，不是背书 —— 但它是闸自己注释里写明的 report-only
//   设计意图（S4 不参与任何阻塞判据），**因此本轮不报为缺陷**，只把这条分支纳入覆盖。
//
// ⛔ 污染面纪律（[[INFRA-F138]] 逐字）：fixture 假名统一用 `Nrix` / `nrix`（落地前实测
//   全仓 0 文件命中，含 30 个历轮 worktree）；散文与断言里提到别的闸一律**去 `.mjs`
//   后缀、去 `audit:` 冒号**，否则量具会把那条闸误报成「已覆盖」（高报）或挪档（低报）。
//   本闸 stdout 只引用它自己（`维护：scripts/audit-doc-shape.mjs`）⇒ 不需要「断言截短」那一招。
//
// ⚠️ **`TITLE_WINDOW`（120 个**字符**）不是导出符号** ⇒ 本面钉不到它的精确边界，
//   fixture 只用「远在窗口内 / 远在窗口外」两侧留足余量。精确边界由旧面覆盖。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
  REPO_ROOT,
  type GateRun,
} from './lib/gate-fixture-root'
import {
  STATUS_FILE,
  BACKLOG_FILE,
  TOP_MAX,
  ACTIVE_LINE_MAX,
  EXEMPTIONS,
  REOPEN_BACKLOG_BYTES,
  REOPEN_NOTE,
  bytes,
  sliceStatus,
} from '../scripts/audit-doc-shape.mjs'

const GATE = 'scripts/audit-doc-shape.mjs'

// ── fixture 自己的读数：**纯 ASCII 拼出来的设计值**，⛔ 不用被测代码算 ──────────────
//    （用 `sliceStatus()` 反算期望值 = 拿被测对象给自己出题，判据坏掉时两边一起坏。）
const FIX_S1 = 1024 // 输入面①：STATUS 顶部区字节
const FIX_S2_A = 128 // 输入面②：Active fence 第一行字节
const FIX_S2_B = 96 //  输入面②：Active fence 第二行字节
const FIX_BACKLOG = 512 // 输入面③：**另一份文件**（backlog）的整文件字节

const LABEL_A = 'Nrix alpha'
const LABEL_B = 'Nrix beta'

/** 纯 ASCII ⇒ 字节数 = 字符数，于是每个读数都是能手算的设计值。 */
function pad(target: number, head: string): string {
  const n = target - head.length
  if (n < 0) throw new Error(`pad: head 已 ${head.length} B，超过目标 ${target} B`)
  return head + 'n'.repeat(n)
}

/** 顶部区 = 首个 `> ### ` 之前的全部内容。`join('\n')` 会原样重建，故字节数恰为 target。 */
const topOf = (target: number) => pad(target, '# Nrix Status\n\n> **Last updated**: nrix one-line summary.\n> ')
const activeOf = (label: string, count: number, target: number) => pad(target, `${label}  (${count}):  `)

const OPEN_HEAD = '> ### 一 · nrix 待办清单'
const OPEN_ENTRIES = ['> 1. nrix open item one', '> 2. nrix open item two']

type DocParts = {
  top?: string
  open?: string[] | null
  hr?: boolean
  activeHeading?: boolean
  fence?: string[] | null
}

function statusDoc(p: DocParts = {}): string {
  const {
    top = topOf(FIX_S1),
    open = OPEN_ENTRIES,
    hr = true,
    activeHeading = true,
    fence = [activeOf(LABEL_A, 2, FIX_S2_A), activeOf(LABEL_B, 1, FIX_S2_B)],
  } = p
  const parts: string[] = []
  if (top !== '') parts.push(top)
  if (open !== null) parts.push(OPEN_HEAD, ...open)
  if (hr) parts.push('', '---', '')
  if (activeHeading) parts.push('## Active 后续工作', '')
  if (fence !== null) parts.push('```', ...fence, '```')
  parts.push('')
  return parts.join('\n')
}

const BACKLOG_DOC = pad(FIX_BACKLOG, '# Nrix Backlog\n\n### NRIX-1: nrix entry\n\n')

const HEALTHY: Record<string, string> = {
  [STATUS_FILE]: statusDoc(),
  [BACKLOG_FILE]: BACKLOG_DOC,
}

/** 建 fixture + 跑整条闸。`overrides` 覆盖 / 新增 fixture 文件，`drop` 删掉某份。 */
function run(
  prefix: string,
  overrides: Record<string, string> = {},
  opts: { drop?: string[]; args?: string[] } = {},
): GateRun {
  const files: Record<string, string> = { ...HEALTHY, ...overrides }
  for (const d of opts.drop ?? []) delete files[d]
  const root = createGateFixture({
    gate: GATE,
    prefix,
    files,
    // ⛔ 显式关掉 harness 默认的 `scripts/lib` 软链 —— 本闸只 import node 内建，不碰它。
    linkDirs: [],
  })
  return runGate(root, GATE, opts.args ?? [])
}

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

// ── 真仓库读数：⛔ **不写死**，当场从活源算，否则真仓库一改反向钉就静默变空 ──────────
const realStatusText = readFileSync(resolve(REPO_ROOT, STATUS_FILE), 'utf-8')
const realBacklogText = readFileSync(resolve(REPO_ROOT, BACKLOG_FILE), 'utf-8')
const realSlices = sliceStatus(realStatusText)
const REAL_S1 = bytes(realSlices.top)
const REAL_BACKLOG_BYTES = bytes(realBacklogText)
const REAL_LABELS: string[] = realSlices.activeLines.map((l: { label: string }) => l.label)

afterAll(cleanupGateFixtures)

// =============================================================================
describe('A 绿档 —— 非空过凭据取自三个不同输入面', () => {
  it('健康 fixture 绿，且自印的三个读数都是 fixture 自己的（三个输入面各出一个）', () => {
    const r = run('nrix-healthy')
    expectGateGreen(r, {
      contains: [
        // 输入面①：STATUS **顶部区**（首个 `> ### ` 之前）的字节
        `S1 顶部摘要区 : ${FIX_S1} B（上限 ${TOP_MAX}）`,
        // 输入面②：同一份文件的**另一个结构区**（Active fence），label 是 fixture 独有字符串
        `S2 "${LABEL_A}" : ${FIX_S2_A} B（上限 ${ACTIVE_LINE_MAX}）`,
        `S2 "${LABEL_B}" : ${FIX_S2_B} B（上限 ${ACTIVE_LINE_MAX}）`,
        // 输入面③：**另一份文件**（backlog）的整文件字节
        `backlog ${FIX_BACKLOG} B`,
      ],
    })
  })

  it('绿档是真绿：stderr 零字节、stdout 无任何失败标记（常驻负向对照）', () => {
    const r = run('nrix-healthy-neg')
    expect(r.status).toBe(0)
    expect(r.stderr).toBe('')
    expect(r.stdout).not.toContain('❌')
    expect(r.stdout).not.toContain('FAIL')
    // must-not-hit 的正向锚点：闸确实跑到了最后一句
    expect(r.stdout).toContain('✓ audit:doc-shape PASS')
  })

  it('覆盖面自陈照常印出（含它自己那条诚实边界：本闸不守 backlog 体量）', () => {
    const r = run('nrix-healthy-scope')
    expect(r.stdout).toContain('别据此宣称「文档体量已受控」')
    expect(r.stdout).toContain('无任何阻塞判据 —— 本闸不守 backlog 体量')
  })

  it('绿档的 S3 计数是 0/0（⇒ 下方 H 组的红不是「本来就红」）', () => {
    const r = run('nrix-healthy-s3')
    expect(r.stdout).toContain('S3 已完成条目 : S3a 0 条 · S3b 0 条')
  })
})

// =============================================================================
describe('B 反向钉 —— 绿档没有退回真仓库读', () => {
  it('反向钉本身是有意义的（fail-closed 自证：fixture 的数 ≠ 真仓库的数）', () => {
    // ⛔ 若哪天真仓库的这几个量恰好等于 fixture 的设计值，下面那条反向钉会静默失去意义
    //    ⇒ 本条先把前提钉住。
    expect(REAL_S1).not.toBe(FIX_S1)
    expect(REAL_BACKLOG_BYTES).not.toBe(FIX_BACKLOG)
    expect(REAL_LABELS).not.toContain(LABEL_A)
    expect(REAL_LABELS).not.toContain(LABEL_B)
    // 分母非空：三个真值都得真的量到了东西
    expect(REAL_S1).toBeGreaterThan(0)
    expect(REAL_BACKLOG_BYTES).toBeGreaterThan(0)
    expect(REAL_LABELS.length).toBeGreaterThan(0)
  })

  it('stdout 不含真仓库的 S1 / backlog 读数，也不含真仓库任何一个 Active label', () => {
    const r = run('nrix-reverse')
    expect(r.stdout).not.toContain(`S1 顶部摘要区 : ${REAL_S1} B`)
    expect(r.stdout).not.toContain(`backlog ${REAL_BACKLOG_BYTES} B`)
    for (const label of REAL_LABELS) expect(r.stdout).not.toContain(`S2 "${label}"`)
    // 正向锚点（must-not-hit 不许空过）：fixture 自己那三个值确实印出来了
    expect(r.stdout).toContain(`S1 顶部摘要区 : ${FIX_S1} B`)
    expect(r.stdout).toContain(`backlog ${FIX_BACKLOG} B`)
    expect(r.stdout).toContain(`S2 "${LABEL_A}"`)
  })
})

// =============================================================================
describe('C 自钉判据 —— 本面的覆盖边界靠的是活源的哪几个事实', () => {
  it('真豁免表是空的（空是终态）⇒ CLI 面结构上造不出豁免三支', () => {
    // 这是文件头那条「覆盖不到豁免三支」的**凭据**，而不是一句声称。
    // 谁哪天往表里加了行，本条会当场红并把人指到文件头那段。
    expect(EXEMPTIONS).toEqual([])
  })

  it('自印的豁免条数与真表长度一致（绿档路径上）', () => {
    const r = run('nrix-exempt-count')
    expect(r.stdout).toContain(`豁免        : ${EXEMPTIONS.length} 条`)
  })

  it('两个阈值是正整数，且 fixture 的设计值确实落在它们两侧（自钉设计前提）', () => {
    expect(Number.isInteger(TOP_MAX)).toBe(true)
    expect(Number.isInteger(ACTIVE_LINE_MAX)).toBe(true)
    // 绿档三个设计值必须在限内，否则 A 组的绿是「阈值太松」而不是「fixture 合规」
    expect(FIX_S1).toBeLessThan(TOP_MAX)
    expect(FIX_S2_A).toBeLessThan(ACTIVE_LINE_MAX)
    expect(FIX_S2_B).toBeLessThan(ACTIVE_LINE_MAX)
  })
})

// =============================================================================
describe('D 空 / 畸形输入 fail closed', () => {
  it('传了任何参数 → 红并回显收到的参数（`pnpm run x -- --foo` 的透传口）', () => {
    const r = run('nrix-argv', {}, { args: ['--nrix-flag'] })
    expectGateRed(r, { checks: ['不接受任何参数', '--nrix-flag'] })
    // 参数分支在自印之前就 exit ⇒ stdout 必须是空的（钉住它真的早退了）
    expect(r.stdout).toBe('')
  })

  it('连 STATUS 都读不到 → 红且点名读不到，ENOENT 路径指向 fixture、⛔ 不是本仓', () => {
    const r = run('nrix-nostatus', {}, { drop: [STATUS_FILE] })
    expectGateRed(r, { checks: [`读不到 ${STATUS_FILE}`, 'ENOENT'] })
    // 无 fallback 回真仓库的凭据
    expect(r.stderr).toContain('nrix-nostatus')
    expect(r.stderr).not.toContain(REPO_ROOT)
  })
})

// =============================================================================
describe('E SLICE fail closed —— 切不出受管段 / 段为空，一律不当 PASS', () => {
  it('没有 `> ### ` 开清单标题 → SLICE 红（顶部切不出来）', () => {
    const r = run('nrix-slice-nohead', { [STATUS_FILE]: statusDoc({ open: null }) })
    expectGateRed(r, { checks: ['SLICE', '切不出受管段'] })
  })

  it('有开清单标题但其后没有 `---` → SLICE 红（open 清单区切不出来）', () => {
    const r = run('nrix-slice-nohr', { [STATUS_FILE]: statusDoc({ hr: false }) })
    expectGateRed(r, { checks: ['SLICE', '切不出受管段'] })
  })

  it('没有 `## Active 后续工作` 段 → SLICE 红', () => {
    const r = run('nrix-slice-noactive', { [STATUS_FILE]: statusDoc({ activeHeading: false }) })
    expectGateRed(r, { checks: ['SLICE', '切不出受管段'] })
  })

  it('Active fence 在但里面全是空行 → SLICE 红（「整体删掉不等于合规」）', () => {
    const r = run('nrix-slice-emptyfence', { [STATUS_FILE]: statusDoc({ fence: ['', '   '] }) })
    expectGateRed(r, { checks: ['SLICE', '受管段为空'] })
  })

  it('顶部区 0 字节（文件第一行就是开清单标题）→ SLICE 红', () => {
    const r = run('nrix-slice-emptytop', { [STATUS_FILE]: statusDoc({ top: '' }) })
    expectGateRed(r, { checks: ['SLICE', '受管段为空'] })
  })

  it('🔴 SLICE 红时 `report` 为 null ⇒ S1/S2/S3 三行不印，但覆盖面与 S4 照常（阴阳对照）', () => {
    const r = run('nrix-slice-noreport', { [STATUS_FILE]: statusDoc({ open: null }) })
    expect(r.stdout).not.toContain('S1 顶部摘要区')
    expect(r.stdout).not.toContain('S3 已完成条目')
    // 正向锚点：闸确实跑完了自印（不是压根没跑 / 半途崩掉）
    expect(r.stdout).toContain(`backlog ${FIX_BACKLOG} B`)
    expect(r.stdout).toContain(`豁免        : ${EXEMPTIONS.length} 条`)
  })
})

// =============================================================================
describe('F S1 顶部摘要区 —— L-core #1 体量失控的那条闸', () => {
  it('顶部超上限 1 B → 红、报出超出量、并说出该搬去哪', () => {
    const r = run('nrix-s1-over', { [STATUS_FILE]: statusDoc({ top: topOf(TOP_MAX + 1) }) })
    expectGateRed(r, {
      marker: '❌ audit:doc-shape FAIL',
      checks: [
        `[S1] 顶部摘要区 ${TOP_MAX + 1} B > 上限 ${TOP_MAX} B（超 1 B）`,
        '当日一条摘要 + 指针',
        'docs/internal/STATUS-CHANGELOG.md 顶部',
      ],
    })
  })

  it('🔴 阴性对照：顶部恰好 = 上限 → 绿（判据是 `>` 不是 `>=`）', () => {
    const r = run('nrix-s1-exact', { [STATUS_FILE]: statusDoc({ top: topOf(TOP_MAX) }) })
    expectGateGreen(r, { contains: [`S1 顶部摘要区 : ${TOP_MAX} B（上限 ${TOP_MAX}）`] })
  })
})

// =============================================================================
describe('G S2 Active fence —— 逐行判，命中「增量长在既有行内部」那个机械成因', () => {
  it('某一行超上限 → 红、点名那一行的 label、并说出规定形态是计数镜像', () => {
    const r = run('nrix-s2-over', {
      [STATUS_FILE]: statusDoc({
        fence: [activeOf(LABEL_A, 2, ACTIVE_LINE_MAX + 1), activeOf(LABEL_B, 1, FIX_S2_B)],
      }),
    })
    expectGateRed(r, {
      checks: [
        `[S2] Active fence 行 "${LABEL_A}" ${ACTIVE_LINE_MAX + 1} B > 上限 ${ACTIVE_LINE_MAX} B`,
        '计数镜像 = ID 列表 + 数字',
        'docs/internal/backlog.md 各自 entry',
      ],
    })
  })

  it('🔴 阴阳对照：只有超限那一行被点名，合规的那一行不许被牵连', () => {
    const r = run('nrix-s2-onlyone', {
      [STATUS_FILE]: statusDoc({
        fence: [activeOf(LABEL_A, 2, ACTIVE_LINE_MAX + 1), activeOf(LABEL_B, 1, FIX_S2_B)],
      }),
    })
    expect(r.stderr).not.toContain(`Active fence 行 "${LABEL_B}"`)
    // must-not-hit 的正向锚点：合规那行确实在分母里（自印照常量到了它）
    expect(r.stdout).toContain(`S2 "${LABEL_B}" : ${FIX_S2_B} B`)
    expect(r.stderr).toContain(`Active fence 行 "${LABEL_A}"`)
  })

  it('🔴 阴性对照：某行恰好 = 上限 → 绿', () => {
    const r = run('nrix-s2-exact', {
      [STATUS_FILE]: statusDoc({
        fence: [activeOf(LABEL_A, 2, ACTIVE_LINE_MAX), activeOf(LABEL_B, 1, FIX_S2_B)],
      }),
    })
    expectGateGreen(r, { contains: [`S2 "${LABEL_A}" : ${ACTIVE_LINE_MAX} B`] })
  })
})

// =============================================================================
describe('H S3 完成叙述 —— open 清单区不许留已完成条目', () => {
  it('S3a：条目标题被整条划掉 → 红、报条数、并带 owner 的判据原文', () => {
    const r = run('nrix-s3a', {
      [STATUS_FILE]: statusDoc({ open: ['> 1. ~~nrix finished item~~ —— 收口', '> 2. nrix open item two'] }),
    })
    expectGateRed(r, {
      checks: [
        '[S3a] open 清单区有 1 条已完成条目仍挂着',
        '不保留已完成的任务，加 ✅ 留着也不算合规',
      ],
    })
    // 自印那格也要跟着动（判据与读数是同一件事）
    expect(r.stdout).toContain('S3 已完成条目 : S3a 1 条 · S3b 0 条')
  })

  it('S3b：标题窗口内有 ✅ 但未删除 → 红并回显那一条', () => {
    const r = run('nrix-s3b', {
      [STATUS_FILE]: statusDoc({ open: ['> 1. nrix open item one', '> 2. nrix wrapped marker ✅'] }),
    })
    expectGateRed(r, { checks: ['[S3b] 条目标题段标了 ✅ 但未删除', 'nrix wrapped marker'] })
    expect(r.stdout).toContain('S3 已完成条目 : S3a 0 条 · S3b 1 条')
  })

  it('🔴 阴性对照：✅ 远在标题窗口之外 → 绿（判据只看标题段）', () => {
    // TITLE_WINDOW = 120 个**字符**且不是导出符号 ⇒ 这里留足余量（前缀 ≈ 240 字符）。
    const line = '> 2. ' + 'nrix filler '.repeat(20) + '✅'
    const r = run('nrix-s3b-outside', { [STATUS_FILE]: statusDoc({ open: ['> 1. nrix open item one', line] }) })
    expectGateGreen(r, { contains: ['S3 已完成条目 : S3a 0 条 · S3b 0 条'] })
  })

  it('🔴 阴性对照：`~~` 只出现在条目**内部**（标题未划）→ 绿', () => {
    // 设计期实测：初版判据「整行含 ~~ 或 ✅」在真仓库 9 命中里 4 条是误报，正是这一形态。
    const line = '> 2. nrix still open —— ' + 'nrix body '.repeat(20) + '~~nrix inner sub~~ 已闭合'
    const r = run('nrix-s3a-inner', { [STATUS_FILE]: statusDoc({ open: ['> 1. nrix open item one', line] }) })
    expectGateGreen(r, { contains: ['S3 已完成条目 : S3a 0 条 · S3b 0 条'] })
  })
})

// =============================================================================
describe('I S4 report-only 的 fail-open 出口（⛔ 钉住现行行为，见文件头）', () => {
  it('backlog 读不到 → 仍绿，且 `S4 体量` 那一行整段消失、**没有任何警告**', () => {
    const r = run('nrix-s4-nobacklog', {}, { drop: [BACKLOG_FILE] })
    expect(r.status).toBe(0)
    expect(r.stdout).not.toContain('S4 体量')
    expect(r.stdout).not.toContain('SKIP')
    // 正向锚点：其余自印与结论照常（⇒ 消失的确实只有 S4 那一行）
    expect(r.stdout).toContain(`S1 顶部摘要区 : ${FIX_S1} B`)
    expect(r.stdout).toContain(`豁免        : ${EXEMPTIONS.length} 条`)
    expect(r.stdout).toContain('✓ audit:doc-shape PASS')
  })

  it('backlog 在场时 S4 两个数都印，且 STATUS 那个数 ≠ backlog 那个数（钉住两个来源）', () => {
    const r = run('nrix-s4-present')
    expect(r.stdout).toMatch(/S4 体量（report-only）: STATUS \d+ B · backlog \d+ B/)
    expect(r.stdout).toContain(`backlog ${FIX_BACKLOG} B`)
    expect(r.stdout).not.toContain(`STATUS ${FIX_BACKLOG} B`)
  })
})

// =============================================================================
// K 路线 B 重开条件的**承载物**（2026-09-11 到期复核落地，判据与读数见闸头注释 📌 段）
//   它换掉的那条是**日期型**重开条件 —— 到期后整整 10 天没有任何机制提醒过任何人。
//   ⇒ 本组钉的不是「印了一行」，是 **两臂有区分力** + **读不到时不许撒谎** +
//     **阈值与它的说明文案同 commit 改**。
//   ⚠️ 落地时已在真仓库上跑过两臂（阈值临时降到 100000 ⇒ 印「已达成」；`cp` 还原后
//     输出与对照臂逐字节相同）；本组把那两臂钉成回归面。
// =============================================================================
describe('K 路线 B 重开条件（report-only ⇒ ⛔ 不参与退出码）', () => {
  it('backlog 在阈值内 → 印「未达成」+ 余量', () => {
    const r = run('nrix-reopen-under')
    expect(r.status).toBe(0)
    expect(r.stdout).toContain(
      `路线 B 重开条件: 未达成（backlog ${FIX_BACKLOG} B ≤ ${REOPEN_BACKLOG_BYTES} B，余量 ${REOPEN_BACKLOG_BYTES - FIX_BACKLOG} B）`,
    )
    expect(r.stdout).not.toContain('已达成')
  })

  it('🔴 故障臂：backlog 超过阈值 → 印「已达成」，**但退出码仍 0**（它 ⛔ 不是阻塞判据）', () => {
    const over = 'n'.repeat(REOPEN_BACKLOG_BYTES + 1) // 纯 ASCII ⇒ 字节数 = 字符数
    const r = run('nrix-reopen-over', { [BACKLOG_FILE]: over })
    expect(r.status).toBe(0) // ⛔ 别改成 1 —— 复核结论逐字是「⛔ 不上闸」
    expect(r.stdout).toContain('路线 B 重开条件**已达成**')
    expect(r.stdout).toContain(`backlog ${REOPEN_BACKLOG_BYTES + 1} B > ${REOPEN_BACKLOG_BYTES} B`)
    expect(r.stdout).not.toContain('未达成')
  })

  it('🔴 must-not-hit：backlog 读不到 → 这行跟着 S4 整段消失，⛔ 不许印成「未达成」', () => {
    const r = run('nrix-reopen-nobacklog', {}, { drop: [BACKLOG_FILE] })
    expect(r.status).toBe(0)
    expect(r.stdout).not.toContain('路线 B 重开条件') // 「读不到」≠「未达成」
  })

  it('🔴 阈值与头注释里引的那个数**必须同 commit 改**（同一个值写了两处 ⇒ 钉住同步性）', () => {
    expect(Number.isInteger(REOPEN_BACKLOG_BYTES)).toBe(true)
    expect(REOPEN_BACKLOG_BYTES).toBeGreaterThan(0)
    const head = readFileSync(resolve(REPO_ROOT, GATE), 'utf8').split('\n').slice(0, 80).join('\n')
    // 头注释 📌 段逐字引了这个阈值（带千分位）+ 它的取值依据
    expect(head).toContain(REOPEN_BACKLOG_BYTES.toLocaleString('en-US'))
    expect(head).toContain('2026-09-11 到期复核')
    expect(REOPEN_NOTE).toContain('2026-09-01')
  })
})

// =============================================================================
describe('J 终态事实 —— 防「碰巧崩在同一个退出码上」的假红', () => {
  // 判据 = **闸自己的最后一句话必须是输出的最后一行**。
  // 崩溃同样是非零退出码，只断言 status 的红是假红（harness 头注释判据 2 逐字预言过）。
  it('绿档：stdout 末行恰好是闸的收尾句', () => {
    const r = run('nrix-terminal-green')
    expect(lastLine(r.stdout)).toBe('✓ audit:doc-shape PASS')
  })

  it('红档：stderr 末行必须是闸自己印的维护行，不是崩溃栈', () => {
    const r = run('nrix-terminal-red', { [STATUS_FILE]: statusDoc({ top: topOf(TOP_MAX + 1) }) })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toBe('  维护：scripts/audit-doc-shape.mjs（INFRA-F95）')
    // 崩溃栈尾会是 `Node.js vXX.Y.Z` 这类；这条把它排除掉
    expect(lastLine(r.stderr)).not.toMatch(/^Node\.js v/)
  })

  it('红档：stdout 那半照常印完（判据红 ≠ 程序半途死掉）', () => {
    const r = run('nrix-terminal-red-out', { [STATUS_FILE]: statusDoc({ top: topOf(TOP_MAX + 1) }) })
    expect(lastLine(r.stdout)).toBe(`  豁免        : ${EXEMPTIONS.length} 条`)
  })

  it('红档：真源形态那段指路照常给出（FAIL 信息必须含「该搬去哪」）', () => {
    const r = run('nrix-terminal-red-dest', { [STATUS_FILE]: statusDoc({ top: topOf(TOP_MAX + 1) }) })
    expect(r.stderr).toContain('真源形态：WRAP-UP.md:13')
    expect(r.stderr).toContain('WRAP-UP.md')
  })
})
