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}