use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
mod arc_str_serde {
use super::*;
pub fn serialize<S>(arc: &Arc<str>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(arc.as_ref())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<str>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(s.into())
}
pub fn serialize_opt<S>(arc: &Option<Arc<str>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match arc {
Some(s) => serializer.serialize_some(s.as_ref()),
None => serializer.serialize_none(),
}
}
pub fn deserialize_opt<'de, D>(deserializer: D) -> Result<Option<Arc<str>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
Ok(opt.map(|s| s.into()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum DetailLevel {
Low = 1,
Medium = 2,
High = 3,
}
impl DetailLevel {
pub fn value(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SignatureInfo {
pub parameters: Vec<(String, Option<String>)>,
pub return_type: Option<String>,
pub decorators: Vec<String>,
pub raw: Option<String>,
}
impl SignatureInfo {
pub fn new() -> Self {
Self {
parameters: Vec::new(),
return_type: None,
decorators: Vec::new(),
raw: None,
}
}
pub fn render(&self, detail: DetailLevel) -> String {
match detail {
DetailLevel::Low => {
let names: Vec<_> = self.parameters.iter().map(|(n, _)| n.as_str()).collect();
format!("({})", names.join(", "))
}
DetailLevel::Medium => {
let parts: Vec<_> = self
.parameters
.iter()
.map(|(name, typ)| {
if let Some(t) = typ {
let simple = simplify_type(t);
format!("{}: {}", name, simple)
} else {
name.clone()
}
})
.collect();
let ret = self
.return_type
.as_ref()
.map(|t| format!(" -> {}", simplify_type(t)))
.unwrap_or_default();
format!("({}){}", parts.join(", "), ret)
}
DetailLevel::High => {
let parts: Vec<_> = self
.parameters
.iter()
.map(|(name, typ)| {
if let Some(t) = typ {
format!("{}: {}", name, t)
} else {
name.clone()
}
})
.collect();
let ret = self
.return_type
.as_ref()
.map(|t| format!(" -> {}", t))
.unwrap_or_default();
format!("({}){}", parts.join(", "), ret)
}
}
}
}
impl Default for SignatureInfo {
fn default() -> Self {
Self::new()
}
}
fn simplify_type(t: &str) -> &str {
if let Some(pos) = t.find('[') {
return &t[..pos];
}
if let Some(pos) = t.find('<') {
return &t[..pos];
}
if let Some(pos) = t.rfind("::") {
return &t[pos + 2..];
}
if let Some(pos) = t.rfind('.') {
return &t[pos + 1..];
}
t
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FieldInfo {
pub name: String,
pub type_annotation: Option<String>,
pub default_value: Option<String>,
}
impl FieldInfo {
pub fn render(&self, detail: DetailLevel) -> String {
match detail {
DetailLevel::Low => self.name.clone(),
DetailLevel::Medium => {
if let Some(t) = &self.type_annotation {
format!("{}: {}", self.name, simplify_type(t))
} else {
self.name.clone()
}
}
DetailLevel::High => {
let typ = self.type_annotation.as_deref().unwrap_or("?");
if let Some(default) = &self.default_value {
format!("{}: {} = {}", self.name, typ, default)
} else {
format!("{}: {}", self.name, typ)
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tag {
#[serde(with = "arc_str_serde")]
pub rel_fname: Arc<str>,
#[serde(with = "arc_str_serde")]
pub fname: Arc<str>,
pub line: u32,
#[serde(with = "arc_str_serde")]
pub name: Arc<str>,
pub kind: TagKind,
#[serde(with = "arc_str_serde")]
pub node_type: Arc<str>,
#[serde(
serialize_with = "arc_str_serde::serialize_opt",
deserialize_with = "arc_str_serde::deserialize_opt"
)]
pub parent_name: Option<Arc<str>>,
pub parent_line: Option<u32>,
pub signature: Option<SignatureInfo>,
pub fields: Option<Vec<FieldInfo>>,
pub metadata: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TagKind {
Def,
Ref,
}
impl TagKind {
pub fn is_definition(&self) -> bool {
matches!(self, TagKind::Def)
}
pub fn is_reference(&self) -> bool {
matches!(self, TagKind::Ref)
}
}
impl Tag {
pub fn is_def(&self) -> bool {
matches!(self.kind, TagKind::Def)
}
pub fn is_ref(&self) -> bool {
matches!(self.kind, TagKind::Ref)
}
}
#[derive(Debug, Clone)]
pub struct RankedTag {
pub rank: f64,
pub tag: Tag,
}
impl RankedTag {
pub fn new(rank: f64, tag: Tag) -> Self {
Self { rank, tag }
}
}
impl PartialEq for RankedTag {
fn eq(&self, other: &Self) -> bool {
self.rank == other.rank
}
}
impl Eq for RankedTag {}
impl PartialOrd for RankedTag {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RankedTag {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other
.rank
.partial_cmp(&self.rank)
.unwrap_or(std::cmp::Ordering::Equal)
}
}
#[derive(Debug, Clone)]
pub struct RankingConfig {
pub pagerank_alpha: f64,
pub pagerank_chat_multiplier: f64,
pub depth_weight_root: f64,
pub depth_weight_moderate: f64,
pub depth_weight_deep: f64,
pub depth_weight_vendor: f64,
pub depth_threshold_shallow: usize,
pub depth_threshold_moderate: usize,
pub boost_mentioned_ident: f64,
pub boost_mentioned_file: f64,
pub boost_chat_file: f64,
pub boost_temporal_coupling: f64,
pub boost_focus_expansion: f64,
pub boost_caller_weight: f64,
pub focus_expansion_max_hops: usize,
pub focus_expansion_decay: f64,
pub boost_test_coupling: f64,
pub test_coupling_min_confidence: f64,
pub hub_damping: f64,
pub git_recency_decay_days: f64,
pub git_recency_max_boost: f64,
pub git_churn_threshold: usize,
pub git_churn_max_boost: f64,
pub git_author_boost: f64,
pub git_badge_recent_days: u32,
pub git_badge_churn_commits: usize,
pub phase_crystal_min_age_days: u32,
pub phase_crystal_min_quiet_days: u32,
pub phase_rotting_min_age_days: u32,
pub phase_rotting_max_quiet_days: u32,
pub phase_rotting_churn_multiplier: f64,
pub phase_emergent_max_age_days: u32,
pub vendor_patterns: Vec<String>,
}
impl Default for RankingConfig {
fn default() -> Self {
Self {
pagerank_alpha: 0.85,
pagerank_chat_multiplier: 100.0,
depth_weight_root: 1.0,
depth_weight_moderate: 0.5,
depth_weight_deep: 0.1,
depth_weight_vendor: 0.01,
depth_threshold_shallow: 2,
depth_threshold_moderate: 4,
boost_mentioned_ident: 10.0,
boost_mentioned_file: 5.0,
boost_chat_file: 20.0,
boost_temporal_coupling: 3.0,
boost_focus_expansion: 5.0,
boost_caller_weight: 2.0,
focus_expansion_max_hops: 2, focus_expansion_decay: 0.5,
boost_test_coupling: 5.0, test_coupling_min_confidence: 0.5,
hub_damping: 0.0,
git_recency_decay_days: 30.0,
git_recency_max_boost: 10.0,
git_churn_threshold: 5,
git_churn_max_boost: 6.0,
git_author_boost: 1.5,
git_badge_recent_days: 7,
git_badge_churn_commits: 10,
phase_crystal_min_age_days: 180,
phase_crystal_min_quiet_days: 30,
phase_rotting_min_age_days: 90,
phase_rotting_max_quiet_days: 14,
phase_rotting_churn_multiplier: 1.5,
phase_emergent_max_age_days: 30,
vendor_patterns: vec![
"node_modules".into(),
"vendor".into(),
"third_party".into(),
"__pycache__".into(),
"site-packages".into(),
".git".into(),
"target".into(),
],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FilePhase {
Crystal,
Rotting,
Emergent,
Evolving,
}
impl FilePhase {
pub fn badge(&self) -> &'static str {
match self {
FilePhase::Crystal => "crystal",
FilePhase::Rotting => "rotting",
FilePhase::Emergent => "emergent",
FilePhase::Evolving => "evolving",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Intent {
Debug,
Explore,
Extend,
Refactor,
}
impl Intent {
pub fn recipe(&self) -> IntentRecipe {
match self {
Intent::Debug => IntentRecipe {
recency_weight: 1.5,
churn_weight: 0.8,
reverse_edge_bias: 2.0,
},
Intent::Explore => IntentRecipe {
recency_weight: 1.0,
churn_weight: 1.0,
reverse_edge_bias: 1.0,
},
Intent::Extend => IntentRecipe {
recency_weight: 1.2,
churn_weight: 0.5,
reverse_edge_bias: 0.7,
},
Intent::Refactor => IntentRecipe {
recency_weight: 0.8,
churn_weight: 2.0,
reverse_edge_bias: 1.0,
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct IntentRecipe {
pub recency_weight: f64,
pub churn_weight: f64,
pub reverse_edge_bias: f64,
}
pub type SymbolId = (Arc<str>, Arc<str>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detail_levels() {
assert!(DetailLevel::Low < DetailLevel::Medium);
assert!(DetailLevel::Medium < DetailLevel::High);
}
#[test]
fn test_signature_render() {
let sig = SignatureInfo {
parameters: vec![
("host".into(), Some("str".into())),
("port".into(), Some("int".into())),
],
return_type: Some("bool".into()),
decorators: vec![],
raw: None,
};
assert_eq!(sig.render(DetailLevel::Low), "(host, port)");
assert_eq!(
sig.render(DetailLevel::Medium),
"(host: str, port: int) -> bool"
);
assert_eq!(
sig.render(DetailLevel::High),
"(host: str, port: int) -> bool"
);
}
#[test]
fn test_simplify_type() {
assert_eq!(simplify_type("Dict[str, int]"), "Dict");
assert_eq!(simplify_type("Vec<String>"), "Vec");
assert_eq!(simplify_type("std::collections::HashMap"), "HashMap");
assert_eq!(simplify_type("collections.OrderedDict"), "OrderedDict");
assert_eq!(simplify_type("int"), "int");
}
#[test]
fn test_ranked_tag_ordering() {
let tag1 = Tag {
rel_fname: "a.rs".into(),
fname: "/a.rs".into(),
line: 1,
name: "foo".into(),
kind: TagKind::Def,
node_type: "function".into(),
parent_name: None,
parent_line: None,
signature: None,
fields: None,
metadata: None,
};
let tag2 = tag1.clone();
let ranked1 = RankedTag::new(0.5, tag1);
let ranked2 = RankedTag::new(0.8, tag2);
assert!(ranked2 < ranked1);
}
#[test]
fn test_tag_kind_helpers() {
assert!(TagKind::Def.is_definition());
assert!(!TagKind::Def.is_reference());
assert!(TagKind::Ref.is_reference());
assert!(!TagKind::Ref.is_definition());
}
}