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 node_count(&self) -> usize {
497 self.nodes.len()
498 }
499
500 pub fn edge_count(&self) -> usize {
502 self.edges.len()
503 }
504
505 pub fn build(self) -> SourceCodeGraph {
507 SourceCodeGraph {
508 nodes: self.nodes,
509 edges: self.edges,
510 metadata: self.metadata,
511 }
512 }
513}
514
515pub fn detect_rust_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
517 let mut refs = Vec::new();
518
519 for line in content.lines() {
520 let trimmed = line.trim();
521
522 if trimmed.starts_with("pub mod ") || trimmed.starts_with("mod ") {
524 let mod_part = trimmed
525 .strip_prefix("pub mod ")
526 .or_else(|| trimmed.strip_prefix("mod "))
527 .unwrap_or("")
528 .split(';')
529 .next()
530 .unwrap_or("")
531 .trim();
532
533 if !mod_part.is_empty() && !mod_part.contains('{') {
534 refs.push(SourceReference {
535 source_path: source_path.to_path_buf(),
536 kind: ReferenceKind::Contains, target_route: PathBuf::from(format!("{}.rs", mod_part)),
538 });
539 refs.push(SourceReference {
541 source_path: source_path.to_path_buf(),
542 kind: ReferenceKind::Contains,
543 target_route: PathBuf::from(format!("{}/mod.rs", mod_part)),
544 });
545 }
546 }
547
548 if !trimmed.starts_with("use ") {
549 continue;
550 }
551
552 let use_part = trimmed
554 .strip_prefix("use ")
555 .unwrap_or("")
556 .split(';')
557 .next()
558 .unwrap_or("")
559 .split('{')
560 .next()
561 .unwrap_or("")
562 .trim();
563
564 if use_part.is_empty() {
565 continue;
566 }
567
568 let module_path = use_part
571 .strip_prefix("crate::")
572 .or_else(|| use_part.strip_prefix("self::"))
573 .or_else(|| use_part.strip_prefix("super::"))
574 .unwrap_or(use_part);
575
576 let path_str = module_path
578 .replace("::", "/")
579 .trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_')
580 .to_string();
581
582 refs.push(SourceReference {
583 source_path: source_path.to_path_buf(),
584 kind: ReferenceKind::Uses,
585 target_route: PathBuf::from(format!("{}.rs", path_str)),
586 });
587 }
588
589 refs
590}
591
592pub fn detect_python_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
594 let mut refs = Vec::new();
595
596 for line in content.lines() {
597 let trimmed = line.trim();
598
599 if trimmed.starts_with("import ") && !trimmed.starts_with("import(") {
601 let import_part = trimmed
602 .strip_prefix("import ")
603 .unwrap_or("")
604 .split_whitespace()
605 .next()
606 .unwrap_or("")
607 .split(',')
608 .next()
609 .unwrap_or("")
610 .trim();
611
612 if !import_part.is_empty() {
613 let path_str = import_part.replace('.', "/");
614 refs.push(SourceReference {
615 source_path: source_path.to_path_buf(),
616 kind: ReferenceKind::Imports,
617 target_route: PathBuf::from(format!("{}.py", path_str)),
618 });
619 }
620 }
621
622 if let Some(module_part) = trimmed
624 .strip_prefix("from ")
625 .and_then(|s| s.split(" import ").next())
626 {
627 let module = module_part.trim();
628 if !module.is_empty() && module != "." && !module.starts_with("..") {
629 let path_str = module.replace('.', "/");
630 refs.push(SourceReference {
631 source_path: source_path.to_path_buf(),
632 kind: ReferenceKind::Imports,
633 target_route: PathBuf::from(format!("{}.py", path_str)),
634 });
635 }
636 }
637 }
638
639 refs
640}
641
642pub fn detect_ts_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
644 let mut refs = Vec::new();
645
646 for line in content.lines() {
647 let trimmed = line.trim();
648
649 if trimmed.starts_with("import ") {
651 if let Some(path_start) = trimmed.find(['\'', '"']) {
653 let quote_char = trimmed.chars().nth(path_start).unwrap();
654 let rest = &trimmed[path_start + 1..];
655 if let Some(path_end) = rest.find(quote_char) {
656 let import_path = &rest[..path_end];
657 if import_path.starts_with('.') {
659 refs.push(SourceReference {
660 source_path: source_path.to_path_buf(),
661 kind: ReferenceKind::Imports,
662 target_route: PathBuf::from(import_path),
663 });
664 }
665 }
666 }
667 }
668 }
669
670 refs
671}
672
673pub fn detect_references(content: &str, source_path: &Path) -> Vec<SourceReference> {
675 match source_path.extension().and_then(|e| e.to_str()) {
676 Some("rs") => detect_rust_references(content, source_path),
677 Some("py") => detect_python_references(content, source_path),
678 Some("ts") | Some("tsx") | Some("js") | Some("jsx") => {
679 detect_ts_references(content, source_path)
680 }
681 _ => Vec::new(),
682 }
683}
684
685fn extension_to_language(ext: &str) -> &'static str {
687 match ext {
688 "rs" => "rust",
689 "py" => "python",
690 "js" => "javascript",
691 "ts" => "typescript",
692 "tsx" => "typescript",
693 "jsx" => "javascript",
694 "go" => "go",
695 "java" => "java",
696 "c" | "h" => "c",
697 "cpp" | "hpp" | "cc" | "cxx" => "cpp",
698 "md" => "markdown",
699 "json" => "json",
700 "yaml" | "yml" => "yaml",
701 "toml" => "toml",
702 _ => "unknown",
703 }
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct Vibe {
709 pub id: String,
711 pub title: String,
713 pub description: String,
715 pub targets: Vec<NodeId>,
717 pub created_by: String,
719 pub created_at: SystemTime,
721 pub metadata: HashMap<String, String>,
723}
724
725#[derive(Debug, Default, Clone, Serialize, Deserialize)]
727pub struct Constitution {
728 pub name: String,
730 pub version: String,
732 pub description: String,
734 pub policies: Vec<String>,
736}
737
738pub type StatePayload = Value;
740
741#[derive(Debug, Clone, Serialize, Deserialize)]
743pub struct CellState {
744 pub node_id: NodeId,
746 pub payload: StatePayload,
748 pub activation: f32,
750 pub last_updated: SystemTime,
752 pub annotations: HashMap<String, String>,
754}
755
756impl CellState {
757 pub fn new(node_id: NodeId, payload: StatePayload) -> Self {
759 Self {
760 node_id,
761 payload,
762 activation: 0.0,
763 last_updated: SystemTime::now(),
764 annotations: HashMap::new(),
765 }
766 }
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct Snapshot {
772 pub id: String,
774 pub graph: SourceCodeGraph,
776 pub vibes: Vec<Vibe>,
778 pub cell_states: Vec<CellState>,
780 pub constitution: Constitution,
782 pub created_at: SystemTime,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
788pub enum LayoutStrategy {
789 #[default]
791 Flat,
792 Lattice {
794 width: usize,
795 group_by_row: bool
796 },
797 Direct,
799 Preserve,
801 Modular,
803}