1use petgraph::stable_graph::{NodeIndex, StableDiGraph};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant, SystemTime};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum GitChangeKind {
17 Modified,
19 Added,
21 Deleted,
23 RenamedFrom,
25 RenamedTo,
27 Untracked,
29}
30
31impl GitChangeKind {
32 pub fn label(&self) -> &'static str {
34 match self {
35 GitChangeKind::Modified => "Modified",
36 GitChangeKind::Added => "Added",
37 GitChangeKind::Deleted => "Deleted",
38 GitChangeKind::RenamedFrom => "Renamed (from)",
39 GitChangeKind::RenamedTo => "Renamed (to)",
40 GitChangeKind::Untracked => "Untracked",
41 }
42 }
43
44 pub fn symbol(&self) -> &'static str {
46 match self {
47 GitChangeKind::Modified => "M",
48 GitChangeKind::Added => "+",
49 GitChangeKind::Deleted => "-",
50 GitChangeKind::RenamedFrom => "R←",
51 GitChangeKind::RenamedTo => "R→",
52 GitChangeKind::Untracked => "?",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct GitFileChange {
60 pub path: PathBuf,
62 pub kind: GitChangeKind,
64 pub staged: bool,
66}
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct GitChangeSnapshot {
71 pub changes: Vec<GitFileChange>,
73 #[serde(skip)]
75 pub captured_at: Option<Instant>,
76}
77
78impl GitChangeSnapshot {
79 pub fn new() -> Self {
81 Self {
82 changes: Vec::new(),
83 captured_at: Some(Instant::now()),
84 }
85 }
86
87 pub fn has_changes(&self, path: &Path) -> bool {
89 self.changes.iter().any(|c| c.path == path)
90 }
91
92 pub fn get_change(&self, path: &Path) -> Option<&GitFileChange> {
94 self.changes.iter().find(|c| c.path == path)
95 }
96
97 pub fn changed_paths(&self) -> impl Iterator<Item = &Path> {
99 self.changes.iter().map(|c| c.path.as_path())
100 }
101
102 pub fn count_by_kind(&self, kind: GitChangeKind) -> usize {
104 self.changes.iter().filter(|c| c.kind == kind).count()
105 }
106
107 pub fn is_stale(&self, max_age: Duration) -> bool {
109 match self.captured_at {
110 Some(at) => at.elapsed() > max_age,
111 None => true,
112 }
113 }
114
115 pub fn age(&self) -> Option<Duration> {
117 self.captured_at.map(|at| at.elapsed())
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct ChangeIndicatorState {
124 pub phase: f32,
126 pub speed: f32,
128 pub enabled: bool,
130}
131
132impl Default for ChangeIndicatorState {
133 fn default() -> Self {
134 Self {
135 phase: 0.0,
136 speed: 1.0,
137 enabled: true,
138 }
139 }
140}
141
142impl ChangeIndicatorState {
143 pub fn tick(&mut self, dt: f32) {
145 if self.enabled {
146 self.phase = (self.phase + dt * self.speed) % 1.0;
147 }
148 }
149
150 pub fn pulse_scale(&self) -> f32 {
152 let t = self.phase * std::f32::consts::TAU;
154 1.0 + 0.15 * (t.sin() * 0.5 + 0.5)
155 }
156
157 pub fn ring_alpha(&self) -> f32 {
159 let t = self.phase * std::f32::consts::TAU;
160 0.3 + 0.4 * (t.sin() * 0.5 + 0.5)
161 }
162}
163
164#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct NodeId(pub u64);
167
168#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
170pub struct EdgeId(pub u64);
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
174pub enum GraphNodeKind {
175 Module,
177 File,
179 Directory,
181 Service,
183 Test,
185 #[default]
187 Other,
188}
189
190#[derive(Debug, Default, Clone, Serialize, Deserialize)]
192pub struct GraphNode {
193 pub id: NodeId,
195 pub name: String,
197 pub kind: GraphNodeKind,
199 pub metadata: HashMap<String, String>,
201}
202
203#[derive(Debug, Default, Clone, Serialize, Deserialize)]
205pub struct GraphEdge {
206 pub id: EdgeId,
208 pub from: NodeId,
210 pub to: NodeId,
212 pub relationship: String,
214 pub metadata: HashMap<String, String>,
216}
217
218#[derive(Debug, Default, Clone, Serialize, Deserialize)]
220pub struct SourceCodeGraph {
221 pub nodes: Vec<GraphNode>,
223 pub edges: Vec<GraphEdge>,
225 pub metadata: HashMap<String, String>,
227}
228
229impl SourceCodeGraph {
230 pub fn empty() -> Self {
232 Self::default()
233 }
234
235 pub fn node_count(&self) -> usize {
237 self.nodes.len()
238 }
239
240 pub fn edge_count(&self) -> usize {
242 self.edges.len()
243 }
244
245 pub fn to_petgraph(&self) -> (StableDiGraph<GraphNode, String>, HashMap<NodeId, NodeIndex>) {
248 let mut graph = StableDiGraph::new();
249 let mut id_to_index = HashMap::new();
250
251 for node in &self.nodes {
253 let idx = graph.add_node(node.clone());
254 id_to_index.insert(node.id, idx);
255 }
256
257 for edge in &self.edges {
259 if let (Some(&from_idx), Some(&to_idx)) =
260 (id_to_index.get(&edge.from), id_to_index.get(&edge.to))
261 {
262 graph.add_edge(from_idx, to_idx, edge.relationship.clone());
263 }
264 }
265
266 (graph, id_to_index)
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272pub enum ReferenceKind {
273 Uses,
275 Imports,
277 Implements,
279 Contains,
281}
282
283impl std::fmt::Display for ReferenceKind {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 match self {
286 ReferenceKind::Uses => write!(f, "uses"),
287 ReferenceKind::Imports => write!(f, "imports"),
288 ReferenceKind::Implements => write!(f, "implements"),
289 ReferenceKind::Contains => write!(f, "contains"),
290 }
291 }
292}
293
294#[derive(Debug, Clone)]
296pub struct SourceReference {
297 pub source_path: PathBuf,
299 pub kind: ReferenceKind,
301 pub target_route: PathBuf,
303}
304
305#[derive(Debug, Default)]
307pub struct SourceCodeGraphBuilder {
308 nodes: Vec<GraphNode>,
309 edges: Vec<GraphEdge>,
310 path_to_node: HashMap<PathBuf, NodeId>,
311 next_node_id: u64,
312 next_edge_id: u64,
313 metadata: HashMap<String, String>,
314}
315
316impl SourceCodeGraphBuilder {
317 pub fn new() -> Self {
319 Self::default()
320 }
321
322 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
324 self.metadata.insert(key.into(), value.into());
325 self
326 }
327
328 pub fn add_directory(&mut self, path: &Path) -> NodeId {
330 if let Some(&id) = self.path_to_node.get(path) {
331 return id;
332 }
333
334 let id = NodeId(self.next_node_id);
335 self.next_node_id += 1;
336
337 let name = path
338 .file_name()
339 .and_then(|n| n.to_str())
340 .unwrap_or_else(|| path.to_str().unwrap_or("."))
341 .to_string();
342
343 let mut metadata = HashMap::new();
344 metadata.insert("path".to_string(), path.to_string_lossy().to_string());
345
346 self.nodes.push(GraphNode {
347 id,
348 name,
349 kind: GraphNodeKind::Directory,
350 metadata,
351 });
352
353 self.path_to_node.insert(path.to_path_buf(), id);
354 id
355 }
356
357 pub fn add_file(&mut self, path: &Path, relative_path: &str) -> NodeId {
359 if let Some(&id) = self.path_to_node.get(path) {
360 return id;
361 }
362
363 let id = NodeId(self.next_node_id);
364 self.next_node_id += 1;
365
366 let name = path
367 .file_name()
368 .and_then(|n| n.to_str())
369 .unwrap_or_else(|| path.to_str().unwrap_or("unknown"))
370 .to_string();
371
372 let kind = match path.extension().and_then(|e| e.to_str()) {
374 Some("rs") | Some("py") | Some("js") | Some("ts") | Some("tsx") | Some("jsx")
375 | Some("go") | Some("java") | Some("c") | Some("cpp") | Some("h") | Some("hpp") => {
376 if relative_path.contains("test") || name.starts_with("test_") {
377 GraphNodeKind::Test
378 } else if name == "mod.rs"
379 || name == "__init__.py"
380 || name == "index.ts"
381 || name == "index.js"
382 {
383 GraphNodeKind::Module
384 } else {
385 GraphNodeKind::File
386 }
387 }
388 _ => GraphNodeKind::File,
389 };
390
391 let mut metadata = HashMap::new();
392 metadata.insert("path".to_string(), path.to_string_lossy().to_string());
393 metadata.insert("relative_path".to_string(), relative_path.to_string());
394
395 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
396 metadata.insert("extension".to_string(), ext.to_string());
397 metadata.insert(
398 "language".to_string(),
399 extension_to_language(ext).to_string(),
400 );
401 }
402
403 self.nodes.push(GraphNode {
404 id,
405 name,
406 kind,
407 metadata,
408 });
409
410 self.path_to_node.insert(path.to_path_buf(), id);
411 id
412 }
413
414 pub fn add_hierarchy_edge(&mut self, parent_path: &Path, child_path: &Path) {
416 if let (Some(&parent_id), Some(&child_id)) = (
417 self.path_to_node.get(parent_path),
418 self.path_to_node.get(child_path),
419 ) {
420 if parent_id != child_id {
421 self.add_edge(parent_id, child_id, ReferenceKind::Contains);
422 }
423 }
424 }
425
426 pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: ReferenceKind) {
428 let id = EdgeId(self.next_edge_id);
429 self.next_edge_id += 1;
430
431 self.edges.push(GraphEdge {
432 id,
433 from,
434 to,
435 relationship: kind.to_string(),
436 metadata: HashMap::new(),
437 });
438 }
439
440 pub fn get_node_id(&self, path: &Path) -> Option<NodeId> {
442 self.path_to_node.get(path).copied()
443 }
444
445 pub fn find_node_by_path_suffix(&self, route: &Path) -> Option<NodeId> {
447 let route_str = route.to_string_lossy();
448
449 for (path, &node_id) in &self.path_to_node {
450 let path_str = path.to_string_lossy();
451
452 if path_str.ends_with(route_str.as_ref()) {
454 return Some(node_id);
455 }
456
457 let normalized_path: String = path_str.trim_start_matches("./").replace('\\', "/");
459 let normalized_route: String = route_str.trim_start_matches("./").replace('\\', "/");
460 if normalized_path.ends_with(&normalized_route) {
461 return Some(node_id);
462 }
463
464 let route_parts: Vec<&str> = normalized_route.split('/').collect();
466 let path_parts: Vec<&str> = normalized_path.split('/').collect();
467 if route_parts.len() <= path_parts.len() {
468 for window in path_parts.windows(route_parts.len()) {
469 if window == route_parts.as_slice() {
470 return Some(node_id);
471 }
472 }
473 }
474
475 if let (Some(file_name), Some(route_name)) = (
477 path.file_name().and_then(|n| n.to_str()),
478 route.file_name().and_then(|n| n.to_str()),
479 ) {
480 if file_name == route_name {
481 return Some(node_id);
482 }
483 }
484 }
485
486 None
487 }
488
489 pub fn node_count(&self) -> usize {
491 self.nodes.len()
492 }
493
494 pub fn edge_count(&self) -> usize {
496 self.edges.len()
497 }
498
499 pub fn build(self) -> SourceCodeGraph {
501 SourceCodeGraph {
502 nodes: self.nodes,
503 edges: self.edges,
504 metadata: self.metadata,
505 }
506 }
507}
508
509pub fn detect_rust_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
511 let mut refs = Vec::new();
512
513 for line in content.lines() {
514 let trimmed = line.trim();
515
516 if !trimmed.starts_with("use ") {
517 continue;
518 }
519
520 let use_part = trimmed
522 .strip_prefix("use ")
523 .unwrap_or("")
524 .split(';')
525 .next()
526 .unwrap_or("")
527 .split('{')
528 .next()
529 .unwrap_or("")
530 .trim();
531
532 if use_part.is_empty() {
533 continue;
534 }
535
536 let is_local = use_part.starts_with("crate::")
538 || use_part.starts_with("self::")
539 || use_part.starts_with("super::");
540
541 if !is_local {
542 continue;
543 }
544
545 let module_path = use_part
546 .strip_prefix("crate::")
547 .or_else(|| use_part.strip_prefix("self::"))
548 .or_else(|| use_part.strip_prefix("super::"))
549 .unwrap_or(use_part);
550
551 let path_str = module_path
553 .replace("::", "/")
554 .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_')
555 .to_string();
556
557 refs.push(SourceReference {
558 source_path: source_path.to_path_buf(),
559 kind: ReferenceKind::Uses,
560 target_route: PathBuf::from(format!("{}.rs", path_str)),
561 });
562 }
563
564 refs
565}
566
567pub fn detect_python_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
569 let mut refs = Vec::new();
570
571 for line in content.lines() {
572 let trimmed = line.trim();
573
574 if trimmed.starts_with("import ") && !trimmed.starts_with("import(") {
576 let import_part = trimmed
577 .strip_prefix("import ")
578 .unwrap_or("")
579 .split_whitespace()
580 .next()
581 .unwrap_or("")
582 .split(',')
583 .next()
584 .unwrap_or("")
585 .trim();
586
587 if !import_part.is_empty() {
588 let path_str = import_part.replace('.', "/");
589 refs.push(SourceReference {
590 source_path: source_path.to_path_buf(),
591 kind: ReferenceKind::Imports,
592 target_route: PathBuf::from(format!("{}.py", path_str)),
593 });
594 }
595 }
596
597 if let Some(module_part) = trimmed
599 .strip_prefix("from ")
600 .and_then(|s| s.split(" import ").next())
601 {
602 let module = module_part.trim();
603 if !module.is_empty() && module != "." && !module.starts_with("..") {
604 let path_str = module.replace('.', "/");
605 refs.push(SourceReference {
606 source_path: source_path.to_path_buf(),
607 kind: ReferenceKind::Imports,
608 target_route: PathBuf::from(format!("{}.py", path_str)),
609 });
610 }
611 }
612 }
613
614 refs
615}
616
617pub fn detect_ts_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
619 let mut refs = Vec::new();
620
621 for line in content.lines() {
622 let trimmed = line.trim();
623
624 if trimmed.starts_with("import ") {
626 if let Some(path_start) = trimmed.find(['\'', '"']) {
628 let quote_char = trimmed.chars().nth(path_start).unwrap();
629 let rest = &trimmed[path_start + 1..];
630 if let Some(path_end) = rest.find(quote_char) {
631 let import_path = &rest[..path_end];
632 if import_path.starts_with('.') {
634 refs.push(SourceReference {
635 source_path: source_path.to_path_buf(),
636 kind: ReferenceKind::Imports,
637 target_route: PathBuf::from(import_path),
638 });
639 }
640 }
641 }
642 }
643 }
644
645 refs
646}
647
648pub fn detect_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
650 match source_path.extension().and_then(|e| e.to_str()) {
651 Some("rs") => detect_rust_references(content, source_path),
652 Some("py") => detect_python_references(content, source_path),
653 Some("ts") | Some("tsx") | Some("js") | Some("jsx") => {
654 detect_ts_references(content, source_path)
655 }
656 _ => Vec::new(),
657 }
658}
659
660fn extension_to_language(ext: &str) -> &'static str {
662 match ext {
663 "rs" => "rust",
664 "py" => "python",
665 "js" => "javascript",
666 "ts" => "typescript",
667 "tsx" => "typescript",
668 "jsx" => "javascript",
669 "go" => "go",
670 "java" => "java",
671 "c" | "h" => "c",
672 "cpp" | "hpp" | "cc" | "cxx" => "cpp",
673 "md" => "markdown",
674 "json" => "json",
675 "yaml" | "yml" => "yaml",
676 "toml" => "toml",
677 _ => "unknown",
678 }
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
683pub struct Vibe {
684 pub id: String,
686 pub title: String,
688 pub description: String,
690 pub targets: Vec<NodeId>,
692 pub created_by: String,
694 pub created_at: SystemTime,
696 pub metadata: HashMap<String, String>,
698}
699
700#[derive(Debug, Default, Clone, Serialize, Deserialize)]
702pub struct Constitution {
703 pub name: String,
705 pub version: String,
707 pub description: String,
709 pub policies: Vec<String>,
711}
712
713pub type StatePayload = Value;
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct CellState {
719 pub node_id: NodeId,
721 pub payload: StatePayload,
723 pub activation: f32,
725 pub last_updated: SystemTime,
727 pub annotations: HashMap<String, String>,
729}
730
731impl CellState {
732 pub fn new(node_id: NodeId, payload: StatePayload) -> Self {
734 Self {
735 node_id,
736 payload,
737 activation: 0.0,
738 last_updated: SystemTime::now(),
739 annotations: HashMap::new(),
740 }
741 }
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
746pub struct Snapshot {
747 pub id: String,
749 pub graph: SourceCodeGraph,
751 pub vibes: Vec<Vibe>,
753 pub cell_states: Vec<CellState>,
755 pub constitution: Constitution,
757 pub created_at: SystemTime,
759}