#!/usr/bin/env python3
"""生成 v2 产物目录的边界清单 V2_excluded_keys.csv（238 键）+ README。
依据 RULING §9 ②：v2 baseline 键集合 ≡ 修准后重跑键集合；现行 baseline 独有的键不填值、不进 v2。

只读现行库，输出到 patches/v2_pool_feat_20260919/（落库日目录，先备好，不写任何权威文件）。
"""
import argparse, io, os, subprocess
import pandas as pd

APP = '/Users/xpresso/zt_app'
OUT = f'{APP}/patches/v2_pool_feat_20260919'


def load_baseline(ref):
    if ref == 'WORK':
        return pd.read_csv(f'{APP}/backtest_zt_full/strength_baseline.csv', dtype={'buy_date': str})
    r = subprocess.run(['git', '-C', APP, 'show', f'{ref}:backtest_zt_full/strength_baseline.csv'], capture_output=True)
    return pd.read_csv(io.BytesIO(r.stdout), dtype={'buy_date': str})


def main():
    ap = argparse.ArgumentParser(); ap.add_argument('--out', default=OUT); a = ap.parse_args()
    os.makedirs(a.out, exist_ok=True)
    post = pd.read_csv(f'{APP}/patches/keyset_diff_v2_20260914/keys_post.csv', dtype={'buy_date': str})
    post = post.drop_duplicates(['code', 'buy_date'])
    cur = load_baseline('WORK'); rec = load_baseline('88438d5^')
    sus = pd.read_csv(f'{APP}/patches/archive_pool_feat_v1_20260914/V1_107存疑行清单.csv', dtype=str)
    kp = set(zip(post['code'], post['buy_date']))
    ksus = set(zip(sus['code'], sus['buy_date']))
    krec = set(zip(rec['code'], rec['buy_date']))
    cur_u = cur.drop_duplicates(['code', 'buy_date'])
    rows = []
    for _, r in cur_u.iterrows():
        k = (r['code'], str(r['buy_date']))
        if k in kp:
            continue
        rows.append({
            'code': k[0], 'pool': r['pool'], 'buy_date': k[1],
            '原因': '修准后 tb 不可复现（107 存疑行，v1 档案原值冻结）' if k in ksus
                    else '修准后数据重跑不产出该键（记录版世代 live 近端行）',
            '亦见于记录版': 'Y' if k in krec else 'N',
            '归属路径': 'patches/archive_pool_feat_v1_20260914/ + sim_trading_state.json / sim_trading_nav.csv（不回改）',
        })
    df = pd.DataFrame(rows).sort_values(['buy_date', 'code'])
    df.to_csv(f'{a.out}/V2_excluded_keys.csv', index=False)
    by = df.groupby('buy_date').size().to_dict()
    L = ['# V2_excluded_keys —— v2 口径边界清单（238 键）\n',
         '依据：研究侧 RULING §9 ②（2026-09-14，分支 48e979d）。',
         '规则：**v2 baseline 键集合 ≡ 修准后数据重跑键集合**（当前 5786 键）；本清单为"现行 baseline 有、修准后重跑不产出"的键，',
         '**一律不填值**（避免"键在、信号不在"的 v1/v2 混合脏行）、**不进任何 v2 统计/回测/权重拟合**；',
         '其真身作为 live 记录留在 v1 冻结档案与 `sim_trading_state.json`/`sim_trading_nav.csv`（不回改）。\n',
         f'- 键数 **{len(df)}**，全部落在 live 窗口（buy_date ≥ 20260826）：',
         '  ' + ', '.join(f'{k}:{v}' for k, v in sorted(by.items())),
         f"- 其中属 107 存疑行 **{int((df['原因'].str.startswith('修准后 tb')).sum())}** 个；"
         f"亦见于记录版 **{int((df['亦见于记录版'] == 'Y').sum())}** 个",
         '- 不另建 live ledger（v1 档案即该窗口实盘日志，另建会造成第二事实源）。',
         '',
         '## 展示口径标注',
         '落库后看板"历史信号"由 6013 → 5786，此为**口径边界，非数据丢失**：减少的 238 键属修准前世代 live 近端行，',
         '已按上述归属留痕；看板该处将显式标注该说明（落库日一并发布）。']
    open(f'{a.out}/README.md', 'w').write('\n'.join(L) + '\n')
    print('\n'.join(L))
    print('\n输出:', a.out)


if __name__ == '__main__':
    main()
