use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelNodeType { Workspace, Folder, Page, Section, Asset, Book, Pdf, Mindmap, MindmapNode, Summary, AiNote, ReferenceAnchor, ContentNode, IndexNode, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelEdgeType { ParentOf, ChildOf, Contains, References, BacklinksTo, SourceOf, DerivedFrom, Summarizes, Indexes, PointsTo, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelProjectionKind { SidebarTree, PageTree, FileTree, Mindmap, ReadView, SearchResults, RagIndex, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelGraphDirection { Outgoing, Incoming, Both, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct KernelSubtreeRef { pub root_node_id: String, #[serde(default)] pub path: Vec, pub depth: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct KernelNodeMetadata { pub title: Option, pub icon: Option, #[serde(default)] pub tags: Vec, pub created_at: Option, pub updated_at: Option, #[serde(default)] pub extra: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelContentPayload { pub format: String, pub body: Value, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct KernelRefsPayload { #[serde(default)] pub reference_node_ids: Vec, #[serde(default)] pub evidence_node_ids: Vec, #[serde(default)] pub source_node_ids: Vec, #[serde(default)] pub extra: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct KernelAuditStamp { pub version: u64, pub revision: Option, pub request_id: Option, pub trace_id: Option, pub actor_id: Option, } impl Default for KernelAuditStamp { fn default() -> Self { Self { version: 1, revision: None, request_id: None, trace_id: None, actor_id: None, } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelNode { pub id: String, pub node_type: KernelNodeType, pub workspace_id: Option, pub parent_id: Option, pub subtree: Option, pub metadata: KernelNodeMetadata, pub content: Option, pub refs: Option, #[serde(default)] pub audit: KernelAuditStamp, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelEdge { pub id: String, pub edge_type: KernelEdgeType, pub workspace_id: Option, pub from_node_id: String, pub to_node_id: String, #[serde(default)] pub metadata: BTreeMap, #[serde(default)] pub audit: KernelAuditStamp, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct KernelProjectionFilter { #[serde(default)] pub node_types: Vec, #[serde(default)] pub edge_types: Vec, #[serde(default)] pub include_deleted: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct KernelGetNode { pub node_id: String, pub workspace_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelGetSubtree { pub subtree: KernelSubtreeRef, pub workspace_id: Option, #[serde(default)] pub include_edges: bool, #[serde(default)] pub filters: KernelProjectionFilter, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelListChildren { pub parent_node_id: String, pub workspace_id: Option, #[serde(default)] pub node_types: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelListEdges { pub node_id: String, pub workspace_id: Option, #[serde(default)] pub edge_types: Vec, pub direction: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelTraverseGraph { pub start_node_id: String, pub workspace_id: Option, #[serde(default)] pub edge_types: Vec, pub max_depth: u32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelProjectionRequest { pub projection: KernelProjectionKind, pub workspace_id: Option, pub root_node_id: Option, pub subtree: Option, #[serde(default)] pub filters: KernelProjectionFilter, #[serde(default)] pub include_content: bool, #[serde(default)] pub include_edges: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelSubtreeResult { pub root_node_id: String, pub nodes: Vec, pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelEdgeListResult { pub node_id: String, pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct KernelGraphVisit { pub node_id: String, pub depth: u32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelGraphTraversalResult { pub start_node_id: String, pub visited: Vec, pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelProjectionItem { pub node_id: String, pub parent_node_id: Option, pub node_type: KernelNodeType, pub title: Option, pub depth: u32, pub position: Option, pub child_count: u32, pub expanded_by_default: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelProjectionResult { pub projection_id: String, pub projection: KernelProjectionKind, pub root_node_id: Option, pub items: Vec, pub edges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelCreateNode { pub node: KernelNode, pub position: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelUpdateNode { pub node_id: String, pub metadata: Option, pub content: Option, pub refs: Option, pub audit: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct KernelMoveSubtree { pub subtree: KernelSubtreeRef, pub new_parent_node_id: Option, pub sort_order: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelAttachEdge { pub edge: KernelEdge, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct KernelDetachEdge { pub edge_id: Option, pub from_node_id: Option, pub to_node_id: Option, pub edge_type: Option, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn kernel_projection_request_roundtrip_keeps_subtree_and_filters() { let request = KernelProjectionRequest { projection: KernelProjectionKind::SidebarTree, workspace_id: Some("ws_1".into()), root_node_id: Some("page_root".into()), subtree: Some(KernelSubtreeRef { root_node_id: "page_root".into(), path: vec!["page_root".into(), "page_child".into()], depth: Some(2), }), filters: KernelProjectionFilter { node_types: vec![KernelNodeType::Page, KernelNodeType::Folder], edge_types: vec![KernelEdgeType::ParentOf], include_deleted: false, }, include_content: false, include_edges: true, }; let value = serde_json::to_value(&request).expect("request 应可序列化"); assert_eq!(value["projection"], json!("sidebar_tree")); assert_eq!(value["subtree"]["rootNodeId"], json!("page_root")); let decoded: KernelProjectionRequest = serde_json::from_value(value).expect("request 应可反序列化"); assert_eq!(decoded, request); } #[test] fn kernel_node_serializes_content_refs_and_audit_boundaries() { let node = KernelNode { id: "node_1".into(), node_type: KernelNodeType::Summary, workspace_id: Some("ws_1".into()), parent_id: Some("page_1".into()), subtree: Some(KernelSubtreeRef { root_node_id: "page_1".into(), path: vec!["page_1".into(), "node_1".into()], depth: Some(1), }), metadata: KernelNodeMetadata { title: Some("摘要节点".into()), icon: None, tags: vec!["summary".into()], created_at: Some("2026-04-16T00:00:00Z".into()), updated_at: Some("2026-04-16T00:00:00Z".into()), extra: BTreeMap::new(), }, content: Some(KernelContentPayload { format: "markdown".into(), body: json!({"text": "摘要正文"}), }), refs: Some(KernelRefsPayload { reference_node_ids: vec!["page_1".into()], evidence_node_ids: vec!["asset_1".into()], source_node_ids: vec!["pdf_1".into()], extra: BTreeMap::new(), }), audit: KernelAuditStamp { version: 3, revision: Some(9), request_id: Some("req_1".into()), trace_id: Some("trace_1".into()), actor_id: Some("user_1".into()), }, }; let value = serde_json::to_value(&node).expect("node 应可序列化"); assert_eq!(value["nodeType"], json!("summary")); assert_eq!(value["content"]["format"], json!("markdown")); assert_eq!(value["refs"]["referenceNodeIds"], json!(["page_1"])); assert_eq!(value["audit"]["traceId"], json!("trace_1")); } }