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}