#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""v1.6.7 立条(二十八)谓词载体 —— 【引号形 heredoc 之展开自检】

立条(二十八)(承 ar 裁决 34):
    凡报面 / 判据面之值经【命令替换 ∧ 变量展开】者, 须核【其已展开】——
    引号形 heredoc(`<<'EOF'`)∧ 单引号字符串【静默抑制展开】⇒ 值为【字面量】;
    并须报【展开后之实值】(禁以模板形充值)。

本载体所治之【面】= **仓库内 shell 件之引号形 heredoc 体**:
    若作者【本意要展开】而用了引号形 ⇒ 输出为字面 `$AGENT` 之类, 且
    **rc=0 ∧ 零告警 ⇒ 与正确输出不可辨**(同族于立条(二十六): 表面正常而实指缺位)。

★ 本载体【不】判「作者本意」—— 那是语义面, 机械不可判。本载体判【登记面】:
    凡【引号形 ∧ 体含 `$`-形】者皆入候选; 候选须在【名册】中逐处登记其【为何引号形为正确】
    (承立条(二十四): 沿用危险用法须逐处核 ∧ 就地留注)。
    未登记之候选 ⇒ 红 —— 强制作者【显式声明】而非默认放行。

★★ 立条(二十八)之【界】(承 ar 裁决 36 §1.3)—— 本载体所判 = 【值面】:
    射程 = 【值面】(欲取之实值); 【不含】【对象面】(被测之逐字原文)。
    ★ 其理: 若机械施用于【对象面】, 则【展开即毁证】—— 证据将不再是【被测之原文】。
    ⇒ 名册之理由须显名该条属【值面之正确引号形】抑或【对象面之须保持字面量】。
    实测例: `verify_release_gate_selftest.sh` 之夹具 heredoc 若内含【被测件之逐字源码行】,
      则该 `$` 系【证据之内容】, 非【待展开之值】⇒ 须登记为【对象面】, 禁改。

子命令:
    scan      扫 git 追踪之 shell 件, 出候选 ∧ 对名册 ⇒ rc 0/1/2
    selftest  阳性对照(夹具十组)⇒ rc 0 = 全符; 1 = 不符; 2 = 阳性对照失效
    list      仅列候选(不判名册)—— 供人工勘定
    coverage  出【候选面】对表(原始 `<<` 行 vs 已解析 heredoc)—— 查盲区

出口码:
    0 = OK(候选全在名册 ∧ 无盲区)
    1 = 违规(有未登记候选 / 名册计数不符 / 有盲区 / 有未收束 heredoc)
    2 = 阳性对照失效 / 谓词不承证力(禁以 rc=0 读作绿)

面 ∧ 域 ∧ 单位(承立条(二)):
    面 = git ls-files 之 shell 件(`*.sh` ∧ `install.sh`)—— 显名, 非工作区全量
    域 = 该面内之 heredoc 开启形
    单位 = 【heredoc 条】(非行 —— 一条 heredoc 可跨多行)
