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.

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_bit→phase_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_semveron everyrehearsal_completion_v1export - 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 193 —
rehearsal_completion_v1.jsonexport 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_bit → phase_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)
- Bump exporter to emit
rollup_schema_semver: "1.1.0". - Add optional
exec_readback_pointeron root object only. - Extend contract
accepts_rollup_schema_semverwith"1.1.0". - Re-run Lesson 167 replay diff on dashboard columns—expect no numeric delta.
- 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_semverpresent 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_opentested 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)
- Export current
rehearsal_completion_v1.jsonfrom Lesson 193 staging. - Add
rollup_schema_semver: "1.0.0"if missing. - Author contract accepting
1.0.0only; run tests. - Add optional
exec_readback_pointer; bump to1.1.0; extend contract; re-test. - Inject
2.0.0fixture; confirmrollup_schema_drift_openblocks 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.