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)]
17#[non_exhaustive]
18pub enum KernelOperation {
19 /// `rm` removing a path permanently — the trash did not catch it.
20 FsRemove,
21 /// A truncating overwrite of an existing file (`cp`, `dd`, `patch`,
22 /// `sed -i`, `tee`, `write`).
23 FsOverwrite,
24 /// `mv` replacing an existing destination.
25 FsRename,
26 /// `kaish-trash empty` discarding the recovery net itself.
27 TrashEmpty,
28}
29
30impl KernelOperation {
31 /// The dotted id this operation declares. Exhaustive by construction.
32 pub const fn as_str(self) -> &'static str {
33 match self {
34 Self::FsRemove => "fs.remove",
35 Self::FsOverwrite => "fs.overwrite",
36 Self::FsRename => "fs.rename",
37 Self::TrashEmpty => "trash.empty",
38 }
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn every_operation_has_a_dotted_two_part_id() {
48 for op in [
49 KernelOperation::FsRemove,
50 KernelOperation::FsOverwrite,
51 KernelOperation::FsRename,
52 KernelOperation::TrashEmpty,
53 ] {
54 let id = op.as_str();
55 let parts: Vec<&str> = id.split('.').collect();
56 assert_eq!(parts.len(), 2, "{id} must be `namespace.verb`");
57 assert!(
58 parts.iter().all(|p| !p.is_empty()),
59 "{id} has an empty half"
60 );
61 }
62 }
63
64 #[test]
65 fn ids_are_distinct() {
66 let ids = [
67 KernelOperation::FsRemove.as_str(),
68 KernelOperation::FsOverwrite.as_str(),
69 KernelOperation::FsRename.as_str(),
70 KernelOperation::TrashEmpty.as_str(),
71 ];
72 let unique: std::collections::BTreeSet<_> = ids.iter().collect();
73 assert_eq!(unique.len(), ids.len(), "two operations share a dotted id");
74 }
75}