"""

import os
import re
import subprocess
import sys

# ── 面: git 追踪之 shell 件 ────────────────────────────────────────────────
SHELL_GLOBS = ["*.sh", "install.sh"]

# ── heredoc 开启形 ────────────────────────────────────────────────────────
# ★ 禁行尾锚: `<<'EOF'` 之后可有重定向 / 分号 / 注释(承本仓 ENTRY_RE 之教训:
#   行尾锚会静默跳过带尾注之条目 ⇒ 覆盖面缩水而 rc 仍 0)。
# ★ `(?<!<)<<(?!<)`: 排除 herestring `<<<`。
#   ★ v1.7.1 修(AC1 之哨符 `# <<< CHECK14_REGISTRY_END` 当场捕出): 原式唯 `(?!<)`
#     ⇒ 于 `<<<` 之【第二个 `<`】处仍可起配(后随 ` TAG` 恰为合法 tag 形)⇒ 将
#     `<<< word` 误判为 heredoc ⇒ 报【未收束】假阳。补 lookbehind ⇒ 三 `<` 皆不匹配。
#   ★ 同族于本仓在册【谓词须自核】: 单侧负向断言不足 ⇒ 须【双侧】。
OPEN_RE = re.compile(r'(?<!<)<<(?!<)(?P<dash>-?)\s*(?P<q>[\'"]?)(?P<tag>[A-Za-z_][A-Za-z0-9_]*)(?P=q)')
# ★ 禁 POSIX 类 `[[:space:]]` —— Python `re` 不支持(视作嵌套集 ⇒ 恒不匹配; 承本仓 SSH_DECL_RE 之教训)。
# ★ 类中【须含数字】—— 夹具九捕出之盲区: 原类 `[({A-Za-z_]` 漏 `\$5` 形
#   (引号形 heredoc 中 `\$` 连反斜杠一并【字面】输出 ⇒ 作者本意显然为 `$` ⇒ 仍属候选)。
DOLLAR_RE = re.compile(r'\$[({A-Za-z_0-9]')

# ── 名册: 引号形【为正确】之已勘定用法 ─────────────────────────────────────
# 键 = `file:TAG`; 值 = (条数, 理由)。★ 条数须与时所见之面【逐数相合】——
#   计数不符即视为未登记(承 AC13/S5: 裁定之射程须与其时所见之面逐数相合)。
REGISTRY = {
    "scripts/dogfood/deploy_km.sh:REMOTE": (
        1, "远端 shell 体(`ssh … bash -l -s` 之 stdin)⇒ `$()`/`$VAR` 须【远端】展开; 引号形【正确】(禁本地展开)"),
    "scripts/dogfood/startup_km.sh:REMOTE": (
        1, "同上 —— 远端 shell 体, 引号形正确"),
    "scripts/dogfood/run_dogfood_km.sh:REMOTE": (
        3, "同上 —— 远端 shell 体(三处: :52 / :84 / :93), 引号形正确"),
    "scripts/dogfood/run_dogfood_km.sh:SH": (
        1, "★ 嵌套: 位于 `<<'REMOTE'` 体内(:65)—— 于 km 端写成 `.dogfood-exec.sh` 后由 bash 执行 ⇒ "
           "`$(date)`/`${PIPESTATUS[0]}` 须在【执行时】展开; 引号形【正确】"),
    "tests/benchmark/swebench/setup.sh:ENVINFO": (
        1, "打印给用户【抄入 ~/.bashrc】之文本 ⇒ `$HOME` 须为【字面】; 引号形【正确】"),
    "scripts/tests/skip_path_registry_selftest.sh:EOF": (
        5, "★ 属【对象面】—— 五条皆【被测之逐字原文】: 夹具(F1/F2/F3/F4/F5 之合成 skip 出口)"
           "经 cat > 写入临时档后交 selfaudit_skip_paths() 解析。其 `$roadmap_ver` / `$in_progress_all`"
           " 等【须保持字面量】—— 一经本层展开即毁证(被测谓词所见之原文变样, 阳性对照随之失真)"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "scripts/tests/ac8_hack_msrv_leg.sh:PY": (
        1, "★ 属【对象面】(v1.7.2 AC8 立)—— 本 heredoc 之体为【判据原文】: 自 `ci.yml` 之 YAML "
           "转储中检索【字面量】`cargo +\"$RV\"`(判 `msrv` job 是否以 `+toolchain` 形显式择版)。"
           "其 `$RV` 【须保持字面量】—— 一经本层展开即变为 shell 变量之【值】, 检索对象变样 ⇒ "
           "该阳性对照失真(G3 之 `plus_rv` 恒 0)⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "scripts/tests/ci_face_leg.sh:REGISTRY": (
        1, "★ 属【对象面】(v1.7.2 批10 立 · 承裁决08 裁2 之 `ci-face` 腿)—— 本 heredoc 之体为"
           "【名册之逐字原文】(四字段行 `job@@disp@@cmd@@why`), 经 `printf` 写入 `$TMP/reg.txt` 后"
           "交 `awk`/`Python` **解析**。其 `$k` / `$RV` / `$cmd` 等【须保持字面量】—— 本 heredoc 之"
           "体即【被测之对象】(`G0` 断言其拆解-重组**逐字相符** ∧ `G2` 逐条 `bash -c` 实跑)"
           "⇒ 一经本层展开即变为值, 名册变样 ⇒ 二断言随之失真。⇒ 引号形【正确】, "
           "承立条(二十八)之界【对象面】。"),
    "scripts/tests/no_swallow_leg.sh:ALLOW": (
        1, "★ 属【对象面】(v1.9.17 `M1` `P0-3` 立 · 承立条(二十八)之界)—— 本 heredoc 之体为"
           "【`J` 类 allow-list 之逐字原文】(行形 `数 @@ 档 @@ 归一形`), 经 `read -r -d ''` 读入 "
           "`J_ALLOW` 后由 `awk` **逐字比对**被测面(`verify_release_gate_selftest.sh` / "
           "`golden_baseline_sm33.sh` / `install-cargo-binstall.sh` 之吞错形行)。其 `$repo` / `$keys` / "
           "`$wrong` / `$bare` / `$BIN_NAME` 等【须保持字面量】—— 键即【被测之对象】(归一形之原文, "
           "须与器所扫之实形逐字对表)⇒ 一经本层展开即变为值, 键形变样 ⇒ 匹配随之失真。"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "release/v1.8.3/tools/m2_gate4_watch_recheck_leg.sh:EOF": (
        1, "★ 属【对象面】(v1.8.3 `M2` `AC3`-③ 立)—— 本 heredoc 之体为【被测之合成发出面原文】"
           "(`:102` `cat > \"$T/ok/emit.sh\" <<'EOF'`), 其后由 `w2_form` 之三形正则**逐字检索** ∧ "
           "由 `grep -v '扫描 $scanned 提交'`(`:117`)取【缺扫描形】以造红侧夹具。其 `${head_ref}` / "
           "`$window_base` / `$scanned` / `$bad` 【须保持字面量】—— 本 heredoc 之体即【被测之对象】"
           "⇒ 一经本层展开即变为值(且该四名于本腿作用域内未定义, `set -u` 下即空/中止), "
           "发出面原文变样 ⇒ `W2` 三形断言与「缺扫描形」红侧夹具**同时失真**。"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "release/v1.8.4/tools/ac1_yi_band_backtest_mutations.sh:PY": (
        1, "★ 属【对象面】(v1.8.4 `M1` `AC1` 立 · 承立条(二十八)之界)—— 本 heredoc 之体为"
           "【变异器之检索/替换模式原文】: Python `MUT`/`MUT5B` 之字符串字面量内嵌【被测腿 "
           "`ac1_yi_band_backtest_leg.sh` 之 shell/awk 片段】—— 如 `[ \"$V\" -lt \"$LO\" ] || [ \"$V\" -gt \"$HI\" ]` ∧ "
           "awk 域引用 `{v[NR]=$1}` ∧ 消息串 `$KEY=` / `$tag` / `$NPMAX` / `$NNOVAL`, 经 `src.count(old)` "
           "**逐字匹配**后写入十二变异副本。其 `$`-名(`V`/`LO`/`HI`/`KEY`/`tag`/`i`/`1`/`NPMAX`/`NNOVAL`)"
           "于本器作用域内**皆未定义** ∧ 本器 `set -uo pipefail` ⇒ 去引号即 (a) `-u` 下**即刻中止**"
           "(unbound variable), 或 (b) 无 `-u` 时展开为空 ⇒ 检索模式变样 ⇒ `src.count` 得 `0` "
           "⇒ `HARNESS_FAIL` ⇒ 十二变异校准**读数失真**(咬合与否不可判)。"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "release/v1.8.4/tools/ac2_predicate_domain_mutations.sh:PY": (
        1, "★ 属【对象面】(v1.8.4 `M1` `AC2` 立 · 承立条(二十八)之界)—— 同族: 本 heredoc 之体为"
           "【变异器之检索/替换模式原文】—— Python `MUT` 之字符串字面量内嵌被测腿 "
           "`ac2_predicate_domain_leg.sh` 之 shell 片段: `[ \"$nface\" -ge 1 ] && [ \"$nclose\" -ge 1 ]` ∧ "
           "`judge \"$f\" \"$LIT\"` ∧ `[ \"$OOD\" -gt 0 ]`; `m5` 另须 `MUT5B` 二处同改"
           "(复原调用形 ∧ 去显式 `return 0`)。其 `$`-名(`f`/`LIT`/`OOD`/`nface`/`nclose`)于本器"
           "作用域内**皆未定义** ∧ 本器 `set -uo pipefail` ⇒ 去引号即 (a) `-u` 下**即刻中止** "
           "或 (b) 展开为空 ⇒ 检索模式变样 ⇒ `HARNESS_FAIL` ⇒ 五变异校准**读数失真**。"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "release/v1.8.5/tools/ac1_yi_band_backtest_mutations.sh:PY": (
        1, "★ 属【对象面】(v1.8.5 `M3` `AC1` 立 · 承立条(二十八)之界)—— 同族(承 `v1.8.4` 之 "
           "`ac1_yi_band_backtest_mutations.sh:PY` 条目): 本 heredoc 之体为【变异器之检索/替换模式原文】—— "
           "Python `MUT` 之字符串字面量内嵌【被测腿 `ac1_yi_band_backtest_leg.sh`(`v7`)之 shell/awk 片段】, "
           "如 `[ \"$V\" -lt \"$LO\" ] || [ \"$V\" -gt \"$HI\" ]` ∧ awk 域引用 `{v[NR]=$1}` ∧ "
           "消息串 `$KEY=` / `$tag` / `$NPMAX` / `$NNOVAL`, 经 `src.count(old)` **逐字匹配**后写入**十三**变异副本"
           "(较 `v1.8.4` 增 `m13`: pre-`RUN` 行计数删之 —— 其替换串为 awk 语句 `if (!seen) { npre++; next }`, "
           "**不含** `$`-名)。其 `$`-名(`V`/`LO`/`HI`/`KEY`/`tag`/`i`/`1`/`NPMAX`/`NNOVAL`)"
           "于本器作用域内**皆未定义** ∧ 本器 `set -uo pipefail` ⇒ 去引号即 (a) `-u` 下**即刻中止**"
           "(unbound variable), 或 (b) 无 `-u` 时展开为空 ⇒ 检索模式变样 ⇒ `src.count` 得 `0` "
           "⇒ `HARNESS_FAIL` ⇒ 十三变异校准**读数失真**(咬合与否不可判)。"
           "⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
    "release/v1.9.18/tools/check5_spec_marker_accept.sh:EOS": (
        1, "★ 属【对象面】(v1.9.18 `M1-b` 立 · 承立条(二十八)之界)—— 本 heredoc 之体为"
           "【被测之判据原文】: `--flip` 翻转对照所造之【合成修面】—— 本器将 `verify_release.sh`"
           " 逐字复制后, 于 `in_progress_spec` 之【面内】注入【一行命令】以充当 `dev` 依 `R03` 所补之"
           "【节标题臂】, 再由 `awk` 注入该行 ∧ 跑本器於合成面 ⇒ 证【本器能翻转】(判据非恒红)。"
           "其 `$(grep -P …)` 与 `$in_progress_spec` 【须保持字面量】—— ★ 本 heredoc 之体即【被测之"
           "对象】(所注入者必须是【命令】而非【本器之值】)⇒ 一经本层展开, 注入者遂不再是命令"
           "(变为此刻之字面值)⇒ 合成面与真门之形【不再同体】⇒ 该对照**自毁**(`--flip` 之绿"
           "将零证力: 所跑者已非器之原行)。⇒ 引号形【正确】, 承立条(二十八)之界【对象面】。"),
}


def shell_files(root):
    """面 = git 追踪之 shell 件。非 git 面 ⇒ 退化为目录扫描并【显名】。"""
    try:
        out = subprocess.run(["git", "-C", root, "ls-files", "--"] + SHELL_GLOBS,
                             capture_output=True, text=True, check=True).stdout
        files = [f for f in out.split("\n") if f.strip()]
        return files, "git ls-files"
    except Exception:
        files = []
        for dp, _dn, fn in os.walk(root):
            if "/target/" in dp or "/.git" in dp:
                continue
            for f in fn:
                if f.endswith(".sh") or f == "install.sh":
                    files.append(os.path.relpath(os.path.join(dp, f), root))
        return sorted(files), "os.walk(非 git 面 —— 读数含未追踪件, 慎读)"


def scan_text(text, path):
    """⇒ (候选表, 未收束表, 已解析 heredoc 数, 原始 `<<` 候选行数)

    候选 = 引号形 ∧ 体含 `$`-形。
    """
    lines = text.split("\n")
    cands, unterm = [], []
    parsed = 0
    raw_ll = 0
    consumed = set()
    for i, ln in enumerate(lines):
        if OPEN_RE.search(ln):
            raw_ll += 1
        for m in OPEN_RE.finditer(ln):
            q, tag = m.group("q"), m.group("tag")
            j = i + 1
            body = []
            while j < len(lines):
                probe = lines[j].lstrip("\t") if m.group("dash") else lines[j]
                if probe.strip() == tag:
                    break
                body.append(lines[j])
                j += 1
            if j >= len(lines):
                unterm.append((path, i + 1, tag))
                continue
            parsed += 1
            consumed.add(i)
            if q in ("'", '"'):
                b = "\n".join(body)
                if DOLLAR_RE.search(b):
                    hits = DOLLAR_RE.findall(b)
                    cands.append({
                        "file": path, "line": i + 1, "tag": tag,
                        "quote": q, "nlines": len(body),
                        "ncmd": len(re.findall(r'\$\(', b)),
                        "nbrace": len(re.findall(r'\$\{', b)),
                        "nbare": len(re.findall(r'\$[A-Za-z_0-9]', b)),
                        "samples": sorted(set(hits))[:6],
                    })
    return cands, unterm, parsed, raw_ll, len(consumed)


def do_scan(root, verbose=True):
    files, how = shell_files(root)
    all_c = []
    all_u = []
    tot_parsed = 0
    tot_raw = 0
    tot_consumed = 0
    for f in files:
        p = os.path.join(root, f)
        try:
            text = open(p, encoding="utf-8", errors="replace").read()
        except Exception:
            continue
        c, u, parsed, raw, consumed = scan_text(text, f)
        all_c += c
        all_u += u
        tot_parsed += parsed
        tot_raw += raw
        tot_consumed += consumed

    if verbose:
        print(f"面        : {how} 之 shell 件 = {len(files)} 个(谓词: {SHELL_GLOBS})")
        print(f"覆盖面    : 原始 `<<`(排除 `<<<`)候选行 = {tot_raw} ∧ 已解析 heredoc 条 = {tot_parsed}")
        print(f"单位      : 【条】—— 候选/名册皆以【heredoc 条】计(非行)")
        print(f"候选(引号形 ∧ 体含 `$`-形)= {len(all_c)} 条")

    # ── 盲区判定: 有 `<<` 候选行【未】被任何 heredoc 解析消费 ⇒ 谓词有盲区 ──
    blind = tot_raw - tot_consumed
    rc = 0
    if blind != 0:
        rc = 1
        if verbose:
            print(f"★★ 盲区: {blind} 行含 `<<` 候选而【未被解析】⇒ 上列候选数【不可读作全量】")

    # ── 名册对表(键 = file:TAG; 计数须逐数相合)──
    from collections import Counter
    cnt = Counter(f"{c['file']}:{c['tag']}" for c in all_c)
    unreg = []
    for key, n in sorted(cnt.items()):
        if key not in REGISTRY:
            unreg.append((key, n, "未登记"))
        elif REGISTRY[key][0] != n:
            unreg.append((key, n, f"名册计 {REGISTRY[key][0]} 条 ≠ 实见 {n} 条"))

    if verbose:
        print()
        print(f"{'候选 file:TAG':<52} {'条':>3} {'行':>5} {'$(':>4} {'${':>4} {'$var':>5}  判")
        for key, n in sorted(cnt.items()):
            rs = [c for c in all_c if f"{c['file']}:{c['tag']}" == key]
            verdict = "登记 ✓" if (key in REGISTRY and REGISTRY[key][0] == n) else "★ 未登记/计数不符"
            print(f"{key:<52} {n:>3} {rs[0]['line']:>5} {sum(r['ncmd'] for r in rs):>4} "
                  f"{sum(r['nbrace'] for r in rs):>4} {sum(r['nbare'] for r in rs):>5}  {verdict}")
        if not cnt:
            print("  (无候选)")

    if unreg:
        rc = 1
        if verbose:
            print()
            for key, n, why in unreg:
                print(f"★ 未登记候选: {key}(实见 {n} 条)—— {why}")
            print("★ 承立条(二十四): 沿用危险用法须【逐处核 ∧ 就地留注】——")
            print("  请【逐处】核其引号形是否本意(若本意要展开 ⇒ 去引号; 若须字面 ⇒ 入名册并书理由)。")
            print("  ★ 承立条(二十八)之【界】: 名册理由须显名【值面】抑或【对象面】——")
            print("    属【对象面】(被测之逐字原文)者【须保持字面量】, 展开即毁证, 禁改。")

    if all_u:
        rc = 1
        if verbose:
            for f, ln, tag in all_u:
                print(f"★ 未收束 heredoc: {f}:{ln} tag={tag} ⇒ 体之范围不可判")

    if verbose:
        print()
        print(f"判: {'OK ✓' if rc == 0 else '★ 违规(rc=1)'}")
    return rc, all_c, tot_raw, tot_parsed, tot_consumed


# ── 阳性对照(夹具十组)──────────────────────────────────────────────────
FIXTURES = [
    ("夹具一 引号形 ∧ $() ⇒ 须命中", "cat <<'EOF'\nx=$(date)\nEOF\n", True),
    ("夹具二 非引号形 ∧ $() ⇒ 不命中(正确展开)", "cat <<EOF\nx=$(date)\nEOF\n", False),
    ("夹具三 引号形 ∧ 纯文本 ⇒ 不命中", "cat <<'EOF'\nhello world\nEOF\n", False),
    ("夹具四 引号形 ∧ ${} ⇒ 须命中", "cat <<'EOF'\nx=${VAR}\nEOF\n", True),
    ("夹具五 引号形 ∧ $var ⇒ 须命中", "cat <<'EOF'\nrole: $AGENT\nEOF\n", True),
    ("夹具六 herestring <<< ⇒ 不误报", "cat <<<\"$x\"\n", False),
    ("夹具七 缩进形 <<-'EOF' ∧ $() ⇒ 须命中", "cat <<-'EOF'\n\tx=$(date)\n\tEOF\n", True),
    ("夹具八 双引号形 <<\"EOF\" ∧ $var ⇒ 须命中", "cat <<\"EOF\"\nrole: $AGENT\nEOF\n", True),
    ("夹具九 体含反斜杠转义 \\$ ⇒ 仍命中(面 = 字面 $)", "cat <<'EOF'\ncost: \\$5\nEOF\n", True),
    ("夹具十 同档二条 heredoc ⇒ 须计 2 条", "cat <<'EOF'\n$a\nEOF\ncat <<'EOF'\n$b\nEOF\n", True),
]


def do_selftest():
    bad = 0
    for name, text, want in FIXTURES:
        c, u, parsed, raw, consumed = scan_text(text, "<fixture>")
        got = len(c) > 0
        if name.startswith("夹具十"):
            got = (len(c) == 2)
        ok = (got == want) if not name.startswith("夹具十") else (got is True)
        print(f"  {'✓' if ok else '★ 不符'} {name}  ⇒ 候选 {len(c)} 条 / parsed {parsed}")
        if not ok:
            bad += 1
    # 夹具十一: 未收束 ⇒ 须入 unterm
    c, u, parsed, raw, consumed = scan_text("cat <<'EOF'\nno terminator\n", "<fixture>")
    ok = len(u) == 1
    print(f"  {'✓' if ok else '★ 不符'} 夹具十一 未收束 heredoc ⇒ 须报  ⇒ unterm {len(u)}")
    if not ok:
        bad += 1
    # 夹具十二: 盲区对表(含 `<<` 而行内为 herestring 之非 heredoc 形)
    c, u, parsed, raw, consumed = scan_text("cat <<<\"$x\"\n", "<fixture>")
    ok = (raw == 0 and parsed == 0)
    print(f"  {'✓' if ok else '★ 不符'} 夹具十二 herestring 不计入原始候选  ⇒ raw {raw}")
    if not ok:
        bad += 1
    # 夹具十三 (v1.7.1 增): herestring 后随【裸词】(非引号形) ⇒ 仍不误报
    #   由: 原式 `(?!<)` 单侧 ⇒ `<<< TAG` 于第二 `<` 起配 ⇒ 误判 heredoc 并报未收束
    #   (实测 = 本版 AC1 之哨符 `# <<< CHECK14_REGISTRY_END` 被误报)。补 lookbehind 后闭环。
    c, u, parsed, raw, consumed = scan_text("cat <<< word\n", "<fixture>")
    ok = (raw == 0 and parsed == 0 and len(u) == 0)
    print(f"  {'✓' if ok else '★ 不符'} 夹具十三 herestring 裸词 <<< word ⇒ 不误报  ⇒ raw {raw} unterm {len(u)}")
    if not ok:
        bad += 1
    # 夹具十四 (v1.7.1 增): 注释行内之 `# <<< TAG` ⇒ 亦不误报(本版哨符之实际形)
    c, u, parsed, raw, consumed = scan_text("# <<< CHECK14_REGISTRY_END\n", "<fixture>")
    ok = (raw == 0 and parsed == 0 and len(u) == 0)
    print(f"  {'✓' if ok else '★ 不符'} 夹具十四 注释内 `# <<< TAG` ⇒ 不误报  ⇒ raw {raw} unterm {len(u)}")
    if not ok:
        bad += 1
    print()
    if bad == 0:
        print("阳性对照十四组全符 ✓(v1.7.1 增 夹具十三/十四: herestring 裸词 ∧ 注释内 `<<<`)")
        return 0
    print(f"★★ 阳性对照【失效】: {bad} 组不符 ⇒ 谓词不承证力")
    return 2


def do_coverage(root):
    files, how = shell_files(root)
    print(f"面 = {how}; 件 = {len(files)}")
    for f in files:
        try:
            text = open(os.path.join(root, f), encoding="utf-8", errors="replace").read()
        except Exception:
            continue
        _c, _u, parsed, raw, consumed = scan_text(text, f)
        if raw or parsed:
            flag = "" if raw == consumed else f"  ★ 盲区 {raw - consumed}"
            print(f"  {f:<56} 原始 << 行 {raw:>2} ∧ 已解析 {parsed:>2} ∧ 消费行 {consumed:>2}{flag}")
    return 0


def main():
    cmd = sys.argv[1] if len(sys.argv) > 1 else "scan"
    root = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
    if cmd == "scan":
        rc, *_ = do_scan(root)
        return rc
    if cmd == "list":
        _rc, c, *_ = do_scan(root, verbose=False)
        for x in c:
            print(f"{x['file']}:{x['line']}\t{x['tag']}\t{x['quote']}\t{x['nlines']}\t{x['samples']}")
        return 0
    if cmd == "coverage":
        return do_coverage(root)
    if cmd == "selftest":
        return do_selftest()
    print(f"未知子命令: {cmd}", file=sys.stderr)
    return 2


if __name__ == "__main__":
    sys.exit(main())