use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PerspectiveMode {
Anchored,
Local,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RouteFamily {
Structural,
Semantic,
Temporal,
Causal,
Ghost,
Hole,
Resonant,
}
impl RouteFamily {
pub fn ordinal(&self) -> u8 {
match self {
Self::Structural => 0,
Self::Semantic => 1,
Self::Temporal => 2,
Self::Causal => 3,
Self::Ghost => 4,
Self::Hole => 5,
Self::Resonant => 6,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WatchTrigger {
Ingest,
Learn,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WatchStrategy {
Manual,
OnIngest,
OnLearn,
Periodic, }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LockScope {
Node,
Subgraph,
QueryNeighborhood,
Path,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerspectiveLens {
#[serde(default = "default_dimensions")]
pub dimensions: Vec<String>,
#[serde(default)]
pub route_families: Vec<RouteFamily>,
#[serde(default = "default_true")]
pub xlr: bool,
#[serde(default = "default_true")]
pub include_ghost_edges: bool,
#[serde(default = "default_true")]
pub include_structural_holes: bool,
#[serde(default = "default_perspective_top_k")]
pub top_k: u32,
#[serde(default)]
pub namespaces: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub node_types: Vec<String>,
#[serde(default)]
pub ranking_weights: Option<RankingWeights>,
}
impl Default for PerspectiveLens {
fn default() -> Self {
Self {
dimensions: default_dimensions(),
route_families: Vec::new(),
xlr: true,
include_ghost_edges: true,
include_structural_holes: true,
top_k: 8,
namespaces: Vec::new(),
tags: Vec::new(),
node_types: Vec::new(),
ranking_weights: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RankingWeights {
pub local_activation: f32,
pub path_coherence: f32,
pub novelty: f32,
pub anchor_relevance: f32,
pub continuity: f32,
}
impl Default for RankingWeights {
fn default() -> Self {
Self {
local_activation: 0.35,
path_coherence: 0.25,
novelty: 0.15,
anchor_relevance: 0.15,
continuity: 0.10,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModeContext {
pub mode: PerspectiveMode,
pub anchor_node: Option<String>,
pub anchor_query: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NavigationEvent {
pub action: String, #[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>, pub timestamp_ms: u64,
pub route_set_version: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerspectiveCheckpoint {
pub focus_node: Option<String>,
pub lens: PerspectiveLens,
pub mode: PerspectiveMode,
pub route_set_version: u64,
pub timestamp_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Route {
pub route_id: String,
pub route_index: u32,
pub family: RouteFamily,
pub target_node: String,
pub target_label: String,
pub reason: String,
pub score: f32,
#[serde(default)]
pub peek_available: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub provenance: Option<RouteProvenance>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RouteProvenance {
#[serde(skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_start: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_end: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CachedRouteSet {
pub routes: Vec<Route>,
pub total_routes: usize,
pub page_size: u32,
pub version: u64, pub synthesis_elapsed_ms: f64,
pub captured_cache_generation: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Diagnostic {
pub sources_checked: Vec<String>,
pub sources_with_results: Vec<String>,
pub sources_failed: Vec<String>,
pub reason: String, pub suggestion: String,
pub graph_stats: DiagnosticGraphStats,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiagnosticGraphStats {
pub node_count: u32,
pub edge_count: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AffinityCandidate {
pub candidate_node: String,
pub candidate_label: String,
pub kind: AffinityCandidateKind,
pub confidence: f32,
pub is_hypothetical: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub proposed_relation: Option<String>,
pub confidence_breakdown: ConfidenceBreakdown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AffinityCandidateKind {
HypothesizedLatentEdge,
MissingBridge,
ResonantNeighbor,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConfidenceBreakdown {
#[serde(skip_serializing_if = "Option::is_none")]
pub ghost_edge_strength: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub structural_hole_pressure: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resonant_amplitude: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantic_overlap: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provenance_overlap: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route_path_neighborhood: Option<f32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SuggestResult {
pub recommended_action: String,
pub confidence: f32,
pub why: String,
pub based_on: String, pub alternatives: Vec<SuggestAlternative>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SuggestAlternative {
pub action: String,
pub confidence: f32,
pub why: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerspectiveState {
pub perspective_id: String,
pub agent_id: String,
pub mode: PerspectiveMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_node: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub focus_node: Option<String>,
pub lens: PerspectiveLens,
pub entry_path: Vec<String>,
pub navigation_history: Vec<NavigationEvent>,
pub checkpoints: Vec<PerspectiveCheckpoint>,
pub visited_nodes: HashSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub route_cache: Option<CachedRouteSet>,
pub route_set_version: u64,
pub captured_cache_generation: u64,
pub stale: bool,
pub created_at_ms: u64,
pub last_accessed_ms: u64,
pub branches: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockScopeConfig {
pub scope_type: LockScope,
pub root_nodes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub radius: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_nodes: Option<Vec<String>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockSnapshot {
pub nodes: HashSet<String>,
pub edges: HashMap<String, EdgeSnapshotEntry>,
pub graph_generation: u64,
pub captured_at_ms: u64,
pub key_format: String, }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EdgeSnapshotEntry {
pub source: String,
pub target: String,
pub relation: String,
pub weight: f32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WatchConfig {
pub strategy: WatchStrategy,
pub last_scan_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockState {
pub lock_id: String,
pub agent_id: String,
pub scope: LockScopeConfig,
pub baseline: LockSnapshot,
#[serde(skip_serializing_if = "Option::is_none")]
pub watcher: Option<WatchConfig>,
pub baseline_stale: bool,
pub created_at_ms: u64,
pub last_diff_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WatcherEvent {
pub lock_id: String,
pub trigger: WatchTrigger,
pub timestamp_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerspectiveLimits {
pub max_perspectives_per_agent: usize,
pub max_locks_per_agent: usize,
pub max_branches_per_agent: usize,
pub max_nav_events_per_perspective: usize,
pub max_checkpoints_per_perspective: usize,
pub max_route_set_snapshots: usize,
pub max_lock_baseline_nodes: usize,
pub max_lock_baseline_edges: usize,
pub max_lock_subgraph_radius: u32,
pub max_affinity_candidates: usize,
pub max_inspect_chars: usize,
pub max_suggest_alternatives: usize,
pub max_compare_chars: usize,
pub max_lock_diff_new_nodes: usize,
pub max_lock_diff_new_edges: usize,
pub max_total_memory_bytes: usize,
}
impl Default for PerspectiveLimits {
fn default() -> Self {
Self {
max_perspectives_per_agent: 5,
max_locks_per_agent: 10,
max_branches_per_agent: 10,
max_nav_events_per_perspective: 1000,
max_checkpoints_per_perspective: 200,
max_route_set_snapshots: 10,
max_lock_baseline_nodes: 2000,
max_lock_baseline_edges: 10000,
max_lock_subgraph_radius: 4,
max_affinity_candidates: 8,
max_inspect_chars: 1500,
max_suggest_alternatives: 3,
max_compare_chars: 3000,
max_lock_diff_new_nodes: 50,
max_lock_diff_new_edges: 100,
max_total_memory_bytes: 50 * 1024 * 1024, }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeekContent {
pub content: String,
pub truncated: bool,
pub provenance_stale: bool,
pub encoding_lossy: bool,
pub relative_path: String,
pub line_start: u32,
pub line_end: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PeekSecurityConfig {
pub allow_roots: Vec<String>,
pub max_file_size: u64,
pub max_lines_before: u32,
pub max_lines_after: u32,
pub max_chars: usize,
}
impl Default for PeekSecurityConfig {
fn default() -> Self {
Self {
allow_roots: Vec::new(),
max_file_size: 10 * 1024 * 1024,
max_lines_before: 20,
max_lines_after: 30,
max_chars: 2000,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockDiffResult {
pub lock_id: String,
pub no_changes: bool,
pub new_nodes: Vec<String>,
pub removed_nodes: Vec<String>,
pub new_edges: Vec<String>, pub removed_edges: Vec<String>,
pub boundary_edges_added: Vec<String>,
pub boundary_edges_removed: Vec<String>,
pub weight_changes: Vec<EdgeWeightChange>,
pub baseline_stale: bool,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EdgeWeightChange {
pub edge_key: String,
pub old_weight: f32,
pub new_weight: f32,
}
fn default_dimensions() -> Vec<String> {
vec![
"structural".into(),
"semantic".into(),
"temporal".into(),
"causal".into(),
]
}
fn default_true() -> bool {
true
}
fn default_perspective_top_k() -> u32 {
8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perspective_lens_defaults_match_spec() {
let lens = PerspectiveLens::default();
assert_eq!(lens.dimensions.len(), 4);
assert!(lens.xlr);
assert!(lens.include_ghost_edges);
assert!(lens.include_structural_holes);
assert_eq!(lens.top_k, 8); assert!(lens.route_families.is_empty()); }
#[test]
fn ranking_weights_default_sum_to_one() {
let w = RankingWeights::default();
let sum =
w.local_activation + w.path_coherence + w.novelty + w.anchor_relevance + w.continuity;
assert!((sum - 1.0).abs() < 0.001);
}
#[test]
fn limits_defaults_match_synthesis() {
let l = PerspectiveLimits::default();
assert_eq!(l.max_perspectives_per_agent, 5);
assert_eq!(l.max_locks_per_agent, 10);
assert_eq!(l.max_lock_baseline_nodes, 2000);
assert_eq!(l.max_lock_subgraph_radius, 4);
assert_eq!(l.max_affinity_candidates, 8);
assert_eq!(l.max_total_memory_bytes, 50 * 1024 * 1024);
}
#[test]
fn perspective_state_serializes_round_trip() {
let state = PerspectiveState {
perspective_id: "persp_jimi_001".into(),
agent_id: "jimi".into(),
mode: PerspectiveMode::Anchored,
anchor_node: Some("session.rs".into()),
anchor_query: Some("session management".into()),
focus_node: Some("session.rs".into()),
lens: PerspectiveLens::default(),
entry_path: vec!["session.rs".into()],
navigation_history: Vec::new(),
checkpoints: Vec::new(),
visited_nodes: HashSet::new(),
route_cache: None,
route_set_version: 1710000000000,
captured_cache_generation: 0,
stale: false,
created_at_ms: 1710000000000,
last_accessed_ms: 1710000000000,
branches: Vec::new(),
};
let json = serde_json::to_string(&state).unwrap();
let deserialized: PerspectiveState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.perspective_id, "persp_jimi_001");
assert_eq!(deserialized.mode, PerspectiveMode::Anchored);
}
#[test]
fn lock_state_serializes_round_trip() {
let lock = LockState {
lock_id: "lock_jimi_001".into(),
agent_id: "jimi".into(),
scope: LockScopeConfig {
scope_type: LockScope::Node,
root_nodes: vec!["session.rs".into()],
radius: None,
query: None,
path_nodes: None,
},
baseline: LockSnapshot {
nodes: HashSet::new(),
edges: HashMap::new(),
graph_generation: 0,
captured_at_ms: 1710000000000,
key_format: "v1_content_addr".into(),
},
watcher: None,
baseline_stale: false,
created_at_ms: 1710000000000,
last_diff_ms: 1710000000000,
};
let json = serde_json::to_string(&lock).unwrap();
let deserialized: LockState = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.lock_id, "lock_jimi_001");
assert_eq!(deserialized.scope.scope_type, LockScope::Node);
}
#[test]
fn route_family_ordinal_is_stable() {
assert_eq!(RouteFamily::Structural.ordinal(), 0);
assert_eq!(RouteFamily::Resonant.ordinal(), 6);
}
#[test]
fn affinity_candidate_epistemic_guard() {
let c = AffinityCandidate {
candidate_node: "foo".into(),
candidate_label: "Foo".into(),
kind: AffinityCandidateKind::HypothesizedLatentEdge,
confidence: 0.42,
is_hypothetical: true,
proposed_relation: None,
confidence_breakdown: ConfidenceBreakdown {
ghost_edge_strength: Some(0.6),
structural_hole_pressure: None,
resonant_amplitude: None,
semantic_overlap: Some(0.3),
provenance_overlap: None,
route_path_neighborhood: None,
},
};
assert!(c.is_hypothetical);
assert!(c.confidence <= 0.85);
assert!(c.proposed_relation.is_none()); }
}