Skip to main content

kernel/event/record/
document.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//! A document saved through the page, and the three lines of a
7//! proposal's life (`crates/kernel/spec/Event/Record.lean` ยง8-83).
8//!
9//! **The save carries two digests and never the text.** The document is
10//! on disk; the line says which document moved, from which version to
11//! which. A proposal is different: its two texts are what a run said,
12//! which no file keeps, so they travel in the line and a reopened city
13//! folds an undecided card back from the Ledger alone.
14//!
15//! The span is two integers rather than `documents::Span`, because the
16//! kernel does not depend on `documents`; a reader turns them back into
17//! a span through the one constructor that refuses a reversed pair.
18
19use serde::{Deserialize, Serialize};
20
21use crate::{Address, B3Hash};
22
23/// `document_written`: one save of one document landed.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26pub struct DocumentWritten {
27    /// Which document.
28    pub at: Address,
29    /// The version the save was made on.
30    pub baseline: B3Hash,
31    /// The version it left, which is the next save's baseline.
32    pub version: B3Hash,
33    /// How long the new version is, not how much changed.
34    pub bytes: u64,
35}
36
37/// `proposal_offered`: a run suggests replacing one stretch of one
38/// version with other text.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41pub struct ProposalOffered {
42    pub doc: Address,
43    pub baseline: B3Hash,
44    /// The half-open byte interval of the baseline the suggestion is
45    /// about.
46    pub start: u64,
47    pub end: u64,
48    /// The text that interval holds, in the baseline's encoding.
49    pub before: String,
50    /// The text suggested in its place.
51    pub after: String,
52}
53
54/// `proposal_decided`: a person's verdicts on one card. A changed
55/// sentence the verdicts do not name is rejected, so an empty list
56/// rejects the whole card.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59pub struct ProposalDecided {
60    pub proposal: B3Hash,
61    pub verdicts: Vec<SliceVerdict>,
62}
63
64/// A person's verdict on one sentence of a card, by its place on the
65/// card counted from zero.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68pub struct SliceVerdict {
69    pub slice: u32,
70    pub verdict: Verdict,
71}
72
73/// Take the sentence's change, or take an inserted sentence after
74/// rewriting it.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78pub enum Verdict {
79    Accept,
80    Amend { text: String },
81}
82
83/// `proposal_withdrawn`: the run that offered a card took it back while
84/// it was still open.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
87pub struct ProposalWithdrawn {
88    pub proposal: B3Hash,
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
93mod tests {
94    use super::*;
95    use crate::event::Payload;
96
97    /// Each line's bytes, so a ledger written by this build reads back
98    /// in the next one.
99    #[test]
100    fn each_document_line_spells_its_keys_once() {
101        let doc = Address::parse("lab/notes.md").unwrap();
102        let zero = B3Hash::from_bytes([0; 32]);
103        let hex = "0".repeat(64);
104        let cases: [(Payload, String); 4] = [
105            (
106                Payload::of(&DocumentWritten {
107                    at: doc.clone(),
108                    baseline: zero,
109                    version: zero,
110                    bytes: 3,
111                })
112                .unwrap(),
113                format!(
114                    "{{\"at\":\"lab/notes.md\",\"baseline\":\"{hex}\",\"bytes\":3,\"version\":\"{hex}\"}}"
115                ),
116            ),
117            (
118                Payload::of(&ProposalOffered {
119                    doc,
120                    baseline: zero,
121                    start: 1,
122                    end: 2,
123                    before: "a".to_owned(),
124                    after: "b".to_owned(),
125                })
126                .unwrap(),
127                format!(
128                    "{{\"after\":\"b\",\"baseline\":\"{hex}\",\"before\":\"a\",\"doc\":\"lab/notes.md\",\"end\":2,\"start\":1}}"
129                ),
130            ),
131            (
132                Payload::of(&ProposalDecided {
133                    proposal: zero,
134                    verdicts: vec![
135                        SliceVerdict {
136                            slice: 1,
137                            verdict: Verdict::Accept,
138                        },
139                        SliceVerdict {
140                            slice: 2,
141                            verdict: Verdict::Amend {
142                                text: "c".to_owned(),
143                            },
144                        },
145                    ],
146                })
147                .unwrap(),
148                format!(
149                    "{{\"proposal\":\"{hex}\",\"verdicts\":[{{\"slice\":1,\"verdict\":\"accept\"}},{{\"slice\":2,\"verdict\":{{\"amend\":{{\"text\":\"c\"}}}}}}]}}"
150                ),
151            ),
152            (
153                Payload::of(&ProposalWithdrawn { proposal: zero }).unwrap(),
154                format!("{{\"proposal\":\"{hex}\"}}"),
155            ),
156        ];
157        for (payload, wire) in cases {
158            assert_eq!(serde_json::to_string(&payload).unwrap(), wire);
159            let back: Payload = serde_json::from_str(&wire).unwrap();
160            assert_eq!(back, payload);
161        }
162    }
163}