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