#!/usr/bin/env python3 """Regenerate tests/fixtures/normalization_benchmark_v1.json with core cases + N random samples. Uses a fixed seed so CI expectations are stable. Run from repo root after changing merge rules if you intentionally want to refresh sampled rows (then review git diff). python scripts/generate_normalization_benchmark_fixture.py """ from __future__ import annotations import argparse import json import random from pathlib import Path from backend.kink_merge import propose_merge_clusters _REPO = Path(__file__).resolve().parents[1] _DEFAULT_OUT = _REPO / "tests" / "fixtures" / "normalization_benchmark_v1.json" # ASCII-only, length >= SINGLE_TOKEN_MERGE_MIN_CHARS (6); prefer 8+ to stay clear of edge. _WORD_POOL = [ "moonbeams", "starlight", "fireworks", "cranberry", "waterfall", "butterfly", "dragonfly", "nightfall", "riverbank", "snowflake", "sunflower", "pineapple", "blueberry", "strawberry", "raspberry", "candlewax", "bookshelf", "doorframe", "sidewalks", "skylights", "clockwork", "dreamland", "friendship", "leadership", "membership", "partnership", "relationship", "scholarship", "fellowship", "citizenship", "apprentice", "basketball", "volleyball", "footprints", "headlights", "spotlights", "flashlight", "lighthouse", "greenhouse", "wheelhouse", "glasshouse", "farmhouses", "workhouses", "guesthouse", "roadhouses", "greenfield", "battlefield", "minefields", "cornfields", "haystacks", ] def _core_cases() -> list[dict]: return [ { "rows": [ ["snug_a", "Snuggles", "fetlife_fetish", 10.0], ["snug_b", "snuggles", "fetlife_fetish", 200.0], ], "expect_canonical_id": "snug_b", "expect_duplicate_ids": ["snug_a"], }, { "rows": [ ["dup_fmf", "threesome fmf", "fetlife_fetish", 100.0], ["canon_fmf", "FMF threesomes", "fetlife_fetish", 500.0], ["mfm_other", "MFM threesomes", "fetlife_fetish", 400.0], ], "expect_canonical_id": "canon_fmf", "expect_duplicate_ids": ["dup_fmf"], "expect_excluded_ids": ["mfm_other"], }, ] def _random_case_only_clusters(*, rng: random.Random, n: int) -> list[dict]: words = rng.sample(_WORD_POOL, k=min(n, len(_WORD_POOL))) cases: list[dict] = [] for i, w in enumerate(words): lo_id = f"rnd_{i:02d}_lo" hi_id = f"rnd_{i:02d}_hi" cluster = f"sbench_{rng.randint(0, 9999):04d}" rows = [ [lo_id, w.capitalize(), cluster, float(rng.randint(1, 50))], [hi_id, w, cluster, float(rng.randint(60, 200))], ] tuples = [tuple(r) for r in rows] clusters = propose_merge_clusters(tuples) assert len(clusters) == 1, (w, clusters) c0 = clusters[0] case: dict = { "rows": rows, "expect_canonical_id": c0["canonical_id"], "expect_duplicate_ids": list(c0["duplicate_ids"]), } cases.append(case) return cases def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--out", type=Path, default=_DEFAULT_OUT, help="fixture path to write") ap.add_argument("--seed", type=int, default=42) ap.add_argument("--random-cases", type=int, default=20, help="number of seeded random case-only clusters") args = ap.parse_args() rng = random.Random(args.seed) # noqa: S311 — deterministic benchmark sampling, not crypto payload = { "comment": ( "Golden expectations for propose_merge_clusters; extend after reviewing export JSONL. " "Random block was generated by scripts/generate_normalization_benchmark_fixture.py." ), "random_seed": args.seed, "random_case_only_count": args.random_cases, "merge_cluster_cases": _core_cases() + _random_case_only_clusters(rng=rng, n=args.random_cases), } args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") print(json.dumps({"wrote": str(args.out), "cases": len(payload["merge_cluster_cases"])}, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())