#!/usr/bin/env python3 """WS3 (г) verification — re-run the frozen miner-v1 (A3 = V-C) on the pinned exp16 inputs and confirm it reproduces the golden fixtures that pin the FUTURE Go port. Does NOT overwrite the golden (writes only to design11). Asserts: - A3 catastrophe ranks {方源:0, 蛊:1, 蛊师:2, 古月:21} on the full slice (PASS <50); - A3 recall@proposed = 0.965 (full) / 0.932 (test-half); - A3 candidate count = 13618 (full). Compares value-by-value against the stored golden JSON (the byte-parity harness in mem_select-reverse: Python = reference, Go port must reproduce). $0: deterministic, no LLM, no network. Reuses eval/exp16 miner-v1 (frozen SHAs §1.10). Writes /home/ubuntu/books/gu-zhenren/design11/ws3_miner_verify.json (OUT of git). """ import json, os, sys EVAL = "/home/ubuntu/projects/textmachine/eval" # exp15 first, then exp16 at position 0 so exp16/arms.py wins over exp15/arms.py # (name collision the exp16_common self-review flagged; it uses sys.path.append for exp15/mem_select). sys.path.insert(0, f"{EVAL}/exp15") sys.path.insert(0, f"{EVAL}/exp16") import exp16_common as X import arms as A GOLD_DIR = "/home/ubuntu/books/gu-zhenren/exp16" OUT = "/home/ubuntu/books/gu-zhenren/design11/ws3_miner_verify.json" CAT_EXPECT = {"方源": 0, "蛊": 1, "蛊师": 2, "古月": 21} def rank_of(ranked, srcnorm): for i, c in enumerate(ranked): if c.src == srcnorm: return i return None def main(): chunks = X.load_chunks() gt = X.load_gt() contrast = X.Contrast() cfg = A.FROZEN arms = A.Arms(chunks, contrast, cfg, use_spread=True) a3_full = arms.arm_A3(cfg["lam"]) # catastrophe ranks on full A3 cat_ranks = {} for term in X.CATASTROPHE: cat_ranks[term] = rank_of(a3_full, X.norm(term)) cat_pass = all(r is not None and r < 50 for r in cat_ranks.values()) # recall@proposed full rec_full, _ = A.recall_by(a3_full, gt, chunks, top_k=None, include_annotation=True) overall_full = rec_full["overall"][0] # test half (ch16-25) — GT filtered to entities occurring in test chapters (run_arms phase_test) test_chunks = [c for c in chunks if c.chapter in X.TEST_CH] gt_t = A.gt_in_chapters(gt, chunks, X.TEST_CH) arms_t = A.Arms(test_chunks, contrast, cfg, use_spread=True) a3_test = arms_t.arm_A3(cfg["lam"]) rec_test, _ = A.recall_by(a3_test, gt_t, test_chunks, top_k=None, include_annotation=True) overall_test = rec_test["overall"][0] # load stored golden for value-by-value compare gold_full = json.load(open(f"{GOLD_DIR}/full_slice_result.json")) gold_test = json.load(open(f"{GOLD_DIR}/test_half_result.json")) gold_a3_full = gold_full.get("A3", {}) gold_a3_test = gold_test.get("A3", {}) n_cands_full = len(a3_full) result = { "catastrophe_ranks_A3_full": cat_ranks, "catastrophe_expected": CAT_EXPECT, "catastrophe_match": cat_ranks == CAT_EXPECT, "catastrophe_screen_pass": cat_pass, "recall_at_proposed_A3_full": round(overall_full, 4) if overall_full is not None else None, "recall_at_proposed_A3_test": round(overall_test, 4) if overall_test is not None else None, "n_candidates_A3_full": n_cands_full, "golden_full_overall": gold_a3_full.get("recall_proposed", {}).get("overall") if isinstance(gold_a3_full.get("recall_proposed"), dict) else gold_a3_full.get("recall_proposed"), "golden_test_overall": gold_a3_test.get("recall_proposed", {}).get("overall") if isinstance(gold_a3_test.get("recall_proposed"), dict) else gold_a3_test.get("recall_proposed"), "top20_A3_full": [{"src": c.src, "score": round(c.score, 2), "freq": c.freq, "types": c.types, "from_pattern": c.from_pattern} for c in a3_full[:20]], } os.makedirs(os.path.dirname(OUT), exist_ok=True) json.dump(result, open(OUT, "w"), ensure_ascii=False, indent=2) print(json.dumps({k: v for k, v in result.items() if k != "top20_A3_full"}, ensure_ascii=False, indent=2)) print("\ntop-5 A3 full:", [(c.src, round(c.score,1), c.freq) for c in a3_full[:5]]) print(f"\nVERDICT: catastrophe {'MATCH' if result['catastrophe_match'] else 'MISMATCH'} {cat_ranks}; " f"n_cands={n_cands_full} (golden 13618); recall_full={result['recall_at_proposed_A3_full']} (golden 0.965).") if __name__ == "__main__": main()