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, Table, 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 WorkspaceSourceKind { LocalFolder, ConvexWorkspace, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum WorkspaceSourceCapability { LoadSnapshot, Watch, PreflightCommand, ExecuteCommand, ResolvePageAggregate, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WorkspaceSource { pub source_kind: WorkspaceSourceKind, pub root_uri: String, pub workspace_id: String, #[serde(default)] pub capabilities: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum KernelProjectionCapability { Expand, Open, Drag, Drop, Select, CreateChild, Rename, Archive, Restore, ContextMenu, Reorder, OpenAsset, Pick, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelProjectionRowKind { Document, Index, Asset, AssetFolder, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelProjectionResourceKind { Workspace, Document, Index, Asset, AssetFolder, Mindmap, Table, Book, Pdf, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum KernelProjectionAssetKind { File, Mindmap, Table, Book, Pdf, Image, Video, Audio, Unknown, } #[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, pub query: Option, pub max_results: Option, #[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 row_id: String, pub row_kind: KernelProjectionRowKind, pub node_id: String, pub parent_node_id: Option, pub node_type: KernelNodeType, pub projection_kind: KernelProjectionKind, pub title: Option, pub depth: u32, pub position: Option, pub child_count: u32, pub expandable: bool, pub expanded_by_default: bool, #[serde(default)] pub capabilities: Vec, pub resource_meta: Option, pub icon_hint: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub struct KernelProjectionResourceMeta { pub resource_kind: Option, pub document_id: Option, pub asset_id: Option, pub workspace_id: Option, pub asset_kind: Option, pub icon_hint: Option, #[serde(default)] pub extra: BTreeMap, } #[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, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub meta: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DocumentReadNodeType { Page, Section, ContentNode, ReferenceAnchor, Mindmap, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadNodeMeta { pub title: Option, pub text_snippet: Option, pub block_type: Option, pub heading_level: Option, pub numbering: Option, pub child_count: u32, pub order: u32, #[serde(default)] pub path: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadNode { pub id: String, pub parent_node_id: Option, pub node_type: DocumentReadNodeType, pub block_id: Option, pub anchor_block_id: Option, pub depth: u32, pub metadata: DocumentReadNodeMeta, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadSubtree { pub root_node_id: String, pub nodes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadOutlineEntry { pub id: String, pub node_id: String, pub anchor_block_id: Option, pub title: String, pub level: u32, pub numbering: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum DocumentReadEvidenceKind { Page, Heading, Paragraph, List, Todo, Quote, Code, Media, Reference, Table, Mindmap, Text, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadEvidenceItem { pub id: String, pub node_id: String, pub block_id: Option, pub kind: DocumentReadEvidenceKind, pub snippet: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadStats { pub block_count: u32, pub heading_count: u32, pub evidence_count: u32, pub max_depth: u32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentReadPageSubtree { pub projection_id: String, pub projection: String, pub root_node_id: String, pub root_node: DocumentReadNode, pub subtree: DocumentReadSubtree, #[serde(default)] pub outline: Vec, #[serde(default)] pub evidence: Vec, pub stats: DocumentReadStats, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DocumentContentResult { pub content: Value, pub revision: u64, pub conflict_detection_key: String, pub title: Option, pub page_subtree: DocumentReadPageSubtree, } #[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], query: Some("预算".into()), max_results: Some(20), 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")); assert_eq!(value["filters"]["query"], json!("预算")); assert_eq!(value["filters"]["maxResults"], json!(20)); 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")); } #[test] fn kernel_projection_item_serializes_contract_fields() { let item = KernelProjectionItem { row_id: "asset:page_1".into(), row_kind: KernelProjectionRowKind::Asset, node_id: "page_1".into(), parent_node_id: None, node_type: KernelNodeType::Page, projection_kind: KernelProjectionKind::FileTree, title: Some("页面 1".into()), depth: 0, position: Some(1), child_count: 2, expandable: true, expanded_by_default: true, capabilities: vec![ KernelProjectionCapability::Expand, KernelProjectionCapability::Open, KernelProjectionCapability::CreateChild, ], resource_meta: Some(KernelProjectionResourceMeta { resource_kind: Some(KernelProjectionResourceKind::Document), document_id: Some("page_1".into()), asset_id: None, workspace_id: Some("ws_1".into()), asset_kind: Some(KernelProjectionAssetKind::File), icon_hint: Some("page".into()), extra: BTreeMap::new(), }), icon_hint: Some("page".into()), }; let value = serde_json::to_value(&item).expect("projection item 应可序列化"); assert_eq!(value["rowId"], json!("asset:page_1")); assert_eq!(value["rowKind"], json!("asset")); assert_eq!(value["projectionKind"], json!("file_tree")); assert_eq!( value["capabilities"], json!(["expand", "open", "create-child"]) ); assert_eq!(value["resourceMeta"]["resourceKind"], json!("document")); assert_eq!(value["iconHint"], json!("page")); } #[test] fn workspace_source_serializes_minimal_command_source_contract() { let source = WorkspaceSource { source_kind: WorkspaceSourceKind::ConvexWorkspace, root_uri: "convex://workspace/ws_1".into(), workspace_id: "ws_1".into(), capabilities: vec![ WorkspaceSourceCapability::LoadSnapshot, WorkspaceSourceCapability::PreflightCommand, WorkspaceSourceCapability::ExecuteCommand, ], }; let value = serde_json::to_value(&source).expect("workspace source 应可序列化"); assert_eq!(value["sourceKind"], json!("convex_workspace")); assert_eq!(value["rootUri"], json!("convex://workspace/ws_1")); assert_eq!(value["workspaceId"], json!("ws_1")); assert_eq!( value["capabilities"], json!(["load-snapshot", "preflight-command", "execute-command"]) ); let decoded: WorkspaceSource = serde_json::from_value(value).expect("workspace source 应可反序列化"); assert_eq!(decoded, source); } }