Lesson 197: rehearsal_completion_v1 JSON Schema Semver and Replay Parser Contract (2026)

Direct answer: Lesson 193 exports rehearsal_completion_v1.json. Lesson 197 treats that export as a versioned API: declare rollup_schema_semver, classify breaking vs additive phase fields, publish rehearsal_completion_parser_contract_v1, run replay parser contract tests in CI, and open publish gate rollup_schema_drift_open when automation would ingest an unpinned schema fork.

Lesson hero for rehearsal_completion_v1 schema semver and replay parser contract

Why this matters now (Q1 2027 intake automation)

Q1 2027 intake robots will pull rehearsal_completion_v1.json from release-evidence/ without a human opening the file. Lesson 165 already semver-governs governance packet footers—the same discipline must apply to rollup JSON or:

  • A MINOR additive field (exec_readback_pointer) crashes strict parsers written in October 2026.
  • A MAJOR rename (pass_bitphase_pass) silently clears completion in BI while JSON still “looks green.”
  • Lesson 167 synthetic replay diffs fire on numeric noise that is actually schema drift.

This lesson pins parsers before Lessons 194–199 attestation bundles hard-code rollup receipts.

Lesson objectives

You will implement:

  • Field rollup_schema_semver on every rehearsal_completion_v1 export
  • Breaking vs additive matrix for phase rows
  • Document rehearsal_completion_parser_contract_v1.json
  • CI job replay_parser_contract_tests
  • Publish gate rollup_schema_drift_open

Prerequisites

  • Lesson 165 — footer semver + replay parser pattern
  • Lesson 193rehearsal_completion_v1.json export job
  • Lesson 167 — synthetic replay diff gate (epsilon vs schema)
  • Lesson 196 — dashboard slice reads same rollup (do not fork field names)

Semver rules for rehearsal_completion_v1

Bump When Example
PATCH Doc-only clarifications; coercion unchanged Fix exported_at_utc description
MINOR Additive keys parsers must ignore exec_readback_pointer optional string
MAJOR Rename/remove keys, unit change, coercion change pass_bitphase_pass bool

Mandatory top-level keys (MAJOR contract):

{
  "schema": "rehearsal_completion_v1",
  "rollup_schema_semver": "1.0.0",
  "cert_window_id": "q1_2027_meta_holiday",
  "exported_at_utc": "2026-10-15T09:00:00Z",
  "completion_score_percent": 100.0,
  "pass": true,
  "phases": []
}

Rule: schema slug stays constant; rollup_schema_semver carries evolution. Never bump schema string for additive fields.

Breaking vs additive phase fields

Field Classification Parser rule
rehearsal_phase Breaking if enum values change Fail closed on unknown enum
pass_bit Breaking if renamed or type changes MAJOR bump
weighted_score Additive if optional shadow weighted_score_v2 added MINOR bump; old parsers ignore
rubric_version Additive if new optional MINOR bump
ics_uid Breaking if format changes MAJOR + migration shim

Store the matrix in docs/rehearsal_completion_schema_matrix.md beside Lesson 165 footer matrix.

rehearsal_completion_parser_contract_v1.json

{
  "contract_id": "rehearsal_completion_parser_contract_v1",
  "parser_version": "rc_parser_v3",
  "accepts_rollup_schema_semver": ["1.0.0", "1.1.0"],
  "unknown_key_policy": "fail_closed",
  "required_keys": [
    "schema",
    "rollup_schema_semver",
    "cert_window_id",
    "exported_at_utc",
    "completion_score_percent",
    "pass",
    "phases"
  ],
  "phase_required_keys": [
    "rehearsal_phase",
    "ics_uid",
    "attendance_status",
    "log_status",
    "pass_bit",
    "weighted_score"
  ],
  "coercion_rules": {
    "exported_at_utc": "rfc3339_utc",
    "completion_score_percent": "float_two_decimal",
    "pass_bit": "strict_bool"
  }
}

Checksum-pin the contract file in release-evidence/00-governance/parser-contracts/.

Replay parser contract tests

# tests/test_rehearsal_completion_parser_contract.py
import json
import pytest

FIXTURES = [
    ("rollup_v1_0_0_pass.json", "1.0.0", True),
    ("rollup_v1_1_0_additive_pointer.json", "1.1.0", True),
    ("rollup_v2_0_0_renamed_pass.json", "2.0.0", False),
]

