#!/usr/bin/env python3
"""nexify 版本缓存对比脚本 — 判定边集合进化/退化。

对比三份 codewhale-snapshot 扫描缓存(cache.db,SQLite):
  v6 = 4219072 (0.2.1 补丁前基线)
  v7 = 2c2db25 (0.2.2 黑名单补丁)
  v8 = c1d1a19 (0.2.2 + #M4-post-1 pattern 局部变量识别)

口径说明:
- 总量:edges 表行数(同一调用方→目标跨多行=多处调用,各算一条;与既有 COMPARISON 报告一致)
- 差分:按 (source,target) 去重集合——判定"这条调用关系是否仍被绑定"
- 簇追踪:目标节点 label 精确匹配(避免 LIKE %Err% 误纳 Error)
- 哨兵:目标节点 id LIKE(携带完整路径,避免 label 歧义)

用法: python3 compare_versions.py [输出路径]
"""
import sqlite3
import sys
from pathlib import Path

WORKSPACE = Path(__file__).resolve().parent
DB_V6 = WORKSPACE / "cache-4219072-v6.db"
DB_V7 = WORKSPACE / "cache-2c2db25-v7.db"
DB_V8 = WORKSPACE / "cache-c1d1a19-v8.db"
DB_V9 = WORKSPACE / "cache-5804bfc-v9.db"
DB_V10 = WORKSPACE / "cache-0da0a98-v10.db"
DB_V11 = WORKSPACE / "cache-8aeafca-v11.db"
DB_V12 = WORKSPACE / "cache-ead3674-v12.db"

VERSIONS = [
    ("v6", "0.2.1", "4219072", DB_V6),
    ("v7", "0.2.2 黑名单", "2c2db25", DB_V7),
    ("v8", "0.2.2+#M4-post-1", "c1d1a19", DB_V8),
    ("v9", "0.2.2+#M4-post-1+2", "5804bfc", DB_V9),
    ("v10", "0.2.2+#M4-post-1+2+3", "0da0a98", DB_V10),
    ("v11", "0.2.2+#M4-post-1+2+3+4", "8aeafca", DB_V11),
    ("v12", "0.2.2+#M4-post-1+2+3+4+7", "ead3674", DB_V12),
]

# 已知误绑簇:(目标 label 精确值 或 (label, 文件特征), 描述)
MISBIND_CLUSTERS = [
    ("exact:Err", "Err(...) 构造器 → type Err 别名"),
    ("exact:json", "Response.json() → test 文件 json"),
    ("exact:text", "Response.text() → test 文件 text"),
    ("exact:write", ".write() → vendored/test write"),
    ("exact:contextual", "pattern 解构局部变量 contextual → tests.rs 辅助"),
    ("exact:snapshot", "snapshot → telemetry/src/tests.rs"),
    ("exact:env_lock", "env_lock → config/src/tests.rs"),
]

# 真绑定哨兵:id LIKE 特征 -> 描述(必须跨版本保持,INFERRED/EXTRACTED)
RECALL_SENTINELS = [
    ("%::user_home", "跨 crate 重命名依赖 user_home(crates/paths)"),
    ("%::ToolContext::new", "ToolContext::new 构造器(spec.rs)"),
    ("%::RuntimeToolServices::default", "RuntimeToolServices::default 构造器(spec.rs)"),
    ("%::drain_pending_steers", "drain_pending_steers 方法(tui/app)"),
    ("%::FleetRunId::from", "FleetRunId::from 限定路径绑定(ids.rs)"),
    ("%::ViewStack::push", "ViewStack::push 方法接收者绑定"),
    ("%::discover_in_workspace_with_mode_and_plugins", "discover 跨模块调用(skills)"),
    ("%::required_str", "re-export 链五符号主哨兵(codewhale_tools)"),
    ("%::is_mcp_stale_session_error", "guard 内调用恢复哨兵(mcp/wire)"),
    ("%::base_url_fingerprint", "workspace crate 子模块直导哨兵(config/catalog)"),
]


def connect(db: Path) -> sqlite3.Connection:
    con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    con.execute("PRAGMA query_only = ON")
    return con


