CB-WP-0011-T02: cb-play --inspect walks a recorded game

Renders the table after every step of a .cbreplay bundle or a scenario
YAML, from any seat's projection or a spectator's. This is the first
thing in the project that answers 'what did the table look like when it
went wrong?' without adding a dbg! and re-running.

INTERFACE CHANGE (flagged per InnerLoop chaos limits -- this is a
tier-S pass that touched a runtime crate): cb-game-runtime gains
replay::open, extracted out of replay::replay. Dev-only, behind the
scenarios feature, no type changed. The point of the extraction is that
the inspector and the replay gate share one bundle reader, controls
included, so the inspector cannot show a state a replay never reached.

Three M-D1-MUT controls, each red for its stated reason. The
load-bearing one asserts one rendered table per step: without it, a
walk that rendered nothing would still report a matching hash.
This commit is contained in:
tegwick 2026-08-02 02:45:55 +02:00
parent d2ca1046c0
commit b11fc91fd4
4 changed files with 478 additions and 22 deletions

View file

@ -133,12 +133,18 @@ pub fn write_bundle(
Ok(bundle)
}
/// Re-execute a bundle and require it to reproduce the recorded hash.
/// Open a bundle for re-execution: the manifest, the restored initial
/// state, and the recorded steps in order.
///
/// Returns `Err` when the bundle is corrupt, incomplete, internally
/// inconsistent, or does not reproduce — all of which are the point. A
/// round-trip that cannot fail proves nothing.
pub fn replay<G>(bundle: &Path) -> Result<ReplayReport, String>
/// Extracted from [`replay`] by CB-WP-0011 T02 so the inspector walks a
/// bundle through **the same reader the replay gate uses**. A second
/// reader would let the inspector show states a replay never reached —
/// and it would be a duplicated fact, checked by nothing, in the one
/// place where being wrong is silent.
///
/// Every control [`replay`] performed before touching the command log is
/// performed here, including the one that makes the seed load-bearing.
pub fn open<G>(bundle: &Path) -> Result<(Manifest, G, Vec<CommandStep>), String>
where
G: ScenarioGame,
{
@ -165,17 +171,33 @@ where
&std::fs::read(bundle.join(INITIAL)).map_err(|e| err("read initial", e))?,
)
.map_err(|e| err("parse initial", e))?;
let mut state: G = serde_json::from_value(initial).map_err(|e| err("restore initial", e))?;
let state: G = serde_json::from_value(initial).map_err(|e| err("restore initial", e))?;
// K11 framing: a truncated or corrupt command log is rejected here.
let raw = std::fs::read(bundle.join(COMMANDS)).map_err(|e| err("read commands", e))?;
let records = parse(&raw).map_err(|e| err("command log", e))?;
let mut steps = Vec::with_capacity(records.len());
for record in &records {
steps.push(serde_json::from_slice(record).map_err(|e| err("decode step", e))?);
}
Ok((manifest, state, steps))
}
/// Re-execute a bundle and require it to reproduce the recorded hash.
///
/// Returns `Err` when the bundle is corrupt, incomplete, internally
/// inconsistent, or does not reproduce — all of which are the point. A
/// round-trip that cannot fail proves nothing.
pub fn replay<G>(bundle: &Path) -> Result<ReplayReport, String>
where
G: ScenarioGame,
{
let (manifest, mut state, steps) = open::<G>(bundle)?;
let mut applied = 0usize;
for record in &records {
let step: CommandStep =
serde_json::from_slice(record).map_err(|e| err("decode step", e))?;
let (actor, command) = G::parse_command(&step)?;
for step in &steps {
let (actor, command) = G::parse_command(step)?;
if let Ok(produced) = state.validate(actor, &command) {
for event in produced {
state.fold(&event);