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, SystemTime};
9
10#[cfg(not(target_arch = "wasm32"))]
12use std::time::Instant;
13#[cfg(target_arch = "wasm32")]
14use web_time::Instant;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub enum GitChangeKind {
23 Modified,
25 Added,
27 Deleted,
29 RenamedFrom,
31 RenamedTo,
33 Untracked,
35}
36
37impl GitChangeKind {
38 pub fn label(&self) -> &'static str {
40 match self {
41 GitChangeKind::Modified => "Modified",
42 GitChangeKind::Added => "Added",
43 GitChangeKind::Deleted => "Deleted",
44 GitChangeKind::RenamedFrom => "Renamed (from)",
45 GitChangeKind::RenamedTo => "Renamed (to)",
46 GitChangeKind::Untracked => "Untracked",
47 }
48 }
49
50 pub fn symbol(&self) -> &'static str {
52 match self {
53 GitChangeKind::Modified => "M",
54 GitChangeKind::Added => "+",
55 GitChangeKind::Deleted => "-",
56 GitChangeKind::RenamedFrom => "R←",
57 GitChangeKind::RenamedTo => "R→",
58 GitChangeKind::Untracked => "?",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct GitFileChange {
66 pub path: PathBuf,
68 pub kind: GitChangeKind,
70 pub staged: bool,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct GitChangeSnapshot {
77 pub changes: Vec<GitFileChange>,
79 #[serde(skip)]
81 pub captured_at: Option<Instant>,
82}
83
84impl GitChangeSnapshot {
85 pub fn new() -> Self {
87 Self {
88 changes: Vec::new(),
89 captured_at: Some(Instant::now()),
90 }
91 }
92
93 pub fn has_changes(&self, path: &Path) -> bool {
95 self.changes.iter().any(|c| c.path == path)
96 }
97
98 pub fn get_change(&self, path: &Path) -> Option<&GitFileChange> {
100 self.changes.iter().find(|c| c.path == path)
101 }
102
103 pub fn changed_paths(&self) -> impl Iterator<Item = &Path> {
105 self.changes.iter().map(|c| c.path.as_path())
106 }
107
108 pub fn count_by_kind(&self, kind: GitChangeKind) -> usize {
110 self.changes.iter().filter(|c| c.kind == kind).count()
111 }
112
113 pub fn is_stale(&self, max_age: Duration) -> bool {
115 match self.captured_at {
116 Some(at) => at.elapsed() > max_age,
117 None => true,
118 }
119 }
120
121 pub fn age(&self) -> Option<Duration> {
123 self.captured_at.map(|at| at.elapsed())
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct ChangeIndicatorState {
130 pub phase: f32,
132 pub speed: f32,
134 pub enabled: bool,
136}
137
138impl Default for ChangeIndicatorState {
139 fn default() -> Self {
140 Self {
141 phase: 0.0,
142 speed: 1.0,
143 enabled: true,
144 }
145 }
146}
147
148impl ChangeIndicatorState {
149 pub fn tick(&mut self, dt: f32) {
151 if self.enabled {
152 self.phase = (self.phase + dt * self.speed) % 1.0;
153 }
154 }
155
156 pub fn pulse_scale(&self) -> f32 {
158 let t = self.phase * std::f32::consts::TAU;
160 1.0 + 0.15 * (t.sin() * 0.5 + 0.5)
161 }
162
163 pub fn ring_alpha(&self) -> f32 {
165 let t = self.phase * std::f32::consts::TAU;
166 0.3 + 0.4 * (t.sin() * 0.5 + 0.5)
167 }
168}
169
170#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
172pub struct NodeId(pub u64);
173
174#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
176pub struct EdgeId(pub u64);
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
180pub enum GraphNodeKind {
181 Module,
183 File,
185 Directory,
187 Service,
189 Test,
191 #[default]
193 Other,
194}
195
196#[derive(Debug, Default, Clone, Serialize, Deserialize)]
198pub struct GraphNode {
199 pub id: NodeId,
201 pub name: String,
203 pub kind: GraphNodeKind,
205 pub metadata: HashMap<String, String>,
207}
208
209#[derive(Debug, Default, Clone, Serialize, Deserialize)]
211pub struct GraphEdge {
212 pub id: EdgeId,
214 pub from: NodeId,
216 pub to: NodeId,
218 pub relationship: String,
220 pub metadata: HashMap<String, String>,
222}
223
224#[derive(Debug, Default, Clone, Serialize, Deserialize)]
226pub struct SourceCodeGraph {
227 pub nodes: Vec<GraphNode>,
229 pub edges: Vec<GraphEdge>,
231 pub metadata: HashMap<String, String>,
233}
234
235impl SourceCodeGraph {
236 pub fn empty() -> Self {
238 Self::default()
239 }
240
241 pub fn node_count(&self) -> usize {
243 self.nodes.len()
244 }
245
246 pub fn edge_count(&self) -> usize {
248 self.edges.len()
249 }
250
251 pub fn to_petgraph(&self) -> (StableDiGraph<GraphNode, String>, HashMap<NodeId, NodeIndex>) {
254 let mut graph = StableDiGraph::new();
255 let mut id_to_index = HashMap::new();
256
257 for node in &self.nodes {
259 let idx = graph.add_node(node.clone());
260 id_to_index.insert(node.id, idx);
261 }
262
263 for edge in &self.edges {
265 if let (Some(&from_idx), Some(&to_idx)) =
266 (id_to_index.get(&edge.from), id_to_index.get(&edge.to))
267 {
268 graph.add_edge(from_idx, to_idx, edge.relationship.clone());
269 }
270 }
271
272 (graph, id_to_index)
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278pub enum ReferenceKind {
279 Uses,
281 Imports,
283 Implements,
285 Contains,
287}
288
289impl std::fmt::Display for ReferenceKind {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 match self {
292 ReferenceKind::Uses => write!(f, "uses"),
293 ReferenceKind::Imports => write!(f, "imports"),
294 ReferenceKind::Implements => write!(f, "implements"),
295 ReferenceKind::Contains => write!(f, "contains"),
296 }
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct SourceReference {
303 pub source_path: PathBuf,
305 pub kind: ReferenceKind,
307 pub target_route: PathBuf,
309}
310
311#[derive(Debug, Default)]
313pub struct SourceCodeGraphBuilder {
314 nodes: Vec<GraphNode>,
315 edges: Vec<GraphEdge>,
316 path_to_node: HashMap<PathBuf, NodeId>,
317 next_node_id: u64,
318 next_edge_id: u64,
319 metadata: HashMap<String, String>,
320}
321
322impl SourceCodeGraphBuilder {
323 pub fn new() -> Self {
325 Self::default()
326 }
327
328 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
330 self.metadata.insert(key.into(), value.into());
331 self
332 }
333
334 pub fn add_directory(&mut self, path: &Path) -> NodeId {
336 if let Some(&id) = self.path_to_node.get(path) {
337 return id;
338 }
339
340 let id = NodeId(self.next_node_id);
341 self.next_node_id += 1;
342
343 let name = path
344 .file_name()
345 .and_then(|n| n.to_str())
346 .unwrap_or_else(|| path.to_str().unwrap_or("."))
347 .to_string();
348
349 let mut metadata = HashMap::new();
350 metadata.insert("path".to_string(), path.to_string_lossy().to_string());
351
352 self.nodes.push(GraphNode {
353 id,
354 name,
355 kind: GraphNodeKind::Directory,
356 metadata,
357 });
358
359 self.path_to_node.insert(path.to_path_buf(), id);
360 id
361 }
362
363 pub fn add_file(&mut self, path: &Path, relative_path: &str) -> NodeId {
365 if let Some(&id) = self.path_to_node.get(path) {
366 return id;
367 }
368
369 let id = NodeId(self.next_node_id);
370 self.next_node_id += 1;
371
372 let name = path
373 .file_name()
374 .and_then(|n| n.to_str())
375 .unwrap_or_else(|| path.to_str().unwrap_or("unknown"))
376 .to_string();
377
378 let kind = match path.extension().and_then(|e| e.to_str()) {
380 Some("rs") | Some("py") | Some("js") | Some("ts") | Some("tsx") | Some("jsx")
381 | Some("go") | Some("java") | Some("c") | Some("cpp") | Some("h") | Some("hpp") => {
382 if relative_path.contains("test") || name.starts_with("test_") {
383 GraphNodeKind::Test
384 } else if name == "mod.rs"
385 || name == "__init__.py"
386 || name == "index.ts"
387 || name == "index.js"
388 {
389 GraphNodeKind::Module
390 } else {
391 GraphNodeKind::File
392 }
393 }
394 _ => GraphNodeKind::File,
395 };
396
397 let mut metadata = HashMap::new();
398 metadata.insert("path".to_string(), path.to_string_lossy().to_string());
399 metadata.insert("relative_path".to_string(), relative_path.to_string());
400
401 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
402 metadata.insert("extension".to_string(), ext.to_string());
403 metadata.insert(
404 "language".to_string(),
405 extension_to_language(ext).to_string(),
406 );
407 }
408
409 self.nodes.push(GraphNode {
410 id,
411 name,
412 kind,
413 metadata,
414 });
415
416 self.path_to_node.insert(path.to_path_buf(), id);
417 id
418 }
419
420 pub fn add_hierarchy_edge(&mut self, parent_path: &Path, child_path: &Path) {
422 if let (Some(&parent_id), Some(&child_id)) = (
423 self.path_to_node.get(parent_path),
424 self.path_to_node.get(child_path),
425 ) {
426 if parent_id != child_id {
427 self.add_edge(parent_id, child_id, ReferenceKind::Contains);
428 }
429 }
430 }
431
432 pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: ReferenceKind) {
434 let id = EdgeId(self.next_edge_id);
435 self.next_edge_id += 1;
436
437 self.edges.push(GraphEdge {
438 id,
439 from,
440 to,
441 relationship: kind.to_string(),
442 metadata: HashMap::new(),
443 });
444 }
445
446 pub fn get_node_id(&self, path: &Path) -> Option<NodeId> {
448 self.path_to_node.get(path).copied()
449 }
450
451 pub fn find_node_by_path_suffix(&self, route: &Path) -> Option<NodeId> {
453 let route_str = route.to_string_lossy();
454
455 for (path, &node_id) in &self.path_to_node {
456 let path_str = path.to_string_lossy();
457
458 if path_str.ends_with(route_str.as_ref()) {
460 return Some(node_id);
461 }
462
463 let normalized_path: String = path_str.trim_start_matches("./").replace('\\', "/");
465 let normalized_route: String = route_str.trim_start_matches("./").replace('\\', "/");
466 if normalized_path.ends_with(&normalized_route) {
467 return Some(node_id);
468 }
469
470 let route_parts: Vec<&str> = normalized_route.split('/').collect();
472 let path_parts: Vec<&str> = normalized_path.split('/').collect();
473 if route_parts.len() <= path_parts.len() {
474 for window in path_parts.windows(route_parts.len()) {
475 if window == route_parts.as_slice() {
476 return Some(node_id);
477 }
478 }
479 }
480
481 if let (Some(file_name), Some(route_name)) = (
483 path.file_name().and_then(|n| n.to_str()),
484 route.file_name().and_then(|n| n.to_str()),
485 ) {
486 if file_name == route_name {
487 return Some(node_id);
488 }
489 }
490 }
491
492 None
493 }
494
495 pub fn set_node_metadata(&mut self, node_id: NodeId, key: impl Into<String>, value: impl Into<String>) {
497 if let Some(node) = self.nodes.iter_mut().find(|n| n.id == node_id) {
498 node.metadata.insert(key.into(), value.into());
499 }
500 }
501
502 pub fn node_count(&self) -> usize {
504 self.nodes.len()
505 }
506
507 pub fn edge_count(&self) -> usize {
509 self.edges.len()
510 }
511
512 pub fn build(self) -> SourceCodeGraph {
514 SourceCodeGraph {
515 nodes: self.nodes,
516 edges: self.edges,
517 metadata: self.metadata,
518 }
519 }
520}
521
522pub fn detect_rust_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
524 let mut refs = Vec::new();
525
526 for line in content.lines() {
527 let trimmed = line.trim();
528
529 if trimmed.starts_with("pub mod ") || trimmed.starts_with("mod ") {
531 let mod_part = trimmed
532 .strip_prefix("pub mod ")
533 .or_else(|| trimmed.strip_prefix("mod "))
534 .unwrap_or("")
535 .split(';')
536 .next()
537 .unwrap_or("")
538 .trim();
539
540 if !mod_part.is_empty() && !mod_part.contains('{') {
541 refs.push(SourceReference {
542 source_path: source_path.to_path_buf(),
543 kind: ReferenceKind::Contains, target_route: PathBuf::from(format!("{}.rs", mod_part)),
545 });
546 refs.push(SourceReference {
548 source_path: source_path.to_path_buf(),
549 kind: ReferenceKind::Contains,
550 target_route: PathBuf::from(format!("{}/mod.rs", mod_part)),
551 });
552 }
553 }
554
555 if !trimmed.starts_with("use ") {
556 continue;
557 }
558
559 let use_part = trimmed
561 .strip_prefix("use ")
562 .unwrap_or("")
563 .split(';')
564 .next()
565 .unwrap_or("")
566 .split('{')
567 .next()
568 .unwrap_or("")
569 .trim();
570
571 if use_part.is_empty() {
572 continue;
573 }
574
575 let module_path = use_part
578 .strip_prefix("crate::")
579 .or_else(|| use_part.strip_prefix("self::"))
580 .or_else(|| use_part.strip_prefix("super::"))
581 .unwrap_or(use_part);
582
583 let path_str = module_path
585 .replace("::", "/")
586 .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_')
587 .to_string();
588
589 refs.push(SourceReference {
590 source_path: source_path.to_path_buf(),
591 kind: ReferenceKind::Uses,
592 target_route: PathBuf::from(format!("{}.rs", path_str)),
593 });
594 }
595
596 refs
597}
598
599pub fn detect_python_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
601 let mut refs = Vec::new();
602
603 for line in content.lines() {
604 let trimmed = line.trim();
605
606 if trimmed.starts_with("import ") && !trimmed.starts_with("import(") {
608 let import_part = trimmed
609 .strip_prefix("import ")
610 .unwrap_or("")
611 .split_whitespace()
612 .next()
613 .unwrap_or("")
614 .split(',')
615 .next()
616 .unwrap_or("")
617 .trim();
618
619 if !import_part.is_empty() {
620 let path_str = import_part.replace('.', "/");
621 refs.push(SourceReference {
622 source_path: source_path.to_path_buf(),
623 kind: ReferenceKind::Imports,
624 target_route: PathBuf::from(format!("{}.py", path_str)),
625 });
626 }
627 }
628
629 if let Some(module_part) = trimmed
631 .strip_prefix("from ")
632 .and_then(|s| s.split(" import ").next())
633 {
634 let module = module_part.trim();
635 if !module.is_empty() && module != "." && !module.starts_with("..") {
636 let path_str = module.replace('.', "/");
637 refs.push(SourceReference {
638 source_path: source_path.to_path_buf(),
639 kind: ReferenceKind::Imports,
640 target_route: PathBuf::from(format!("{}.py", path_str)),
641 });
642 }
643 }
644 }
645
646 refs
647}
648
649pub fn detect_ts_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
651 let mut refs = Vec::new();
652
653 for line in content.lines() {
654 let trimmed = line.trim();
655
656 if trimmed.starts_with("import ") {
658 if let Some(path_start) = trimmed.find(['\'', '"']) {
660 let quote_char = trimmed.chars().nth(path_start).unwrap();
661 let rest = &trimmed[path_start + 1..];
662 if let Some(path_end) = rest.find(quote_char) {
663 let import_path = &rest[..path_end];
664 if import_path.starts_with('.') {
666 refs.push(SourceReference {
667 source_path: source_path.to_path_buf(),
668 kind: ReferenceKind::Imports,
669 target_route: PathBuf::from(import_path),
670 });
671 }
672 }
673 }
674 }
675 }
676
677 refs
678}
679
680pub fn detect_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
682 match source_path.extension().and_then(|e| e.to_str()) {
683 Some("rs") => detect_rust_references(content, source_path),
684 Some("py") => detect_python_references(content, source_path),
685 Some("ts") | Some("tsx") | Some("js") | Some("jsx") => {
686 detect_ts_references(content, source_path)
687 }
688 _ => Vec::new(),
689 }
690}
691
692fn extension_to_language(ext: &str) -> &'static str {
694 match ext {
695 "rs" => "rust",
696 "py" => "python",
697 "js" => "javascript",
698 "ts" => "typescript",
699 "tsx" => "typescript",
700 "jsx" => "javascript",
701 "go" => "go",
702 "java" => "java",
703 "c" | "h" => "c",
704 "cpp" | "hpp" | "cc" | "cxx" => "cpp",
705 "md" => "markdown",
706 "json" => "json",
707 "yaml" | "yml" => "yaml",
708 "toml" => "toml",
709 _ => "unknown",
710 }
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct Vibe {
716 pub id: String,
718 pub title: String,
720 pub description: String,
722 pub targets: Vec<NodeId>,
724 pub created_by: String,
726 pub created_at: SystemTime,
728 pub metadata: HashMap<String, String>,
730}
731
732#[derive(Debug, Default, Clone, Serialize, Deserialize)]
734pub struct Constitution {
735 pub name: String,
737 pub version: String,
739 pub description: String,
741 pub policies: Vec<String>,
743}
744
745pub type StatePayload = Value;
747
748#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct CellState {
751 pub node_id: NodeId,
753 pub payload: StatePayload,
755 pub activation: f32,
757 pub last_updated: SystemTime,
759 pub annotations: HashMap<String, String>,
761}
762
763impl CellState {
764 pub fn new(node_id: NodeId, payload: StatePayload) -> Self {
766 Self {
767 node_id,
768 payload,
769 activation: 0.0,
770 last_updated: SystemTime::now(),
771 annotations: HashMap::new(),
772 }
773 }
774}
775
776#[derive(Debug, Clone, Serialize, Deserialize)]
778pub struct Snapshot {
779 pub id: String,
781 pub graph: SourceCodeGraph,
783 pub vibes: Vec<Vibe>,
785 pub cell_states: Vec<CellState>,
787 pub constitution: Constitution,
789 pub created_at: SystemTime,
791}
792
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
795pub enum LayoutStrategy {
796 #[default]
798 Flat,
799 Lattice {
801 width: usize,
802 group_by_row: bool
803 },
804 Direct,
806 Preserve,
808 Modular,
810}