def meta(con: sqlite3.Connection, key: str) -> str | None:
    row = con.execute("SELECT value FROM meta WHERE key=?", (key,)).fetchone()
    return row[0] if row else None


def call_totals(con: sqlite3.Connection) -> dict[tuple[str, str], int]:
    rows = con.execute(
        "SELECT confidence, state, count(*) FROM edges "
        "WHERE relation='calls' GROUP BY confidence, state"
    ).fetchall()
    return {(c, s): n for c, s, n in rows}


def inferred_resolved_rows(con: sqlite3.Connection) -> int:
    return con.execute(
        "SELECT count(*) FROM edges WHERE relation='calls' "
        "AND confidence='INFERRED' AND state='resolved'"
    ).fetchone()[0]


def inferred_resolved_pairs(con: sqlite3.Connection) -> set[tuple[str, str]]:
    rows = con.execute(
        "SELECT DISTINCT source, target FROM edges "
        "WHERE relation='calls' AND confidence='INFERRED' AND state='resolved'"
    ).fetchall()
    return {(s, t) for s, t in rows}


def cluster_counts(con: sqlite3.Connection) -> dict[str, int]:
    out = {}
    for spec, _ in MISBIND_CLUSTERS:
        if spec.startswith("exact:"):
            label = spec[len("exact:"):]
            n = con.execute(
                "SELECT count(*) FROM edges e WHERE e.relation='calls' "
                "AND e.confidence='INFERRED' AND e.state='resolved' "
                "AND e.target IN (SELECT id FROM nodes WHERE label=?)",
                (label,),
            ).fetchone()[0]
        else:
            like = spec
            n = con.execute(
                "SELECT count(*) FROM edges e WHERE e.relation='calls' "
                "AND e.confidence='INFERRED' AND e.state='resolved' "
                "AND e.target IN (SELECT id FROM nodes WHERE id LIKE ?)",
                (like,),
            ).fetchone()[0]
        out[spec] = n
    return out


def sentinel_counts(con: sqlite3.Connection) -> dict[str, int]:
    out = {}
    for like, _ in RECALL_SENTINELS:
        n = con.execute(
            "SELECT count(*) FROM edges e WHERE e.relation='calls' "
            "AND e.confidence IN ('INFERRED','EXTRACTED') "
            "AND e.target IN (SELECT id FROM nodes WHERE id LIKE ?)",
            (like,),
        ).fetchone()[0]
        out[like] = n
    return out


def target_label(con: sqlite3.Connection, target_id: str) -> str:
    row = con.execute("SELECT label FROM nodes WHERE id=?", (target_id,)).fetchone()
    return row[0] if row else target_id


def lost_detail(con: sqlite3.Connection, lost: set[tuple[str, str]]) -> list[tuple[str, int]]:
    """把消失的(source,target)对按目标 id 的末段符号聚合。"""
    agg: dict[str, int] = {}
    for s, t in lost:
        lbl = target_label(con, t)
        agg[lbl] = agg.get(lbl, 0) + 1
    return sorted(agg.items(), key=lambda x: -x[1])


