Skip to main content

runtime/replay/
shape.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Rebuilding one request's cache shape from the records it rests on,
7//! and refusing either record when the rebuild disagrees with it.
8
9use kernel::event::record::PromptShapeCompared;
10use kernel::{AxCode, AxError};
11use serde::Deserialize;
12
13/// A15 over one request's cache shape: rebuild it from two records -
14/// `prompt_shape_compared` for the tool table and the conversation, and
15/// for the four segments the run's most recent `prompt_assembled` at or
16/// before that line, because a run records `prompt_assembled` once and
17/// `prompt_shape_compared` once per request - re-derive what moved since the request before it, and refuse either
18/// line when the rebuild disagrees with what was written down. A replay
19/// re-derives an attribution; it does not trust one.
20///
21/// `previous` is the same pair of payloads for the request before this
22/// one; `None` is a run's first request.
23///
24/// # Errors
25/// A payload that does not parse, or a line whose recorded judgement or
26/// billing bound disagrees with the parts it states.
27pub fn rebuild_shape(
28    previous: Option<(&serde_json::Value, &serde_json::Value)>,
29    assembled: &serde_json::Value,
30    compared: &serde_json::Value,
31) -> Result<(), AxError> {
32    let (rebuilt, recorded) = rebuild_one(assembled, compared)?;
33    let prior = match previous {
34        Some((prior_assembled, prior_compared)) => {
35            Some(rebuild_one(prior_assembled, prior_compared)?.0)
36        }
37        None => None,
38    };
39    if rebuilt.attribute(prior.as_ref()) != recorded.changed {
40        return Err(shape_line_disagrees(
41            "its attribution names regions that did not move",
42        ));
43    }
44    let bound = rebuilt.request_upper_bound()?;
45    if bound != recorded.upper_bound {
46        return Err(shape_line_disagrees(
47            "its billing bound is not what its own parts add up to",
48        ));
49    }
50    Ok(())
51}
52
53/// One rebuilt shape, and the `prompt_shape_compared` line it was
54/// rebuilt from.
55fn rebuild_one(
56    assembled: &serde_json::Value,
57    compared: &serde_json::Value,
58) -> Result<(crate::prefix::shape::PromptShape, PromptShapeCompared), AxError> {
59    let recorded = PromptShapeCompared::deserialize(compared).map_err(|err| {
60        AxError::failure(
61            AxCode::CasCorrupt,
62            "rebuild the prompt shape",
63            format!("a prompt_shape_compared payload does not parse: {err}"),
64        )
65        .with_recovery(
66            "replay with the build that wrote this ledger, or report the damaged line: its \
67             prompt_shape_compared payload is not what this build reads",
68        )
69    })?;
70    let parts = crate::prefix::shape::segment_parts_of(assembled)?;
71    Ok((
72        crate::prefix::shape::PromptShape::from_parts(parts, recorded.tools, recorded.run),
73        recorded,
74    ))
75}
76
77fn shape_line_disagrees(why: &str) -> AxError {
78    AxError::failure(
79        AxCode::CasCorrupt,
80        "rebuild the prompt shape",
81        format!("a prompt_shape_compared line disagrees with its own records: {why}"),
82    )
83    .with_recovery(
84        "replay with the build that wrote this ledger, or report the damaged line: its recorded \
85         facts do not add up to its recorded judgement",
86    )
87}