@pytest.mark.parametrize("fixture,semver,should_pass", FIXTURES)
def test_parser_accepts_contract(fixture, semver, should_pass):
    payload = json.loads(open(f"fixtures/{fixture}").read())
    result = parse_rehearsal_completion(payload, contract=load_contract())
    assert result.ok == should_pass
    assert result.semver_seen == semver

CI gate: merge blocked if exporter emits semver not listed in accepts_rollup_schema_semver without contract bump PR.

Export job extension

ALTER TABLE rehearsal_completion_export_staging
  ADD COLUMN rollup_schema_semver text NOT NULL DEFAULT '1.0.0';

Export template:

{
  "schema": "rehearsal_completion_v1",
  "rollup_schema_semver": "1.0.0",
  "cert_window_id": "q1_2027_meta_holiday",
  "exported_at_utc": "2026-10-18T14:00:00Z",
  "completion_score_percent": 100.0,
  "pass": true,
  "phases": [ "..."]
}

Publish gate rollup_schema_drift_open

def validate_rollup_schema(cert_window_id: str, export_path: Path) -> None:
    payload = json.loads(export_path.read_text())
    semver = payload.get("rollup_schema_semver")
    if semver not in contract.accepts_rollup_schema_semver:
        open_gate(
            "rollup_schema_drift_open",
            block_reason=f"semver {semver} not in parser contract",
            remediation="Bump contract or migrate export",
        )
        raise PublishBlocked("rollup_schema_drift_open")

Pairs with Lesson 171 tuple drift—schema drift is a separate gate so ops knows which playbook to run.

Migration: 1.0.0 → 1.1.0 (additive example)

  1. Bump exporter to emit rollup_schema_semver: "1.1.0".
  2. Add optional exec_readback_pointer on root object only.
  3. Extend contract accepts_rollup_schema_semver with "1.1.0".
  4. Re-run Lesson 167 replay diff on dashboard columns—expect no numeric delta.
  5. Archive sample JSON in release-evidence/samples/rollup_v1_1_0/.

Never rename pass_bit without MAJOR 2.0.0 + migration shim shim_id: "pass_bit_rename_v2".

Procedure checklist

  • [ ] rollup_schema_semver present on every new export
  • [ ] Schema matrix updated for each field change
  • [ ] Parser contract version bumped with PR
  • [ ] Contract tests green on CI
  • [ ] rollup_schema_drift_open tested with bad semver fixture
  • [ ] Lesson 196 slice still reads pass + phases without forked names

Troubleshooting

Symptom Fix
CI pass, intake bot fail Bot pinned old parser_version — deploy contract + parser together
Additive field crashes parser unknown_key_policy too strict — MINOR bump + allow-list update
Dashboard green, gate open Semver mismatch only — fix contract list, not rollup math
Replay diff noise Lesson 167 epsilon — separate from schema; check semver first

Mini exercise (30 minutes)

  1. Export current rehearsal_completion_v1.json from Lesson 193 staging.
  2. Add rollup_schema_semver: "1.0.0" if missing.
  3. Author contract accepting 1.0.0 only; run tests.
  4. Add optional exec_readback_pointer; bump to 1.1.0; extend contract; re-test.
  5. Inject 2.0.0 fixture; confirm rollup_schema_drift_open blocks publish.

Continuity

  • Lesson 165 — footer semver pattern source.
  • Lesson 193 — rollup export source.
  • Lesson 167 — replay diff after schema pinned.
  • Next: Lesson 198 — WORM-pinned rollup receipt retention.

FAQ

Is schema: rehearsal_completion_v1 the semver?
No—keep slug stable; use rollup_schema_semver for evolution.

Can partner letter (Lesson 195) use different semver?
Partner letter is markdown; only JSON rollup lanes use this contract.

Does footer semver (Lesson 165) replace this?
No—footers and rollups version independently; cross-link in attestation (Lesson 199).

What if we skip 1.1.0 and jump to 2.0.0?
Allowed with MAJOR migration shim and archived samples for intake bots.


Q1 2027 automation ingests rehearsal_completion_v1.json with the same rigor as governance footers—semver + parser contract so additive fields help planners instead of silently breaking replay.