Skip to main content

kaish_kernel/
operation.rs

1//! The kernel's operation taxonomy — what class of effect a builtin can have.
2//!
3//! This is analysis vocabulary, not policy. A tool declares the operations it
4//! can perform via [`ToolSchema::with_operations`](kaish_types::ToolSchema::with_operations),
5//! and an embedder reading a plan learns that `rm` can remove a path without
6//! having to recognize the name `rm`. The kernel itself does not decide
7//! anything from these ids.
8//!
9//! In-tree operations come from a closed enum and the mapping to the dotted id
10//! is an exhaustive match, so a new effect site is a **compile error** until it
11//! names its operation — a string sniff on the command name would instead pick
12//! a plausible wrong default in silence. The `fs.` and `trash.` namespaces
13//! belong to the kernel.
14
15/// Every operation an in-tree builtin can declare.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum KernelOperation {
18    /// `rm` removing a path permanently — the trash did not catch it.
19    FsRemove,
20    /// A truncating overwrite of an existing file (`cp`, `dd`, `patch`,
21    /// `sed -i`, `tee`, `write`).
22    FsOverwrite,
23    /// `mv` replacing an existing destination.
24    FsRename,
25    /// `kaish-trash empty` discarding the recovery net itself.
26    TrashEmpty,
27}
28
29impl KernelOperation {
30    /// The dotted id this operation declares. Exhaustive by construction.
31    pub const fn as_str(self) -> &'static str {
32        match self {
33            Self::FsRemove => "fs.remove",
34            Self::FsOverwrite => "fs.overwrite",
35            Self::FsRename => "fs.rename",
36            Self::TrashEmpty => "trash.empty",
37        }
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn every_operation_has_a_dotted_two_part_id() {
47        for op in [
48            KernelOperation::FsRemove,
49            KernelOperation::FsOverwrite,
50            KernelOperation::FsRename,
51            KernelOperation::TrashEmpty,
52        ] {
53            let id = op.as_str();
54            let parts: Vec<&str> = id.split('.').collect();
55            assert_eq!(parts.len(), 2, "{id} must be `namespace.verb`");
56            assert!(
57                parts.iter().all(|p| !p.is_empty()),
58                "{id} has an empty half"
59            );
60        }
61    }
62
63    #[test]
64    fn ids_are_distinct() {
65        let ids = [
66            KernelOperation::FsRemove.as_str(),
67            KernelOperation::FsOverwrite.as_str(),
68            KernelOperation::FsRename.as_str(),
69            KernelOperation::TrashEmpty.as_str(),
70        ];
71        let unique: std::collections::BTreeSet<_> = ids.iter().collect();
72        assert_eq!(unique.len(), ids.len(), "two operations share a dotted id");
73    }
74}