use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CrossFileEdgeType {
Imports,
Calls,
Registers,
Configures,
Tests,
Inherits,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SeekInput {
pub query: String,
pub agent_id: String,
#[serde(default = "default_top_k")]
pub top_k: usize,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub node_types: Vec<String>,
#[serde(default = "default_min_score")]
pub min_score: f32,
#[serde(default = "default_true")]
pub graph_rerank: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct SeekOutput {
pub query: String,
pub results: Vec<SeekResultEntry>,
pub total_candidates_scanned: usize,
pub embeddings_used: bool,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct SeekResultEntry {
pub node_id: String,
pub label: String,
#[serde(rename = "type")]
pub node_type: String,
pub score: f32,
pub score_breakdown: SeekScoreBreakdown,
pub intent_summary: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excerpt: Option<String>,
pub connections: Vec<SeekConnection>,
}
#[derive(Clone, Debug, Serialize)]
pub struct SeekScoreBreakdown {
pub embedding_similarity: f32,
pub graph_activation: f32,
pub temporal_recency: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct SeekConnection {
pub node_id: String,
pub label: String,
pub relation: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ScanInput {
pub pattern: String,
pub agent_id: String,
#[serde(default)]
pub scope: Option<String>,
#[serde(default = "default_severity_min")]
pub severity_min: f32,
#[serde(default = "default_true")]
pub graph_validate: bool,
#[serde(default = "default_scan_limit")]
pub limit: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct ScanOutput {
pub pattern: String,
pub findings: Vec<ScanFinding>,
pub files_scanned: usize,
pub total_matches_raw: usize,
pub total_matches_validated: usize,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct ScanFinding {
pub pattern: String,
pub status: String,
pub severity: f32,
pub node_id: String,
pub label: String,
pub file_path: String,
pub line: u32,
pub message: String,
pub graph_context: Vec<ScanContextNode>,
}
#[derive(Clone, Debug, Serialize)]
pub struct ScanContextNode {
pub node_id: String,
pub label: String,
pub relation: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TimelineInput {
pub node: String,
pub agent_id: String,
#[serde(default = "default_depth_30d")]
pub depth: String,
#[serde(default = "default_true")]
pub include_co_changes: bool,
#[serde(default = "default_true")]
pub include_churn: bool,
#[serde(default = "default_top_k_10")]
pub top_k: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct TimelineOutput {
pub node: String,
pub depth: String,
pub changes: Vec<TimelineChange>,
pub co_changed_with: Vec<CoChangePartner>,
pub velocity: String,
pub stability_score: f32,
pub pattern: String,
pub total_churn: ChurnSummary,
pub commit_count_in_window: usize,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct TimelineChange {
pub date: String,
pub commit: String,
pub author: String,
pub delta: String,
pub co_changed: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct CoChangePartner {
pub file: String,
pub times: u32,
pub coupling_degree: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct ChurnSummary {
pub lines_added: u32,
pub lines_deleted: u32,
}
#[derive(Clone, Debug, Deserialize)]
pub struct DivergeInput {
pub agent_id: String,
pub baseline: String,
#[serde(default)]
pub scope: Option<String>,
#[serde(default = "default_true")]
pub include_coupling_changes: bool,
#[serde(default = "default_true")]
pub include_anomalies: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct DivergeOutput {
pub baseline: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
pub structural_drift: f32,
pub new_nodes: Vec<String>,
pub removed_nodes: Vec<String>,
pub modified_nodes: Vec<DivergeModifiedNode>,
pub coupling_changes: Vec<CouplingChange>,
pub anomalies: Vec<DivergeAnomaly>,
pub summary: String,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct DivergeModifiedNode {
pub file: String,
pub delta: String,
pub growth_ratio: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct CouplingChange {
pub pair: [String; 2],
pub was: f32,
pub now: f32,
pub direction: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct DivergeAnomaly {
#[serde(rename = "type")]
pub anomaly_type: String,
pub file: String,
pub detail: String,
pub severity: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailSaveInput {
pub agent_id: String,
pub label: String,
#[serde(default)]
pub hypotheses: Vec<TrailHypothesisInput>,
#[serde(default)]
pub conclusions: Vec<TrailConclusionInput>,
#[serde(default)]
pub open_questions: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub visited_nodes: Vec<TrailVisitedNodeInput>,
#[serde(default)]
pub activation_boosts: HashMap<String, f32>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailHypothesisInput {
pub statement: String,
#[serde(default = "default_confidence")]
pub confidence: f32,
#[serde(default)]
pub supporting_nodes: Vec<String>,
#[serde(default)]
pub contradicting_nodes: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailConclusionInput {
pub statement: String,
#[serde(default = "default_confidence")]
pub confidence: f32,
#[serde(default)]
pub from_hypotheses: Vec<String>,
#[serde(default)]
pub supporting_nodes: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailVisitedNodeInput {
pub node_external_id: String,
#[serde(default)]
pub annotation: Option<String>,
#[serde(default = "default_relevance")]
pub relevance: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailSaveOutput {
pub trail_id: String,
pub label: String,
pub agent_id: String,
pub nodes_saved: usize,
pub hypotheses_saved: usize,
pub conclusions_saved: usize,
pub open_questions_saved: usize,
pub graph_generation_at_creation: u64,
pub created_at_ms: u64,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailResumeInput {
pub agent_id: String,
pub trail_id: String,
#[serde(default)]
pub force: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailResumeOutput {
pub trail_id: String,
pub label: String,
pub stale: bool,
pub generations_behind: u64,
pub missing_nodes: Vec<String>,
pub nodes_reactivated: usize,
pub hypotheses_downgraded: Vec<String>,
pub trail: TrailSummaryOutput,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailSummaryOutput {
pub trail_id: String,
pub agent_id: String,
pub label: String,
pub status: String,
pub created_at_ms: u64,
pub last_modified_ms: u64,
pub node_count: usize,
pub hypothesis_count: usize,
pub conclusion_count: usize,
pub open_question_count: usize,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailMergeInput {
pub agent_id: String,
pub trail_ids: Vec<String>,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailMergeOutput {
pub merged_trail_id: String,
pub label: String,
pub source_trails: Vec<String>,
pub nodes_merged: usize,
pub hypotheses_merged: usize,
pub conflicts: Vec<TrailMergeConflict>,
pub connections_discovered: Vec<TrailConnection>,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailMergeConflict {
pub hypothesis_a: String,
pub hypothesis_b: String,
pub resolution: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub winner: Option<String>,
pub score_delta: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailConnection {
#[serde(rename = "type")]
pub connection_type: String,
pub detail: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_node: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_node: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f32>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TrailListInput {
pub agent_id: String,
#[serde(default)]
pub filter_agent_id: Option<String>,
#[serde(default)]
pub filter_status: Option<String>,
#[serde(default)]
pub filter_tags: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrailListOutput {
pub trails: Vec<TrailSummaryOutput>,
pub total_count: usize,
}
#[derive(Clone, Debug, Deserialize)]
pub struct HypothesizeInput {
pub claim: String,
pub agent_id: String,
#[serde(default = "default_max_hops")]
pub max_hops: u8,
#[serde(default = "default_true")]
pub include_ghost_edges: bool,
#[serde(default = "default_true")]
pub include_partial_flow: bool,
#[serde(default = "default_path_budget")]
pub path_budget: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct HypothesizeOutput {
pub claim: String,
pub claim_type: String,
pub subject_nodes: Vec<String>,
pub object_nodes: Vec<String>,
pub verdict: String,
pub confidence: f32,
pub supporting_evidence: Vec<HypothesisEvidence>,
pub contradicting_evidence: Vec<HypothesisEvidence>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial_reach: Option<Vec<PartialReachEntry>>,
pub paths_explored: usize,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct HypothesisEvidence {
#[serde(rename = "type")]
pub evidence_type: String,
pub description: String,
pub likelihood_factor: f32,
pub nodes: Vec<String>,
#[serde(default)]
pub relations: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_weight: Option<f32>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PartialReachEntry {
pub node_id: String,
pub label: String,
pub hops_from_source: u8,
pub activation_at_stop: f32,
}
#[derive(Clone, Debug, Deserialize)]
pub struct DifferentialInput {
pub agent_id: String,
pub snapshot_a: String,
pub snapshot_b: String,
#[serde(default)]
pub question: Option<String>,
#[serde(default)]
pub focus_nodes: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct DifferentialOutput {
pub snapshot_a: String,
pub snapshot_b: String,
pub new_edges: Vec<DiffEdgeDelta>,
pub removed_edges: Vec<DiffEdgeDelta>,
pub weight_changes: Vec<DiffWeightDelta>,
pub new_nodes: Vec<String>,
pub removed_nodes: Vec<String>,
pub coupling_deltas: Vec<DiffCouplingDelta>,
pub summary: String,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct DiffEdgeDelta {
pub source: String,
pub target: String,
pub relation: String,
pub weight: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct DiffWeightDelta {
pub source: String,
pub target: String,
pub relation: String,
pub old_weight: f32,
pub new_weight: f32,
pub delta: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct DiffCouplingDelta {
pub community_a: String,
pub community_b: String,
pub old_coupling: f32,
pub new_coupling: f32,
pub delta: f32,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TraceInput {
pub error_text: String,
pub agent_id: String,
#[serde(default)]
pub language: Option<String>,
#[serde(default = "default_window_hours")]
pub window_hours: f32,
#[serde(default = "default_top_k_10")]
pub top_k: usize,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceOutput {
pub language_detected: String,
pub error_type: String,
pub error_message: String,
pub frames_parsed: usize,
pub frames_mapped: usize,
pub suspects: Vec<TraceSuspect>,
pub co_change_suspects: Vec<TraceCoChangeSuspect>,
pub causal_chain: Vec<String>,
pub fix_scope: TraceFixScope,
pub unmapped_frames: Vec<TraceUnmappedFrame>,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceSuspect {
pub node_id: String,
pub label: String,
#[serde(rename = "type")]
pub node_type: String,
pub suspiciousness: f32,
pub signals: TraceSuspiciousnessSignals,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_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>,
pub related_callers: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceSuspiciousnessSignals {
pub trace_depth_score: f32,
pub recency_score: f32,
pub centrality_score: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceCoChangeSuspect {
pub node_id: String,
pub label: String,
pub modified_at: f64,
pub reason: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceFixScope {
pub files_to_inspect: Vec<String>,
pub estimated_blast_radius: usize,
pub risk_level: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct TraceUnmappedFrame {
pub file: String,
pub line: u32,
pub function: String,
pub reason: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ValidatePlanInput {
pub agent_id: String,
pub actions: Vec<PlannedAction>,
#[serde(default = "default_true")]
pub include_test_impact: bool,
#[serde(default = "default_true")]
pub include_risk_score: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PlannedAction {
pub action_type: String,
pub file_path: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub depends_on: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct ValidatePlanOutput {
pub actions_analyzed: usize,
pub actions_resolved: usize,
pub actions_unresolved: usize,
pub gaps: Vec<PlanGap>,
pub risk_score: f32,
pub risk_level: String,
pub test_coverage: PlanTestCoverage,
pub suggested_additions: Vec<PlanSuggestedAction>,
pub blast_radius_total: usize,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct PlanGap {
pub file_path: String,
pub node_id: String,
pub reason: String,
pub severity: String,
pub signal_strength: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct PlanTestCoverage {
pub modified_files: usize,
pub tested_files: usize,
pub untested_files: Vec<String>,
pub coverage_ratio: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct PlanSuggestedAction {
pub action_type: String,
pub file_path: String,
pub reason: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct FederateInput {
pub agent_id: String,
pub repos: Vec<FederateRepo>,
#[serde(default = "default_true")]
pub detect_cross_repo_edges: bool,
#[serde(default)]
pub incremental: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct FederateRepo {
pub name: String,
pub path: String,
#[serde(default = "default_adapter")]
pub adapter: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct FederateOutput {
pub repos_ingested: Vec<FederateRepoResult>,
pub total_nodes: u32,
pub total_edges: u64,
pub cross_repo_edges: Vec<FederateCrossRepoEdge>,
pub cross_repo_edge_count: usize,
pub incremental: bool,
pub skipped_repos: Vec<String>,
pub elapsed_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct FederateRepoResult {
pub name: String,
pub path: String,
pub node_count: u32,
pub edge_count: u32,
pub from_cache: bool,
pub ingest_ms: f64,
}
#[derive(Clone, Debug, Serialize)]
pub struct FederateCrossRepoEdge {
pub source_repo: String,
pub target_repo: String,
pub source_node: String,
pub target_node: String,
pub edge_type: String,
pub relation: String,
pub weight: f32,
pub causal_strength: f32,
}
fn default_top_k() -> usize {
20
}
fn default_top_k_10() -> usize {
10
}
fn default_true() -> bool {
true
}
fn default_max_hops() -> u8 {
5
}
fn default_min_score() -> f32 {
0.1
}
fn default_severity_min() -> f32 {
0.3
}
fn default_scan_limit() -> usize {
50
}
fn default_depth_30d() -> String {
"30d".into()
}
fn default_confidence() -> f32 {
0.5
}
fn default_relevance() -> f32 {
0.5
}
fn default_path_budget() -> usize {
1000
}
fn default_window_hours() -> f32 {
24.0
}
fn default_adapter() -> String {
"code".into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seek_input_deserializes_minimal() {
let json = r#"{"query": "find auth code", "agent_id": "jimi"}"#;
let input: SeekInput = serde_json::from_str(json).unwrap();
assert_eq!(input.query, "find auth code");
assert_eq!(input.agent_id, "jimi");
assert_eq!(input.top_k, 20);
assert!(input.scope.is_none());
assert!(input.node_types.is_empty());
assert!((input.min_score - 0.1).abs() < f32::EPSILON);
assert!(input.graph_rerank);
}
#[test]
fn scan_input_defaults() {
let json = r#"{"pattern": "error_handling", "agent_id": "jimi"}"#;
let input: ScanInput = serde_json::from_str(json).unwrap();
assert_eq!(input.pattern, "error_handling");
assert!((input.severity_min - 0.3).abs() < f32::EPSILON);
assert!(input.graph_validate);
assert_eq!(input.limit, 50);
}
#[test]
fn timeline_input_deserializes_minimal() {
let json = r#"{"node": "file::backend/config.py", "agent_id": "jimi"}"#;
let input: TimelineInput = serde_json::from_str(json).unwrap();
assert_eq!(input.node, "file::backend/config.py");
assert_eq!(input.depth, "30d");
assert!(input.include_co_changes);
assert!(input.include_churn);
assert_eq!(input.top_k, 10);
}
#[test]
fn diverge_input_with_scope() {
let json = r#"{
"agent_id": "jimi",
"baseline": "2026-03-01",
"scope": "backend/stormender*"
}"#;
let input: DivergeInput = serde_json::from_str(json).unwrap();
assert_eq!(input.baseline, "2026-03-01");
assert_eq!(input.scope.as_deref(), Some("backend/stormender*"));
assert!(input.include_coupling_changes);
assert!(input.include_anomalies);
}
#[test]
fn trail_save_input_minimal() {
let json = r#"{"agent_id": "jimi", "label": "race condition investigation"}"#;
let input: TrailSaveInput = serde_json::from_str(json).unwrap();
assert_eq!(input.label, "race condition investigation");
assert!(input.hypotheses.is_empty());
assert!(input.conclusions.is_empty());
assert!(input.open_questions.is_empty());
assert!(input.tags.is_empty());
}
#[test]
fn trail_resume_input_defaults() {
let json = r#"{"agent_id": "jimi", "trail_id": "trail_jimi_001_abc"}"#;
let input: TrailResumeInput = serde_json::from_str(json).unwrap();
assert_eq!(input.trail_id, "trail_jimi_001_abc");
assert!(!input.force);
}
#[test]
fn trail_merge_input_two_trails() {
let json = r#"{
"agent_id": "jimi",
"trail_ids": ["trail_a", "trail_b"]
}"#;
let input: TrailMergeInput = serde_json::from_str(json).unwrap();
assert_eq!(input.trail_ids.len(), 2);
assert!(input.label.is_none());
}
#[test]
fn trail_list_input_with_filters() {
let json = r#"{
"agent_id": "jimi",
"filter_status": "saved",
"filter_tags": ["stormender", "concurrency"]
}"#;
let input: TrailListInput = serde_json::from_str(json).unwrap();
assert_eq!(input.filter_status.as_deref(), Some("saved"));
assert_eq!(input.filter_tags.len(), 2);
}
#[test]
fn hypothesize_input_minimal() {
let json = r#"{
"claim": "handler never validates session tokens",
"agent_id": "jimi"
}"#;
let input: HypothesizeInput = serde_json::from_str(json).unwrap();
assert_eq!(input.claim, "handler never validates session tokens");
assert_eq!(input.max_hops, 5);
assert!(input.include_ghost_edges);
assert!(input.include_partial_flow);
assert_eq!(input.path_budget, 1000);
}
#[test]
fn differential_input_minimal() {
let json = r#"{
"agent_id": "jimi",
"snapshot_a": "before.json",
"snapshot_b": "current"
}"#;
let input: DifferentialInput = serde_json::from_str(json).unwrap();
assert_eq!(input.snapshot_a, "before.json");
assert_eq!(input.snapshot_b, "current");
assert!(input.question.is_none());
assert!(input.focus_nodes.is_empty());
}
#[test]
fn trace_input_minimal() {
let json = r#"{
"error_text": "Traceback (most recent call last):\n File \"test.py\", line 1\nTypeError: bad",
"agent_id": "jimi"
}"#;
let input: TraceInput = serde_json::from_str(json).unwrap();
assert!(input.language.is_none());
assert!((input.window_hours - 24.0).abs() < f32::EPSILON);
assert_eq!(input.top_k, 10);
}
#[test]
fn validate_plan_input_with_actions() {
let json = r#"{
"agent_id": "jimi",
"actions": [
{"action_type": "modify", "file_path": "backend/config.py"},
{"action_type": "test", "file_path": "backend/tests/test_config.py"}
]
}"#;
let input: ValidatePlanInput = serde_json::from_str(json).unwrap();
assert_eq!(input.actions.len(), 2);
assert!(input.include_test_impact);
assert!(input.include_risk_score);
assert_eq!(input.actions[0].action_type, "modify");
assert!(input.actions[0].depends_on.is_empty());
}
#[test]
fn federate_input_minimal() {
let json = r#"{
"agent_id": "jimi",
"repos": [
{"name": "my-project", "path": "/tmp/my-project"},
{"name": "my-library", "path": "/tmp/my-library"}
]
}"#;
let input: FederateInput = serde_json::from_str(json).unwrap();
assert_eq!(input.repos.len(), 2);
assert!(input.detect_cross_repo_edges);
assert!(!input.incremental);
assert_eq!(input.repos[0].name, "my-project");
assert_eq!(input.repos[1].adapter, "code");
}
}