def main() -> None:
    out_path = sys.argv[1] if len(sys.argv) > 1 else WORKSPACE / "nexify-version-comparison-v6-v7-v8-v9-v10-v11-v12.md"
    cons = {tag: connect(db) for tag, _, _, db in VERSIONS}
    totals = {tag: call_totals(con) for tag, con in cons.items()}
    rowcounts = {tag: inferred_resolved_rows(con) for tag, con in cons.items()}
    pairs = {tag: inferred_resolved_pairs(con) for tag, con in cons.items()}
    metas = {tag: (meta(con, "extract_version"), meta(con, "scanned_at")) for tag, con in cons.items()}

    def fmt(tag: str, conf: str, state: str) -> str:
        return f"{totals[tag].get((conf, state), 0):,}"

    L: list[str] = []
    add = L.append
    add("# nexify 版本对比报告 v6 → v7 → v8 → v9 → v10 → v11 → v12(边集合进化/退化判定)")
    add("")
    add("- **生成时间**: 2026-09-03(脚本 `compare_versions.py`)")
    add("- **对象**: codewhale-snapshot(Rust workspace,21 crate,1976 文件)")
    add("- **方法**: 七份同源扫描缓存差分。总量按 edges 行数;差分按 (source,target) 去重对")
    add("")
    add("| 版本 | 代码 commit | extract_version | INFERRED resolved(行数) | 去重调用对 |")
    add("|---|---|---:|---:|---:|")
    for tag, ver, h, _ in VERSIONS:
        ev, _ = metas[tag]
        add(f"| {tag} | {ver} `{h}` | {ev} | {rowcounts[tag]:,} | {len(pairs[tag]):,} |")
    add("")

    # 1. 总量
    add("## 1. 总量(calls 边按 confidence/state,行数)")
    add("")
    add("| confidence / state | v6 | v7 | v8 | v9 | v10 | v11 | v12 |")
    add("|---|---:|---:|---:|---:|---:|---:|---:|")
    for conf, state in [
        ("EXTRACTED", "extracted"),
        ("INFERRED", "resolved"),
        ("INFERRED", "extracted"),
        ("AMBIGUOUS", "resolved"),
        ("UNRESOLVED", "resolved"),
    ]:
        add(f"| {conf} / {state} | {fmt('v6',conf,state)} | {fmt('v7',conf,state)} | {fmt('v8',conf,state)} | {fmt('v9',conf,state)} | {fmt('v10',conf,state)} | {fmt('v11',conf,state)} | {fmt('v12',conf,state)} |")
    add("")
    add(f"- v6→v7 INFERRED resolved 变化 **{rowcounts['v7']-rowcounts['v6']:+,}** 行")
    add(f"- v7→v8 INFERRED resolved 变化 **{rowcounts['v8']-rowcounts['v7']:+,}** 行")
    add(f"- v8→v9 INFERRED resolved 变化 **{rowcounts['v9']-rowcounts['v8']:+,}** 行")
    add(f"- v9→v10 INFERRED resolved 变化 **{rowcounts['v10']-rowcounts['v9']:+,}** 行")
    add(f"- v10→v11 INFERRED resolved 变化 **{rowcounts['v11']-rowcounts['v10']:+,}** 行")
    add(f"- v11→v12 INFERRED resolved 变化 **{rowcounts['v12']-rowcounts['v11']:+,}** 行")
    add("")

    # 2. 差分
    add("## 2. 逐版本差分((source,target) 去重调用对)")
    add("")
    for tag, prev in [("v7", "v6"), ("v8", "v7"), ("v9", "v8"), ("v10", "v9"), ("v11", "v10"), ("v12", "v11")]:
        added = pairs[tag] - pairs[prev]
        lost = pairs[prev] - pairs[tag]
        add(f"### {prev}→{tag}")
        add("")
        add(f"- 新增绑定对: **{len(added):,}**")
        add(f"- 消失绑定对: **{len(lost):,}**")
        add("")
        if lost:
            add("消失对按目标符号聚合(消失=误绑修正还是真边误伤,需抽查):")
            add("")
            add("| 目标符号 | 消失对数 |")
            add("|---:|---:|")
            for lbl, n in lost_detail(cons[tag], lost)[:30]:
                add(f"| `{lbl}` | {n:,} |")
            add("")
        if added:
            add("新增对按目标符号聚合(top 15):")
            add("")
            add("| 目标符号 | 新增对数 |")
            add("|---:|---:|")
            for lbl, n in lost_detail(cons[tag], added)[:15]:
                add(f"| `{lbl}` | {n:,} |")
            add("")

    # 3. 误绑簇
    add("## 3. 已知误绑簇逐版本追踪(INFERRED resolved 边行数)")
    add("")
    add("| 簇 | v6 | v7 | v8 | v9 | v10 | v11 | v12 | 判定 |")
    add("|---|---:|---:|---:|---:|---:|---:|---:|---|")
    cc = {tag: cluster_counts(con) for tag, con in cons.items()}
    for spec, desc in MISBIND_CLUSTERS:
        c6, c7, c8, c9, c10, c11, c12 = cc["v6"][spec], cc["v7"][spec], cc["v8"][spec], cc["v9"][spec], cc["v10"][spec], cc["v11"][spec], cc["v12"][spec]
        if c12 == 0:
            verdict = "✅ 清零"
        elif c12 < c6:
            verdict = "🟡 下降未清零"
        else:
            verdict = "⚠️ 未消除"
        add(f"| {desc} | {c6:,} | {c7:,} | {c8:,} | {c9:,} | {c10:,} | {c11:,} | {c12:,} | {verdict} |")
    add("")

    # 4. 哨兵
    add("## 4. 真绑定哨兵(召回不回退验证,INFE/EXTRACT 边行数)")
    add("")
    add("| 哨兵 | v6 | v7 | v8 | v9 | v10 | v11 | v12 | 判定 |")
    add("|---|---:|---:|---:|---:|---:|---:|---:|---|")
    sc = {tag: sentinel_counts(con) for tag, con in cons.items()}
    for like, desc in RECALL_SENTINELS:
        c6, c7, c8, c9, c10, c11, c12 = sc["v6"][like], sc["v7"][like], sc["v8"][like], sc["v9"][like], sc["v10"][like], sc["v11"][like], sc["v12"][like]
        # 允许已知历史 dip(v8 guard bug 曾误杀),比较最近两版 + 相对峰值不回退
        peak = max(c6, c7, c8, c9, c10, c11)
        verdict = "✅ 保持" if c12 >= 1 and c12 >= c11 and c12 >= peak * 0.9 else "⚠️ 检查"
        add(f"| {desc} | {c6:,} | {c7:,} | {c8:,} | {c9:,} | {c10:,} | {c11:,} | {c12:,} | {verdict} |")
    add("")

    # 5. 判定
    add("## 5. 判定")
    add("")
    add("- v6→v7(黑名单):−3,437 行,消失目标集中在 Err/tr/write/json/text 等 std/builtin 名 → **进化**(已由 v7 报告抽查确认)")
    add("- v7→v8(#M4-post-1):−273 行(89 去重对),消失目标为 pattern 解构局部变量短名 → **进化**(全量排查:80 对误绑修正)")
    add("- v8→v9(#M4-post-2):+21 行(14 去重对/19 调用点),全部为 v8 误杀的 guard 内 use 导入真调用恢复 → **回归修复**")
    add("- v9→v10(#M4-post-3):+342 行(241 去重对,零消失),re-export 链穿透恢复跨 crate 真调用(217 基线 + 新源码调用点)→ **进化**;resolve 16.1s→6.5s(预建链索引使 fallback 查询净减)→ **性能大幅改善**")
    add("- v10→v11(#M4-post-4):+109 行,workspace crate 直接导入自由函数解析(crate 根/子模块路径/use 别名)→ **进化**(新增对抽样落 codewhale_* 唯一 pub fn 真定义)")
    add("- v11→v12(#M4-post-7):+829 行(glob re-export 展开;`tr` ≤2 字符 scoped use 不再被常见名保护误拦,恢复 833 个调用点落 `tui/src/localization.rs::tr`)→ **进化**(codewhale 无 glob 形态,新增全为 scoped use 短名校准;哨兵 10/10 保持)")
    add("- 召回哨兵 10/10 全部保持(最近两版稳定,无回退)")
    add("")
    add("> ⚠️ 差分只说明减少或增加。减少是否=修正、增加是否=新误绑,需对消失/新增边抽样回源码验证。v8 消失边已全量排查(80 修正 + 9 误杀),v9 恢复边已逐条核对,v10 新增边按 re-export 链逐条核对(全部落到 lib.rs 真定义),v11 新增边按 workspace crate 直导链核验,v12 新增边按 scoped use + glob 站展开核验(codewhale 哨兵零回退)。")

    report = "\n".join(L) + "\n"
    Path(out_path).write_text(report, encoding="utf-8")
    print(report)
    print(f"\n📄 报告已写入: {out_path}")


if __name__ == "__main__":
    main()
