Skip to main content

kernel/
change.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//! What moved between two checkpoints, as a fact rather than as a patch.
7//! The part `crates/kernel/spec/Change.lean` specifies this module.
8//!
9//! Here rather than beside the git call that produces it, for the reason
10//! `Restoration` is here: three places need this shape — `storage` reads
11//! it off two trees, `wire` carries it, and the interface draws it —
12//! and `wire` cannot see `storage`. Defining it twice and converting
13//! between the copies would be two definitions of what a file change is,
14//! and the one that drifts is always the one nobody is looking at.
15//!
16//! Paths and counts only. Patch text is file content, and file content
17//! leaving this machine is the question `secret::scan` exists to answer;
18//! a hunk therefore has to be asked for on its own and scanned like
19//! anything else, which is why no field here can hold one.
20
21use serde::{Deserialize, Serialize};
22
23/// How much of a file moved.
24///
25/// An enum rather than two numbers, because a binary file has no line
26/// count. Rendering one as `+0 −0` would be the interface reporting a
27/// measurement nobody made, and a reader would take it for a file that
28/// was touched and left alone.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub enum Lines {
33    Counted { added: u32, removed: u32 },
34    Binary,
35}
36
37/// What happened to the file.
38///
39/// `Renamed` is its own arm because a rename and a delete-plus-add are
40/// different facts about the same two trees, and somebody deciding
41/// whether an agent moved code or rewrote it needs the difference.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub enum How {
46    Added,
47    Modified,
48    Deleted,
49    Renamed { from: String },
50}
51
52/// One file, as the difference between two trees describes it.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55pub struct FileChange {
56    pub path: String,
57    pub how: How,
58    pub lines: Lines,
59}
60
61#[cfg(test)]
62#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
63mod tests {
64    use super::*;
65
66    /// A binary file and an untouched one must not spell the same row.
67    #[test]
68    fn a_binary_file_is_not_a_file_that_moved_nothing() {
69        let binary = serde_json::to_string(&Lines::Binary).unwrap();
70        let nothing = serde_json::to_string(&Lines::Counted {
71            added: 0,
72            removed: 0,
73        })
74        .unwrap();
75        assert_ne!(binary, nothing);
76    }
77
78    #[test]
79    fn a_rename_carries_where_the_file_came_from() {
80        let moved = FileChange {
81            path: "lab/lex.rs".to_owned(),
82            how: How::Renamed {
83                from: "lab/lexer.rs".to_owned(),
84            },
85            lines: Lines::Counted {
86                added: 0,
87                removed: 0,
88            },
89        };
90        let bytes = serde_json::to_vec(&moved).unwrap();
91        let back: FileChange = serde_json::from_slice(&bytes).unwrap();
92        assert_eq!(back, moved);
93    }
94}