use std::collections::{BTreeMap, BTreeSet, VecDeque};
use serde::Serialize;
use crate::store::{Store, StoreError};
use crate::{Edge, EdgeKind, NodeKind, Provenance};
pub const SCHEMA: &str = "roteiro.query/v1";
pub fn window<T>(items: &mut Vec<T>, offset: usize, limit: usize) {
items.drain(..offset.min(items.len()));
if limit > 0 {
items.truncate(limit);
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct NodeSummary {
pub key: String,
pub kind: String,
pub name: String,
pub path: Option<String>,
pub lang: Option<String>,
}
impl NodeSummary {
fn from_node(node: &crate::Node) -> Self {
Self {
key: node.key.clone(),
kind: node.kind.as_str().to_owned(),
name: node.name.clone(),
path: node.path.clone(),
lang: node.lang.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct EdgeRef {
pub kind: String,
pub provenance: &'static str,
pub confidence: Option<f64>,
pub node: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Explanation {
pub schema: &'static str,
pub node: NodeSummary,
pub meta: serde_json::Value,
pub outgoing: Vec<EdgeRef>,
pub incoming: Vec<EdgeRef>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Listing {
pub schema: &'static str,
pub kind: String,
pub nodes: Vec<NodeSummary>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DebtItem {
pub key: String,
pub category: String,
pub text: String,
pub path: Option<String>,
pub line: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DebtReport {
pub schema: &'static str,
pub total: usize,
pub by_category: BTreeMap<String, usize>,
pub items: Vec<DebtItem>,
}
pub fn debt(
store: &Store,
categories: &[String],
ignore: &[String],
) -> Result<DebtReport, StoreError> {
let filter: std::collections::BTreeSet<&str> = categories.iter().map(String::as_str).collect();
let mut items = Vec::new();
let mut by_category: BTreeMap<String, usize> = BTreeMap::new();
for node in store.nodes_by_kind(&NodeKind::Marker)? {
let category = node
.meta
.get("category")
.and_then(serde_json::Value::as_str)
.unwrap_or("other")
.to_owned();
if !filter.is_empty() && !filter.contains(category.as_str()) {
continue;
}
if let Some(path) = node.path.as_deref()
&& ignore.iter().any(|glob| glob_match(glob, path))
{
continue;
}
let text = node
.meta
.get("text")
.and_then(serde_json::Value::as_str)
.unwrap_or(node.name.as_str())
.to_owned();
let line = node
.meta
.get("line")
.and_then(serde_json::Value::as_u64)
.and_then(|l| u32::try_from(l).ok());
*by_category.entry(category.clone()).or_default() += 1;
items.push(DebtItem {
key: node.key.clone(),
category,
text,
path: node.path.clone(),
line,
});
}
items.sort_by(|a, b| (&a.path, a.line, &a.key).cmp(&(&b.path, b.line, &b.key)));
Ok(DebtReport {
schema: SCHEMA,
total: items.len(),
by_category,
items,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DensityOrder {
#[default]
Density,
Markers,
Lines,
}
impl DensityOrder {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Density => "density",
Self::Markers => "markers",
Self::Lines => "lines",
}
}
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
match s {
"density" => Some(Self::Density),
"markers" => Some(Self::Markers),
"lines" => Some(Self::Lines),
_ => None,
}
}
#[must_use]
pub fn tokens() -> [&'static str; 3] {
[
Self::Density.as_str(),
Self::Markers.as_str(),
Self::Lines.as_str(),
]
}
}
pub const DEFAULT_MIN_LINES: u32 = 50;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DensityItem {
pub path: String,
pub markers: u32,
pub lines: u32,
pub per_kloc: f64,
pub by_category: BTreeMap<String, usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DebtDensityReport {
pub schema: &'static str,
pub order: &'static str,
pub limit: usize,
pub min_lines: u32,
pub files_with_markers: usize,
pub ranked_files: usize,
pub short_files: usize,
pub unknown_length_files: usize,
pub total_markers: usize,
pub total_lines: u64,
pub overall_per_kloc: f64,
pub items: Vec<DensityItem>,
}
pub fn debt_density(
store: &Store,
categories: &[String],
ignore: &[String],
order: DensityOrder,
limit: usize,
min_lines: u32,
) -> Result<DebtDensityReport, StoreError> {
let inventory = debt(store, categories, ignore)?;
let mut per_file: BTreeMap<String, BTreeMap<String, usize>> = BTreeMap::new();
let mut total_markers = 0usize;
for item in &inventory.items {
let Some(path) = item.path.as_deref() else {
continue;
};
*per_file
.entry(path.to_owned())
.or_default()
.entry(item.category.clone())
.or_default() += 1;
total_markers += 1;
}
let files_with_markers = per_file.len();
let mut ranked: Vec<(String, u32, u32, BTreeMap<String, usize>)> = Vec::new();
let mut short_files = 0usize;
let mut unknown_length_files = 0usize;
for (path, by_category) in per_file {
let markers = u32::try_from(by_category.values().sum::<usize>()).unwrap_or(u32::MAX);
let Some(lines) = file_lines(store, &path)? else {
unknown_length_files += 1;
continue;
};
if lines < min_lines {
short_files += 1;
continue;
}
ranked.push((path, markers, lines, by_category));
}
let ranked_files = ranked.len();
let total_lines: u64 = ranked
.iter()
.map(|(_, _, lines, _)| u64::from(*lines))
.sum();
let ranked_markers: u64 = ranked.iter().map(|(_, m, _, _)| u64::from(*m)).sum();
ranked.sort_by(|a, b| {
let metric =
|&(_, markers, lines, _): &(String, u32, u32, BTreeMap<String, usize>)| match order {
DensityOrder::Density => (u128::from(markers) * 1000, u128::from(lines)),
DensityOrder::Markers => (u128::from(markers), 1),
DensityOrder::Lines => (u128::from(lines), 1),
};
let (an, ad) = metric(a);
let (bn, bd) = metric(b);
(bn * ad).cmp(&(an * bd)).then_with(|| a.0.cmp(&b.0))
});
window(&mut ranked, 0, limit);
let items = ranked
.into_iter()
.map(|(path, markers, lines, by_category)| DensityItem {
path,
markers,
lines,
per_kloc: per_kloc(u64::from(markers), u64::from(lines)),
by_category,
})
.collect();
Ok(DebtDensityReport {
schema: SCHEMA,
order: order.as_str(),
limit,
min_lines,
files_with_markers,
ranked_files,
short_files,
unknown_length_files,
total_markers,
total_lines,
overall_per_kloc: per_kloc(ranked_markers, total_lines),
items,
})
}
fn file_lines(store: &Store, path: &str) -> Result<Option<u32>, StoreError> {
let Some(node) = store.get_node(&format!("file:{path}"))? else {
return Ok(None);
};
Ok(node
.meta
.get("lines")
.and_then(serde_json::Value::as_u64)
.and_then(|n| u32::try_from(n).ok())
.filter(|&n| n > 0))
}
fn per_kloc(markers: u64, lines: u64) -> f64 {
if lines == 0 {
return 0.0;
}
#[expect(clippy::cast_precision_loss, reason = "counts are far below 2^53")]
let ratio = (markers as f64) * 1000.0 / (lines as f64);
round2(ratio)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RedactionState {
Redacted,
Declared,
Present,
}
impl RedactionState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Redacted => "redacted",
Self::Declared => "declared",
Self::Present => "present",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ConfigSecretItem {
pub key: String,
pub path: Option<String>,
pub name: String,
pub state: RedactionState,
pub source: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ConfigSecretReport {
pub schema: &'static str,
pub limit: usize,
pub config_keys: usize,
pub secret_named: usize,
pub redacted: usize,
pub declared: usize,
pub unredacted: usize,
pub redacted_not_secret_named: usize,
pub files: usize,
pub items: Vec<ConfigSecretItem>,
}
pub fn config_secrets(store: &Store, limit: usize) -> Result<ConfigSecretReport, StoreError> {
let nodes = store.nodes_by_kind(&NodeKind::Other(crate::config_keys::KIND.to_owned()))?;
let config_keys = nodes.len();
let mut items = Vec::new();
let mut redacted = 0usize;
let mut declared = 0usize;
let mut unredacted = 0usize;
let mut redacted_not_secret_named = 0usize;
let mut files: BTreeSet<String> = BTreeSet::new();
for node in nodes {
let name = node
.meta
.get("key")
.and_then(serde_json::Value::as_str)
.unwrap_or(node.name.as_str())
.to_owned();
let value = node.meta.get("value").and_then(serde_json::Value::as_str);
if !crate::config_keys::is_secret_key(&name) {
if value == Some(crate::config_keys::REDACTED) {
redacted_not_secret_named += 1;
}
continue;
}
let state = match value {
Some(v) if v == crate::config_keys::REDACTED => {
redacted += 1;
RedactionState::Redacted
}
None => {
declared += 1;
RedactionState::Declared
}
Some(_) => {
unredacted += 1;
RedactionState::Present
}
};
if let Some(path) = node.path.as_deref() {
files.insert(path.to_owned());
}
items.push(ConfigSecretItem {
key: node.key,
path: node.path,
name,
state,
source: node
.meta
.get("source")
.and_then(serde_json::Value::as_str)
.map(str::to_owned),
});
}
let secret_named = items.len();
items.sort_by(|a, b| (&a.path, &a.name, &a.key).cmp(&(&b.path, &b.name, &b.key)));
window(&mut items, 0, limit);
Ok(ConfigSecretReport {
schema: SCHEMA,
limit,
config_keys,
secret_named,
redacted,
declared,
unredacted,
redacted_not_secret_named,
files: files.len(),
items,
})
}
#[must_use]
fn glob_match(pattern: &str, path: &str) -> bool {
let pat: Vec<&str> = pattern.split('/').collect();
let seg: Vec<&str> = path.split('/').collect();
match_segments(&pat, &seg)
}
fn match_segments(pat: &[&str], seg: &[&str]) -> bool {
match pat.first() {
None => seg.is_empty(),
Some(&"**") => (0..=seg.len()).any(|i| match_segments(&pat[1..], &seg[i..])),
Some(token) => {
!seg.is_empty() && match_token(token, seg[0]) && match_segments(&pat[1..], &seg[1..])
}
}
}
fn match_token(pattern: &str, s: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let chars: Vec<char> = s.chars().collect();
match_token_chars(&pat, &chars)
}
fn match_token_chars(pat: &[char], chars: &[char]) -> bool {
match pat.first() {
None => chars.is_empty(),
Some('*') => (0..=chars.len()).any(|i| match_token_chars(&pat[1..], &chars[i..])),
Some('?') => !chars.is_empty() && match_token_chars(&pat[1..], &chars[1..]),
Some(&ch) => {
!chars.is_empty() && chars[0] == ch && match_token_chars(&pat[1..], &chars[1..])
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CouplingOrder {
#[default]
Total,
FanIn,
FanOut,
}
impl CouplingOrder {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Total => "total",
Self::FanIn => "fan_in",
Self::FanOut => "fan_out",
}
}
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
match s {
"total" => Some(Self::Total),
"fan_in" => Some(Self::FanIn),
"fan_out" => Some(Self::FanOut),
_ => None,
}
}
#[must_use]
pub fn tokens() -> [&'static str; 3] {
[
Self::Total.as_str(),
Self::FanIn.as_str(),
Self::FanOut.as_str(),
]
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CouplingItem {
pub key: String,
pub kind: String,
pub name: String,
pub path: Option<String>,
pub fan_in: u32,
pub fan_out: u32,
pub total: u32,
pub instability: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CouplingReport {
pub schema: &'static str,
pub edge_kind: &'static str,
pub order: &'static str,
pub limit: usize,
pub call_edges: usize,
pub self_calls: usize,
pub cross_language_calls: usize,
pub coupled_nodes: usize,
pub items: Vec<CouplingItem>,
}
pub fn coupling(
store: &Store,
order: CouplingOrder,
limit: usize,
) -> Result<CouplingReport, StoreError> {
let mut inbound: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut outbound: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut call_edges = 0usize;
let mut self_calls = 0usize;
let mut cross_language_calls = 0usize;
for edge in store.all_edges()? {
if edge.kind != EdgeKind::Calls {
continue;
}
call_edges += 1;
if edge.src == edge.dst {
self_calls += 1;
continue;
}
if !same_language(&edge.src, &edge.dst) {
cross_language_calls += 1;
continue;
}
inbound
.entry(edge.dst.clone())
.or_default()
.insert(edge.src.clone());
outbound.entry(edge.src).or_default().insert(edge.dst);
}
let keys: BTreeSet<&String> = inbound.keys().chain(outbound.keys()).collect();
let coupled_nodes = keys.len();
let mut ranked: Vec<(u32, u32, &String)> = keys
.into_iter()
.map(|key| {
let fan_in = count_of(&inbound, key);
let fan_out = count_of(&outbound, key);
(fan_in, fan_out, key)
})
.collect();
ranked.sort_by(|a, b| {
let metric = |&(fan_in, fan_out, _): &(u32, u32, &String)| match order {
CouplingOrder::Total => fan_in + fan_out,
CouplingOrder::FanIn => fan_in,
CouplingOrder::FanOut => fan_out,
};
metric(b).cmp(&metric(a)).then_with(|| a.2.cmp(b.2))
});
window(&mut ranked, 0, limit);
let mut items = Vec::with_capacity(ranked.len());
for (fan_in, fan_out, key) in ranked {
let Some(node) = store.get_node(key)? else {
continue;
};
let total = fan_in + fan_out;
items.push(CouplingItem {
key: node.key,
kind: node.kind.as_str().to_owned(),
name: node.name,
path: node.path,
fan_in,
fan_out,
total,
instability: round2(f64::from(fan_out) / f64::from(total)),
});
}
Ok(CouplingReport {
schema: SCHEMA,
edge_kind: EdgeKind::Calls.as_str(),
order: order.as_str(),
limit,
call_edges,
self_calls,
cross_language_calls,
coupled_nodes,
items,
})
}
fn sym_lang(key: &str) -> Option<&str> {
let rest = key.strip_prefix("sym:")?;
let (lang, _) = rest.split_once(':')?;
(!lang.is_empty()).then_some(lang)
}
fn same_language(src: &str, dst: &str) -> bool {
match (sym_lang(src), sym_lang(dst)) {
(Some(a), Some(b)) => a == b,
_ => true,
}
}
fn count_of(map: &BTreeMap<String, BTreeSet<String>>, key: &str) -> u32 {
map.get(key)
.map_or(0, |set| u32::try_from(set.len()).unwrap_or(u32::MAX))
}
fn round2(v: f64) -> f64 {
(v * 100.0).round() / 100.0
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PathHop {
pub kind: String,
pub provenance: &'static str,
pub confidence: Option<f64>,
pub direction: &'static str,
pub node: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Path {
pub schema: &'static str,
pub from: String,
pub to: String,
pub found: bool,
pub length: usize,
pub hops: Vec<PathHop>,
}
fn out_ref(edge: &Edge) -> EdgeRef {
EdgeRef {
kind: edge.kind.as_str().to_owned(),
provenance: edge.provenance.as_str(),
confidence: edge.confidence,
node: edge.dst.clone(),
}
}
fn in_ref(edge: &Edge) -> EdgeRef {
EdgeRef {
kind: edge.kind.as_str().to_owned(),
provenance: edge.provenance.as_str(),
confidence: edge.confidence,
node: edge.src.clone(),
}
}
fn sort_refs(refs: &mut [EdgeRef]) {
refs.sort_by(|a, b| (&a.kind, &a.node, a.provenance).cmp(&(&b.kind, &b.node, b.provenance)));
}
pub fn explain(store: &Store, key: &str) -> Result<Option<Explanation>, StoreError> {
let Some(node) = store.get_node(key)? else {
return Ok(None);
};
let mut outgoing: Vec<EdgeRef> = store.edges_from(key)?.iter().map(out_ref).collect();
let mut incoming: Vec<EdgeRef> = store.edges_to(key)?.iter().map(in_ref).collect();
sort_refs(&mut outgoing);
sort_refs(&mut incoming);
Ok(Some(Explanation {
schema: SCHEMA,
node: NodeSummary::from_node(&node),
meta: node.meta,
outgoing,
incoming,
}))
}
pub fn list_kind(store: &Store, kind: &NodeKind) -> Result<Listing, StoreError> {
let nodes = store
.nodes_by_kind(kind)?
.iter()
.map(NodeSummary::from_node)
.collect();
Ok(Listing {
schema: SCHEMA,
kind: kind.as_str().to_owned(),
nodes,
})
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SearchHit {
pub score: u32,
#[serde(flatten)]
pub node: NodeSummary,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
}
const SNIPPET_MAX: usize = 300;
fn content_snippet(meta: &serde_json::Value) -> Option<String> {
let content = meta.get("content").and_then(|v| v.as_str())?;
let mut collapsed: Vec<char> = Vec::with_capacity(SNIPPET_MAX + 1);
let mut pending_space = false;
for ch in content.chars() {
if ch.is_whitespace() {
pending_space = !collapsed.is_empty();
continue;
}
if pending_space {
collapsed.push(' ');
pending_space = false;
if collapsed.len() > SNIPPET_MAX {
break;
}
}
collapsed.push(ch);
if collapsed.len() > SNIPPET_MAX {
break;
}
}
if collapsed.is_empty() {
return None;
}
if collapsed.len() > SNIPPET_MAX {
let snippet: String = collapsed[..SNIPPET_MAX - 1].iter().collect();
Some(format!("{snippet}…"))
} else {
Some(collapsed.into_iter().collect())
}
}
pub fn search(store: &Store, query: &str, limit: usize) -> Result<Vec<SearchHit>, StoreError> {
let q = query.trim().to_lowercase();
let tokens: Vec<&str> = q.split("::").flat_map(str::split_whitespace).collect();
if tokens.is_empty() {
return Ok(Vec::new());
}
let mut hits: Vec<SearchHit> = Vec::new();
for node in store.all_nodes()? {
let name = node.name.to_lowercase();
let key = node.key.to_lowercase();
let path = node.path.as_deref().unwrap_or("").to_lowercase();
let content = node
.meta
.get("content")
.and_then(|v| v.as_str())
.map(str::to_lowercase);
let content = content.as_deref().unwrap_or("");
if !tokens
.iter()
.all(|t| name.contains(t) || key.contains(t) || path.contains(t) || content.contains(t))
{
continue;
}
let mut relevance: i32 = 0;
if name == q {
relevance += 100;
} else if name.contains(&q) {
relevance += 60;
} else if content.contains(&q) {
relevance += 25;
}
for t in &tokens {
if name.contains(t) {
relevance += 12;
} else if key.contains(t) {
relevance += 6;
} else if content.contains(t) {
relevance += 8;
} else if path.contains(t) {
relevance += 3;
}
}
if node.provenance == Provenance::Authored {
relevance += 40;
}
if is_overview_path(&path) {
relevance += 30;
}
if is_test_path(&path) {
relevance -= 60;
}
hits.push(SearchHit {
score: u32::try_from(relevance.max(0)).unwrap_or(0),
snippet: content_snippet(&node.meta),
node: NodeSummary::from_node(&node),
});
}
hits.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(|| a.node.key.cmp(&b.node.key))
});
window(&mut hits, 0, limit);
Ok(hits)
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct GeneratedHit {
pub score: u32,
pub generated: bool,
pub producer: String,
pub model: String,
pub kind: &'static str,
pub blob: String,
pub path: String,
pub snippet: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct MemoryHit {
pub score: u32,
pub memory: bool,
pub id: i64,
pub kind: &'static str,
pub scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor: Option<String>,
pub anchor_state: &'static str,
pub applies: bool,
pub evidence: f64,
pub snippet: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SearchResults {
pub schema: &'static str,
pub hits: Vec<SearchHit>,
pub generated: Vec<GeneratedHit>,
pub memory: Vec<MemoryHit>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SearchOptions {
pub limit: usize,
pub include_generated: bool,
pub include_memory: bool,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
limit: 10,
include_generated: false,
include_memory: false,
}
}
}
pub fn search_channels(
store: &Store,
query: &str,
opts: SearchOptions,
) -> Result<SearchResults, StoreError> {
let hits = search(store, query, opts.limit)?;
let generated = if opts.include_generated {
search_generated(store, query, opts.limit)?
} else {
Vec::new()
};
let memory = if opts.include_memory {
search_memory(store, query, opts.limit)?
} else {
Vec::new()
};
Ok(SearchResults {
schema: SCHEMA,
hits,
generated,
memory,
})
}
fn search_memory(store: &Store, query: &str, limit: usize) -> Result<Vec<MemoryHit>, StoreError> {
let q = query.trim().to_lowercase();
let tokens: Vec<&str> = q.split("::").flat_map(str::split_whitespace).collect();
if tokens.is_empty() {
return Ok(Vec::new());
}
let recalled = store.recall_memory(&crate::RecallOptions {
query: Some(query),
decay: crate::Decay::None,
..crate::RecallOptions::default()
})?;
let mut hits: Vec<MemoryHit> = recalled
.results
.into_iter()
.map(|r| {
let body = r.record.body.to_lowercase();
let anchor = r
.record
.anchor
.as_ref()
.map(|a| a.key.to_lowercase())
.unwrap_or_default();
MemoryHit {
score: memory_score(&q, &tokens, &body, &anchor, r.score),
memory: true,
id: r.record.id,
kind: r.record.kind.as_str(),
scope: r.record.scope.clone(),
anchor: r.record.anchor.as_ref().map(|a| a.key.clone()),
anchor_state: r.record.anchor_state.as_str(),
applies: r.record.applies,
evidence: r.score,
snippet: content_snippet(&serde_json::json!({ "content": r.record.body })),
}
})
.collect();
hits.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| b.id.cmp(&a.id)));
window(&mut hits, 0, limit);
Ok(hits)
}
fn memory_score(q: &str, tokens: &[&str], body: &str, anchor: &str, evidence: f64) -> u32 {
let mut relevance: i32 = 0;
if body.contains(q) {
relevance += 25;
}
for t in tokens {
if body.contains(t) {
relevance += 8;
} else if anchor.contains(t) {
relevance += 3;
}
}
let weighted = f64::from(relevance.max(0)) * evidence.clamp(0.0, 1.0);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the product of a small non-negative relevance and a weight in [0, 1]"
)]
let score = weighted.round() as u32;
score
}
fn search_generated(
store: &Store,
query: &str,
limit: usize,
) -> Result<Vec<GeneratedHit>, StoreError> {
let q = query.trim().to_lowercase();
let tokens: Vec<&str> = q.split("::").flat_map(str::split_whitespace).collect();
if tokens.is_empty() {
return Ok(Vec::new());
}
let mut hits: Vec<GeneratedHit> = Vec::new();
for record in store.media_records(&crate::MediaFilter::default())? {
let Some(generated_text) = record.outcome.text() else {
continue;
};
let text = generated_text.to_lowercase();
let path = record.path.to_lowercase();
if !tokens.iter().all(|t| text.contains(t) || path.contains(t)) {
continue;
}
hits.push(GeneratedHit {
score: generated_score(&q, &tokens, &text, &path),
generated: true,
producer: record.producer_id.to_string(),
model: record.producer.model.clone(),
kind: record.producer.kind.as_str(),
blob: record.blob_id.clone(),
path: record.path.clone(),
snippet: content_snippet(&serde_json::json!({ "content": generated_text })),
});
}
hits.sort_by(|a, b| {
b.score
.cmp(&a.score)
.then_with(|| (&a.producer, &a.blob).cmp(&(&b.producer, &b.blob)))
});
window(&mut hits, 0, limit);
Ok(hits)
}
fn generated_score(q: &str, tokens: &[&str], text: &str, path: &str) -> u32 {
let mut relevance: i32 = 0;
if text.contains(q) {
relevance += 25;
}
for t in tokens {
if text.contains(t) {
relevance += 8;
} else if path.contains(t) {
relevance += 3;
}
}
u32::try_from(relevance.max(0)).unwrap_or(0)
}
fn is_overview_path(path: &str) -> bool {
path.rsplit('/')
.next()
.is_some_and(|base| base.starts_with("readme") || base.starts_with("overview"))
}
fn is_test_path(path: &str) -> bool {
path.contains("/tests/") || path.contains("/test/")
}
struct Step {
node: String,
hop: PathHop,
}
fn steps_from(store: &Store, key: &str) -> Result<Vec<Step>, StoreError> {
let mut steps = Vec::new();
for edge in store.edges_from(key)? {
steps.push(Step {
node: edge.dst.clone(),
hop: hop(&edge, "outgoing", edge.dst.clone()),
});
}
for edge in store.edges_to(key)? {
steps.push(Step {
node: edge.src.clone(),
hop: hop(&edge, "incoming", edge.src.clone()),
});
}
steps.sort_by(|a, b| {
(&a.node, &a.hop.kind, a.hop.provenance, a.hop.direction).cmp(&(
&b.node,
&b.hop.kind,
b.hop.provenance,
b.hop.direction,
))
});
Ok(steps)
}
fn hop(edge: &Edge, direction: &'static str, node: String) -> PathHop {
PathHop {
kind: edge.kind.as_str().to_owned(),
provenance: edge.provenance.as_str(),
confidence: edge.confidence,
direction,
node,
}
}
pub fn path(store: &Store, from: &str, to: &str) -> Result<Path, StoreError> {
let not_found = |found: bool, hops: Vec<PathHop>| Path {
schema: SCHEMA,
from: from.to_owned(),
to: to.to_owned(),
found,
length: hops.len(),
hops,
};
if store.get_node(from)?.is_none() || store.get_node(to)?.is_none() {
return Ok(not_found(false, Vec::new()));
}
if from == to {
return Ok(not_found(true, Vec::new()));
}
let mut came_from: BTreeMap<String, (String, PathHop)> = BTreeMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
queue.push_back(from.to_owned());
came_from.insert(from.to_owned(), (String::new(), placeholder_hop()));
while let Some(current) = queue.pop_front() {
if current == to {
break;
}
for step in steps_from(store, ¤t)? {
if came_from.contains_key(&step.node) {
continue;
}
came_from.insert(step.node.clone(), (current.clone(), step.hop));
queue.push_back(step.node);
}
}
let mut hops = Vec::new();
let mut cursor = to.to_owned();
while cursor != from {
let Some((prev, hop)) = came_from.get(&cursor) else {
return Ok(not_found(false, Vec::new()));
};
hops.push(hop.clone());
cursor = prev.clone();
}
hops.reverse();
Ok(not_found(true, hops))
}
fn placeholder_hop() -> PathHop {
PathHop {
kind: String::new(),
provenance: "derived",
confidence: None,
direction: "outgoing",
node: String::new(),
}
}
#[cfg(test)]
mod tests {
use super::{
ConfigSecretReport, CouplingItem, CouplingOrder, CouplingReport, DebtDensityReport,
DensityItem, DensityOrder, RedactionState, SCHEMA, SNIPPET_MAX, SearchOptions,
SearchResults, config_secrets, coupling, debt_density, explain, glob_match, list_kind,
memory_score, path, search, search_channels, window,
};
use crate::{AnchorState, Edge, EdgeKind, FactSet, Node, NodeKind, Store};
fn seeded() -> Store {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main"))
.with_node(Node::new("sym:rust:a.rs#helper", NodeKind::Fn, "helper"))
.with_node(Node::new("adr:0001", NodeKind::Adr, "Build Roteiro"))
.with_edge(Edge::derived(
"sym:rust:a.rs#main",
"sym:rust:a.rs#helper",
EdgeKind::Calls,
))
.with_edge(Edge::authored(
"adr:0001",
"sym:rust:a.rs#main",
EdgeKind::References,
));
store.apply_factset(&facts).expect("apply");
store
}
#[test]
fn window_reads_zero_as_unlimited_and_offsets_before_limiting() {
let ten = || (0..10).collect::<Vec<u8>>();
let mut all = ten();
window(&mut all, 0, 0);
assert_eq!(all, ten(), "limit 0 keeps everything");
let mut top = ten();
window(&mut top, 0, 3);
assert_eq!(top, vec![0, 1, 2]);
let mut exact = ten();
window(&mut exact, 0, 10);
assert_eq!(exact, ten());
let mut over = ten();
window(&mut over, 0, 99);
assert_eq!(over, ten());
let mut paged = ten();
window(&mut paged, 4, 3);
assert_eq!(paged, vec![4, 5, 6]);
let mut rest = ten();
window(&mut rest, 7, 0);
assert_eq!(rest, vec![7, 8, 9], "offset then unlimited");
let mut at_end = ten();
window(&mut at_end, 10, 0);
assert!(at_end.is_empty());
let mut past_end = ten();
window(&mut past_end, 500, 0);
assert!(past_end.is_empty());
let mut past_end_limited = ten();
window(&mut past_end_limited, 500, 5);
assert!(past_end_limited.is_empty());
let mut empty: Vec<u8> = Vec::new();
window(&mut empty, 0, 0);
window(&mut empty, 3, 0);
window(&mut empty, 0, 3);
assert!(empty.is_empty());
}
#[test]
fn search_ranks_by_relevance_and_is_bounded() {
let store = seeded();
let hits = search(&store, "helper", 10).expect("search");
assert_eq!(hits[0].node.key, "sym:rust:a.rs#helper");
assert!(hits[0].score >= 100, "exact name match scores high");
assert!(
search(&store, "main roteiro", 10)
.expect("search")
.is_empty()
);
let by_prefix = search(&store, "sym:rust", 10).expect("search");
assert!(!by_prefix.is_empty());
assert!(
by_prefix
.iter()
.all(|h| h.node.key.starts_with("sym:rust:"))
);
assert!(search(&store, " ", 10).expect("search").is_empty());
assert!(search(&store, "a.rs", 1).expect("search").len() <= 1);
}
fn three_channels(population: usize) -> Store {
use crate::{
GeneratedContent, MediaKind, MediaOutcome, MediaWrite, MemoryKind, MemoryWrite,
Producer,
};
let mut store = Store::open_in_memory().expect("store");
let mut facts = FactSet::new();
for i in 0..population {
facts = facts.with_node(Node::new(
format!("sym:rust:a.rs#quokka{i}"),
NodeKind::Fn,
format!("quokka{i}"),
));
}
store.apply_factset(&facts).expect("apply");
let producer = Producer {
kind: MediaKind::Audio,
model: "voxtral-mini-3b".to_owned(),
model_digest: "4705be8e".to_owned(),
quantisation: "Q4_K_M".to_owned(),
mmproj_digest: "4f24c4ef".to_owned(),
prompt: "Transcribe this audio recording.".to_owned(),
temperature: 0.0,
max_tokens: 512,
};
for i in 0..population {
store
.record_memory(&MemoryWrite {
scope: crate::DEFAULT_MEMORY_SCOPE,
kind: MemoryKind::Lesson,
anchor: None,
body: &format!("wombat lesson number {i}"),
confidence: None,
supersedes: None,
})
.expect("memory write");
assert!(
store
.record_media_content(&MediaWrite {
blob_id: &format!("blob-{i}"),
path: &format!("assets/clip{i}.wav"),
producer: &producer,
tool_version: "0.0.0",
outcome: &MediaOutcome::Generated(GeneratedContent {
text: format!("narwhal transcript number {i}"),
confidence: None,
}),
replace: false,
})
.expect("media write"),
"each clip is a fresh record",
);
}
store
}
fn all_channels(store: &Store, query: &str, limit: usize) -> SearchResults {
search_channels(
store,
query,
SearchOptions {
limit,
include_generated: true,
include_memory: true,
},
)
.expect("search")
}
#[test]
fn search_reads_zero_as_unlimited_and_only_removes_the_cut() {
const POPULATION: usize = 12;
let store = three_channels(POPULATION);
let bounded = search(&store, "quokka", 10).expect("search");
assert_eq!(bounded.len(), 10, "a positive limit still cuts");
let unlimited = search(&store, "quokka", 0).expect("search");
assert_eq!(unlimited.len(), POPULATION, "0 is every match");
assert_eq!(
unlimited[..10]
.iter()
.map(|h| h.node.key.as_str())
.collect::<Vec<_>>(),
bounded
.iter()
.map(|h| h.node.key.as_str())
.collect::<Vec<_>>(),
"unlimited only removes the cut",
);
}
#[test]
fn each_search_channel_reads_zero_as_unlimited_over_its_own_population() {
const POPULATION: usize = 12;
let store = three_channels(POPULATION);
assert_eq!(all_channels(&store, "quokka", 10).hits.len(), 10);
assert_eq!(
all_channels(&store, "quokka", 0).hits.len(),
POPULATION,
"graph channel: 0 is unlimited",
);
assert_eq!(all_channels(&store, "wombat", 10).memory.len(), 10);
assert_eq!(
all_channels(&store, "wombat", 0).memory.len(),
POPULATION,
"memory channel: 0 is unlimited",
);
assert_eq!(all_channels(&store, "narwhal", 10).generated.len(), 10);
assert_eq!(
all_channels(&store, "narwhal", 0).generated.len(),
POPULATION,
"generated channel: 0 is unlimited",
);
let graph_only = all_channels(&store, "quokka", 0);
assert!(
graph_only.memory.is_empty() && graph_only.generated.is_empty(),
"unlimited is per channel, not a merged population",
);
}
#[test]
fn a_tokenless_query_is_nothing_in_every_channel_at_every_limit() {
let store = three_channels(12);
for blank in ["", " ", "\t\n"] {
for limit in [0, 10] {
let nothing = all_channels(&store, blank, limit);
assert!(
nothing.hits.is_empty()
&& nothing.generated.is_empty()
&& nothing.memory.is_empty(),
"a query with no tokens is nothing, not everything ({blank:?}, limit {limit})",
);
}
}
}
#[test]
fn search_prefers_curated_content_over_same_named_test_symbols() {
use crate::Provenance;
let mut store = Store::open_in_memory().expect("store");
let mut test_fn = Node::new(
"sym:rust:crates/x/tests/cli.rs#roteiro",
NodeKind::Fn,
"roteiro",
);
test_fn.path = Some("crates/x/tests/cli.rs".into());
let mut adr = Node::new("adr:0001", NodeKind::Adr, "Build Roteiro")
.with_provenance(Provenance::Authored);
adr.path = Some("docs/adr/0001.md".into());
adr.meta = serde_json::json!({ "content": "Roteiro is a provenance-tagged codebase knowledge graph." });
let mut readme = Node::new("file:README.md", NodeKind::File, "README.md");
readme.path = Some("README.md".into());
readme.meta =
serde_json::json!({ "content": "Roteiro turns a repo into one knowledge graph." });
store
.apply_factset(
&FactSet::new()
.with_node(test_fn)
.with_node(adr)
.with_node(readme),
)
.expect("apply");
let hits = search(&store, "roteiro", 10).expect("search");
let keys: Vec<&str> = hits.iter().map(|h| h.node.key.as_str()).collect();
let idx = |k: &str| keys.iter().position(|x| *x == k).expect("present");
assert!(
idx("adr:0001") < idx("sym:rust:crates/x/tests/cli.rs#roteiro"),
"authored ADR outranks the test symbol: {keys:?}"
);
assert!(
idx("file:README.md") < idx("sym:rust:crates/x/tests/cli.rs#roteiro"),
"README (matched via content) outranks the test symbol: {keys:?}"
);
let by_content = search(&store, "provenance-tagged", 10).expect("search");
assert_eq!(
by_content.first().map(|h| h.node.key.as_str()),
Some("adr:0001"),
"content search matches the ADR by its captured text"
);
}
#[test]
fn search_hit_carries_a_bounded_content_snippet() {
use crate::Provenance;
let mut store = Store::open_in_memory().expect("store");
let long = "word ".repeat(200);
let mut adr =
Node::new("adr:0001", NodeKind::Adr, "Overview").with_provenance(Provenance::Authored);
adr.meta = serde_json::json!({ "content": format!("Roteiro is\n\na graph. {long}") });
let sym = Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main");
store
.apply_factset(&FactSet::new().with_node(adr).with_node(sym))
.expect("apply");
let hits = search(&store, "roteiro", 10).expect("search");
let adr_hit = hits
.iter()
.find(|h| h.node.key == "adr:0001")
.expect("adr hit");
let snippet = adr_hit
.snippet
.as_deref()
.expect("a content-bearing node yields a snippet");
assert!(snippet.starts_with("Roteiro is a graph."), "got: {snippet}");
assert!(!snippet.contains(" "));
assert!(!snippet.contains('\n'));
assert!(
snippet.chars().count() <= SNIPPET_MAX,
"snippet is bounded: {} chars",
snippet.chars().count()
);
assert!(
snippet.ends_with('…'),
"over-long content is truncated with an ellipsis"
);
let hits = search(&store, "main", 10).expect("search");
let sym_hit = hits
.iter()
.find(|h| h.node.key == "sym:rust:a.rs#main")
.expect("sym hit");
assert!(
sym_hit.snippet.is_none(),
"a node with no content has no snippet"
);
}
#[test]
fn explain_reports_labelled_neighbourhood() {
let store = seeded();
let ex = explain(&store, "sym:rust:a.rs#main")
.expect("query")
.expect("present");
assert_eq!(ex.schema, SCHEMA);
assert_eq!(ex.node.kind, "fn");
assert_eq!(ex.outgoing.len(), 1);
assert_eq!(ex.outgoing[0].kind, "calls");
assert_eq!(ex.outgoing[0].provenance, "derived");
assert_eq!(ex.outgoing[0].node, "sym:rust:a.rs#helper");
assert_eq!(ex.incoming.len(), 1);
assert_eq!(ex.incoming[0].provenance, "authored");
assert_eq!(ex.incoming[0].node, "adr:0001");
}
#[test]
fn explain_missing_node_is_none() {
let store = seeded();
assert!(explain(&store, "sym:rust:a.rs#ghost").expect("q").is_none());
}
#[test]
fn edges_differing_only_in_provenance_are_ordered() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new("a", NodeKind::Fn, "a"))
.with_node(Node::new("b", NodeKind::Fn, "b"))
.with_edge(Edge::derived("a", "b", EdgeKind::References))
.with_edge(Edge::authored("a", "b", EdgeKind::References));
store.apply_factset(&facts).expect("apply");
let ex = explain(&store, "a").expect("q").expect("present");
let provs: Vec<_> = ex.outgoing.iter().map(|e| e.provenance).collect();
assert_eq!(provs, ["authored", "derived"]);
}
#[test]
fn list_kind_is_ordered() {
let store = seeded();
let listing = list_kind(&store, &NodeKind::Fn).expect("list");
let keys: Vec<_> = listing.nodes.iter().map(|n| n.key.as_str()).collect();
assert_eq!(keys, ["sym:rust:a.rs#helper", "sym:rust:a.rs#main"]);
}
#[test]
fn json_schema_is_stable() {
let store = seeded();
let ex = explain(&store, "adr:0001").expect("q").expect("present");
let json = serde_json::to_value(&ex).expect("json");
assert_eq!(json["schema"], SCHEMA);
assert_eq!(json["node"]["key"], "adr:0001");
assert_eq!(json["node"]["kind"], "adr");
assert_eq!(json["outgoing"][0]["kind"], "references");
assert_eq!(json["outgoing"][0]["provenance"], "authored");
assert_eq!(json["outgoing"][0]["node"], "sym:rust:a.rs#main");
assert!(json["outgoing"][0]["confidence"].is_null());
}
#[test]
fn path_crosses_provenance_and_direction() {
let store = seeded();
let p = path(&store, "adr:0001", "sym:rust:a.rs#helper").expect("path");
assert!(p.found);
assert_eq!(p.length, 2);
assert_eq!(p.schema, SCHEMA);
assert_eq!(p.hops[0].kind, "references");
assert_eq!(p.hops[0].provenance, "authored");
assert_eq!(p.hops[0].direction, "outgoing");
assert_eq!(p.hops[0].node, "sym:rust:a.rs#main");
assert_eq!(p.hops[1].kind, "calls");
assert_eq!(p.hops[1].provenance, "derived");
assert_eq!(p.hops[1].node, "sym:rust:a.rs#helper");
}
#[test]
fn path_follows_edges_against_direction() {
let store = seeded();
let p = path(&store, "sym:rust:a.rs#helper", "adr:0001").expect("path");
assert!(p.found);
assert_eq!(p.length, 2);
assert!(p.hops.iter().all(|h| h.direction == "incoming"));
assert_eq!(p.hops.last().unwrap().node, "adr:0001");
}
#[test]
fn path_same_node_is_trivial() {
let store = seeded();
let p = path(&store, "adr:0001", "adr:0001").expect("path");
assert!(p.found);
assert_eq!(p.length, 0);
assert!(p.hops.is_empty());
}
#[test]
fn path_missing_endpoint_or_unreachable_is_not_found() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new("a", NodeKind::Fn, "a"))
.with_node(Node::new("b", NodeKind::Fn, "b"))
.with_node(Node::new("island", NodeKind::Fn, "island"))
.with_edge(Edge::derived("a", "b", EdgeKind::Calls));
store.apply_factset(&facts).expect("apply");
let missing = path(&store, "a", "ghost").expect("path");
assert!(!missing.found);
assert!(missing.hops.is_empty());
let unreachable = path(&store, "a", "island").expect("path");
assert!(!unreachable.found);
assert!(unreachable.hops.is_empty());
}
#[test]
fn path_is_shortest() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(Node::new("a", NodeKind::Fn, "a"))
.with_node(Node::new("b", NodeKind::Fn, "b"))
.with_node(Node::new("c", NodeKind::Fn, "c"))
.with_node(Node::new("d", NodeKind::Fn, "d"))
.with_edge(Edge::derived("a", "b", EdgeKind::Calls))
.with_edge(Edge::derived("b", "c", EdgeKind::Calls))
.with_edge(Edge::derived("c", "d", EdgeKind::Calls))
.with_edge(Edge::derived("a", "d", EdgeKind::Calls));
store.apply_factset(&facts).expect("apply");
let p = path(&store, "a", "d").expect("path");
assert!(p.found);
assert_eq!(p.length, 1, "the direct a->d edge is the shortest path");
assert_eq!(p.hops[0].node, "d");
}
#[test]
fn glob_matches_segments_and_wildcards() {
assert!(glob_match("vendor/**", "vendor/lib/a.rs"));
assert!(glob_match("vendor/**", "vendor")); assert!(glob_match("**/generated/*", "src/gen/generated/x.rs"));
assert!(glob_match("**/*.rs", "a/b/c.rs"));
assert!(glob_match("src/*.rs", "src/main.rs"));
assert!(!glob_match("src/*.rs", "src/sub/main.rs"));
assert!(glob_match("a?c.rs", "abc.rs"));
assert!(!glob_match("a?c.rs", "ac.rs"));
assert!(!glob_match("generated", "src/generated"));
assert!(!glob_match("vendor/**", "third_party/vendor/a.rs"));
}
#[test]
fn the_evidence_weight_is_closed_at_both_ends() {
let score = |evidence| memory_score("batch", &["batch"], "a batch cursor", "", evidence);
let full = score(1.0);
assert!(full > 0, "a fully-evidenced hit scores");
assert_eq!(score(0.0), 0, "and a zero weight takes it to zero");
assert!(
score(0.5) < full && score(0.5) > 0,
"in between, in between"
);
let worst_inferable = [
AnchorState::Valid,
AnchorState::Unanchored,
AnchorState::Unverifiable,
AnchorState::Vanished,
AnchorState::Drifted,
]
.into_iter()
.map(crate::anchor_penalty)
.fold(f64::INFINITY, f64::min);
assert!(
score(worst_inferable) > 0,
"the most demoted anchor state ({worst_inferable}) must not silence a hit",
);
assert_eq!(score(2.0), full, "clamped at the top");
assert_eq!(score(-1.0), 0, "and at the bottom");
}
fn coupled() -> Store {
let mut store = Store::open_in_memory().expect("store");
let mut facts = FactSet::new();
for name in ["hub", "spread", "a", "b", "x", "y"] {
facts = facts.with_node(Node::new(
format!("sym:rust:a.rs#{name}"),
NodeKind::Fn,
name,
));
}
for (src, dst) in [("a", "hub"), ("b", "hub"), ("spread", "x"), ("spread", "y")] {
facts = facts.with_edge(Edge::derived(
format!("sym:rust:a.rs#{src}"),
format!("sym:rust:a.rs#{dst}"),
EdgeKind::Calls,
));
}
store.apply_factset(&facts).expect("apply");
store
}
fn item<'a>(report: &'a CouplingReport, name: &str) -> &'a CouplingItem {
report
.items
.iter()
.find(|i| i.name == name)
.unwrap_or_else(|| panic!("`{name}` missing from {:?}", report.items))
}
#[test]
fn coupling_keeps_the_direction_an_undirected_degree_discards() {
let report = coupling(&coupled(), CouplingOrder::Total, 0).expect("coupling");
let hub = item(&report, "hub");
let spread = item(&report, "spread");
assert_eq!(hub.total, spread.total, "same total coupling");
assert_eq!((hub.fan_in, hub.fan_out), (2, 0), "hub is depended upon");
assert_eq!(
(spread.fan_in, spread.fan_out),
(0, 2),
"spread depends on others"
);
assert!(
(hub.instability - 0.0).abs() < f64::EPSILON,
"a purely called node is maximally stable: {}",
hub.instability
);
assert!(
(spread.instability - 1.0).abs() < f64::EPSILON,
"a purely calling node is maximally unstable: {}",
spread.instability
);
assert_eq!(report.edge_kind, "calls");
assert_eq!(report.coupled_nodes, 6);
assert_eq!(report.call_edges, 4);
assert_eq!(report.self_calls, 0);
assert_eq!(report.cross_language_calls, 0);
}
#[test]
fn coupling_excludes_cross_language_name_collisions() {
let mut store = coupled();
let mut facts = FactSet::new().with_node(Node::new(
"sym:javascript:app.js#render",
NodeKind::Fn,
"render",
));
facts = facts.with_edge(Edge::derived(
"sym:javascript:app.js#render",
"sym:rust:a.rs#hub",
EdgeKind::Calls,
));
store.apply_factset(&facts).expect("apply");
let report = coupling(&store, CouplingOrder::Total, 0).expect("coupling");
assert_eq!(
item(&report, "hub").fan_in,
2,
"a JavaScript caller is not a dependant of a Rust function"
);
assert_eq!(
report.cross_language_calls, 1,
"the excluded edge is reported, not silently dropped"
);
assert_eq!(report.call_edges, 5, "and still counted as scanned");
}
#[test]
fn same_language_never_guesses_about_unknown_key_shapes() {
assert!(super::same_language("sym:rust:a.rs#f", "sym:rust:b.rs#g"));
assert!(!super::same_language(
"sym:javascript:a.js#f",
"sym:rust:b.rs#g"
));
assert!(super::same_language("file:a.md", "sym:rust:b.rs#g"));
assert!(super::same_language("sym:", "sym:rust:b.rs#g"));
assert_eq!(super::sym_lang("sym:rust:a.rs#f"), Some("rust"));
assert_eq!(
super::sym_lang("sym::a.rs#f"),
None,
"empty lang is no lang"
);
assert_eq!(super::sym_lang("marker:a.rs#7"), None);
}
#[test]
fn coupling_counts_distinct_callers_not_parallel_edges() {
let mut store = coupled();
let inferred = Edge::inferred("sym:rust:a.rs#a", "sym:rust:a.rs#hub", EdgeKind::Calls, 0.9);
store
.apply_factset(&FactSet::new().with_edge(inferred))
.expect("apply");
let report = coupling(&store, CouplingOrder::Total, 0).expect("coupling");
assert_eq!(
item(&report, "hub").fan_in,
2,
"the same caller at two provenances is one dependant, not two"
);
assert_eq!(
report.call_edges, 5,
"the extra edge is reported as scanned"
);
}
#[test]
fn coupling_excludes_self_calls_from_both_fans() {
let mut store = coupled();
let recursive = Edge::derived("sym:rust:a.rs#hub", "sym:rust:a.rs#hub", EdgeKind::Calls);
store
.apply_factset(&FactSet::new().with_edge(recursive))
.expect("apply");
let report = coupling(&store, CouplingOrder::Total, 0).expect("coupling");
let hub = item(&report, "hub");
assert_eq!(
(hub.fan_in, hub.fan_out),
(2, 0),
"recursion changes neither fan"
);
assert_eq!(report.self_calls, 1, "but it is reported, not dropped");
}
#[test]
fn coupling_order_picks_the_question_being_asked() {
let store = coupled();
let top = |order| {
coupling(&store, order, 1).expect("coupling").items[0]
.name
.clone()
};
assert_eq!(top(CouplingOrder::FanIn), "hub", "most depended-on");
assert_eq!(top(CouplingOrder::FanOut), "spread", "reaches furthest");
let by_total = coupling(&store, CouplingOrder::Total, 2).expect("coupling");
assert_eq!(
by_total.items.iter().map(|i| &i.name).collect::<Vec<_>>(),
["hub", "spread"],
"ties break by key ascending"
);
}
#[test]
fn coupling_reports_truncation_and_is_deterministic() {
let store = coupled();
let capped = coupling(&store, CouplingOrder::Total, 2).expect("coupling");
assert_eq!(capped.items.len(), 2);
assert_eq!(
capped.coupled_nodes, 6,
"the population is reported, so a capped list cannot read as the whole graph"
);
assert_eq!(capped.limit, 2);
let a = serde_json::to_string(&capped).expect("json");
let b =
serde_json::to_string(&coupling(&store, CouplingOrder::Total, 2).expect("coupling"))
.expect("json");
assert_eq!(a, b, "deterministic serialisation");
}
#[test]
fn coupling_ignores_edge_kinds_whose_direction_is_not_a_call() {
let mut store = coupled();
let mut facts = FactSet::new().with_node(Node::new("adr:0001", NodeKind::Adr, "A"));
facts = facts.with_edge(Edge::authored(
"adr:0001",
"sym:rust:a.rs#hub",
EdgeKind::References,
));
store.apply_factset(&facts).expect("apply");
let report = coupling(&store, CouplingOrder::Total, 0).expect("coupling");
assert_eq!(item(&report, "hub").fan_in, 2, "a reference is not a call");
assert!(
!report.items.iter().any(|i| i.key == "adr:0001"),
"a node with no call edges is not in the population: {:?}",
report.items
);
assert_eq!(report.call_edges, 4);
}
#[test]
fn coupling_order_tokens_round_trip() {
for token in CouplingOrder::tokens() {
let order = CouplingOrder::from_token(token)
.unwrap_or_else(|| panic!("`{token}` is advertised but not accepted"));
assert_eq!(order.as_str(), token);
}
assert!(
CouplingOrder::from_token("degree").is_none(),
"an unknown order is rejected, not silently defaulted"
);
}
fn file_of(path: &str, lines: u64) -> Node {
let mut node = Node::new(format!("file:{path}"), NodeKind::File, path);
node.path = Some(path.to_owned());
node.meta = serde_json::json!({ "bytes": lines * 30, "lines": lines });
node
}
fn marker_of(path: &str, line: u32, category: &str) -> Node {
let mut node = Node::new(
format!("marker:{path}#{line}"),
NodeKind::Marker,
format!("TODO {line}"), );
node.path = Some(path.to_owned());
node.meta = serde_json::json!({
"category": category,
"text": format!("TODO {line}"), "line": line,
});
node
}
fn marked() -> Store {
let mut store = Store::open_in_memory().expect("store");
let mut facts = FactSet::new()
.with_node(file_of("big.rs", 4000))
.with_node(file_of("small.rs", 200))
.with_node(file_of("tiny.rs", 10));
for line in 1..=40 {
facts = facts.with_node(marker_of("big.rs", line, "todo")); facts = facts.with_node(marker_of("small.rs", line, "todo")); }
facts = facts.with_node(marker_of("tiny.rs", 3, "stub"));
store.apply_factset(&facts).expect("apply");
store
}
fn density(store: &Store, order: DensityOrder) -> DebtDensityReport {
debt_density(store, &[], &[], order, 0, super::DEFAULT_MIN_LINES).expect("density")
}
fn at<'a>(report: &'a DebtDensityReport, path: &str) -> &'a DensityItem {
report
.items
.iter()
.find(|i| i.path == path)
.unwrap_or_else(|| panic!("`{path}` missing from {:?}", report.items))
}
#[test]
fn density_separates_files_a_raw_marker_count_cannot() {
let report = density(&marked(), DensityOrder::Density);
let big = at(&report, "big.rs");
let small = at(&report, "small.rs");
assert_eq!(big.markers, small.markers, "same raw count");
assert!(
(big.per_kloc - 10.0).abs() < f64::EPSILON,
"40 markers in 4000 lines is 10 per kloc, was {}",
big.per_kloc
);
assert!(
(small.per_kloc - 200.0).abs() < f64::EPSILON,
"40 markers in 200 lines is 200 per kloc, was {}",
small.per_kloc
);
assert_eq!(
report.items.first().map(|i| i.path.as_str()),
Some("small.rs"),
"the dense file ranks first: {:?}",
report.items
);
assert_eq!(small.by_category.get("todo"), Some(&40)); assert_eq!(report.schema, SCHEMA);
}
#[test]
fn markers_order_ranks_the_way_debt_already_does() {
let report = density(&marked(), DensityOrder::Markers);
assert_eq!(
report.items.iter().map(|i| &i.path).collect::<Vec<_>>(),
["big.rs", "small.rs"],
"equal counts tie and break on path ascending"
);
}
#[test]
fn the_short_file_floor_excludes_without_hiding() {
let report = density(&marked(), DensityOrder::Density);
assert!(
!report.items.iter().any(|i| i.path == "tiny.rs"),
"a 10-line file is not ranked: {:?}",
report.items
);
assert_eq!(report.short_files, 1, "and its exclusion is reported");
assert_eq!(
report.files_with_markers, 3,
"the population still counts it"
);
assert_eq!(report.ranked_files, 2);
assert_eq!(
report.total_markers, 81,
"and so do the totals: 40 + 40 + 1"
);
let unfloored =
debt_density(&marked(), &[], &[], DensityOrder::Density, 0, 0).expect("density");
assert_eq!(unfloored.short_files, 0);
assert_eq!(unfloored.ranked_files, 3);
let tiny = at(&unfloored, "tiny.rs").per_kloc;
assert!(
(tiny - 100.0).abs() < f64::EPSILON,
"the arithmetic the floor exists to keep out of the ranking, was {tiny}"
);
}
#[test]
fn a_file_with_no_recorded_length_is_reported_not_divided_by() {
let mut store = Store::open_in_memory().expect("store");
let mut no_lines = Node::new("file:b.rs", NodeKind::File, "b.rs");
no_lines.path = Some("b.rs".into());
no_lines.meta = serde_json::json!({ "bytes": 90 });
let facts = FactSet::new()
.with_node(marker_of("orphan.rs", 1, "todo")) .with_node(no_lines)
.with_node(marker_of("b.rs", 1, "todo")) .with_node(file_of("empty.rs", 0))
.with_node(marker_of("empty.rs", 1, "todo")); store.apply_factset(&facts).expect("apply");
let report = density(&store, DensityOrder::Density);
assert!(report.items.is_empty(), "nothing rankable: {report:?}");
assert_eq!(report.unknown_length_files, 3);
assert_eq!(
report.total_markers, 3,
"the markers are still inventoried, so the file cannot vanish silently"
);
assert!(
(report.overall_per_kloc - 0.0).abs() < f64::EPSILON,
"and no density is invented from a zero denominator"
);
}
#[test]
fn density_shares_debt_s_filters_rather_than_adding_a_second_vocabulary() {
let store = marked();
let ignored = debt_density(
&store,
&[],
&["small.rs".into()],
DensityOrder::Density,
0,
super::DEFAULT_MIN_LINES,
)
.expect("density");
assert!(
!ignored.items.iter().any(|i| i.path == "small.rs"),
"an ignored path leaves the report entirely: {:?}",
ignored.items
);
assert_eq!(
ignored.files_with_markers, 2,
"not merely unranked — it is not in the population either"
);
let stubs = debt_density(&store, &["stub".into()], &[], DensityOrder::Density, 0, 0)
.expect("density");
assert_eq!(stubs.total_markers, 1);
assert_eq!(
stubs.items.iter().map(|i| &i.path).collect::<Vec<_>>(),
["tiny.rs"]
);
}
#[test]
fn density_ranks_on_the_exact_ratio_not_the_rounded_one() {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(file_of("a.rs", 3001))
.with_node(marker_of("a.rs", 1, "todo")) .with_node(file_of("b.rs", 3000))
.with_node(marker_of("b.rs", 1, "todo")); store.apply_factset(&facts).expect("apply");
let report = density(&store, DensityOrder::Density);
assert_eq!(
report.items.iter().map(|i| &i.path).collect::<Vec<_>>(),
["b.rs", "a.rs"],
"the shorter file is denser, however the figures round"
);
assert_eq!(
(report.items[0].per_kloc, report.items[1].per_kloc),
(0.33, 0.33),
"and the rendered figures really are equal, so the order came from elsewhere"
);
}
#[test]
fn density_reports_truncation_and_is_deterministic() {
let store = marked();
let capped = debt_density(&store, &[], &[], DensityOrder::Density, 1, 0).expect("density");
assert_eq!(capped.items.len(), 1);
assert_eq!(capped.limit, 1);
assert_eq!(
capped.ranked_files, 3,
"the population is reported, so a capped list cannot read as the whole repository"
);
assert_eq!(capped.total_lines, 4210);
assert!(
(capped.overall_per_kloc - 19.24).abs() < f64::EPSILON,
"81 markers over 4210 lines, was {}",
capped.overall_per_kloc
);
let a = serde_json::to_string(&capped).expect("json");
let b = serde_json::to_string(
&debt_density(&store, &[], &[], DensityOrder::Density, 1, 0).expect("density"),
)
.expect("json");
assert_eq!(a, b, "deterministic serialisation");
}
#[test]
fn density_order_tokens_round_trip() {
for token in DensityOrder::tokens() {
let order = DensityOrder::from_token(token)
.unwrap_or_else(|| panic!("`{token}` is advertised but not accepted"));
assert_eq!(order.as_str(), token);
}
assert!(
DensityOrder::from_token("count").is_none(),
"an unknown order is rejected, not silently defaulted"
);
}
fn cfgkey(path: &str, dotted: &str, value: &str) -> Node {
let mut node = Node::new(
format!("cfgkey:{path}#{dotted}"),
NodeKind::Other("config_key".to_owned()),
dotted,
);
node.path = Some(path.to_owned());
node.meta = serde_json::json!({ "key": dotted, "value": value });
node
}
fn struct_cfgkey(path: &str, dotted: &str) -> Node {
let mut node = Node::new(
format!("cfgkey:{path}#{dotted}"),
NodeKind::Other("config_key".to_owned()),
dotted,
);
node.path = Some(path.to_owned());
node.meta = serde_json::json!({
"key": dotted,
"source": "struct",
"struct": "AppConfig",
});
node
}
fn configured() -> Store {
let mut store = Store::open_in_memory().expect("store");
let facts = FactSet::new()
.with_node(cfgkey(".env", "API_TOKEN", "<redacted>"))
.with_node(cfgkey("config.toml", "db.password", "<redacted>"))
.with_node(struct_cfgkey("src/config.rs", "serve.api_key"))
.with_node(cfgkey("config.toml", "serve.addr", "127.0.0.1:8017"))
.with_node(cfgkey("k8s/secret.yaml", "database-url", "<redacted>"));
store.apply_factset(&facts).expect("apply");
store
}
fn secret<'a>(report: &'a ConfigSecretReport, name: &str) -> &'a super::ConfigSecretItem {
report
.items
.iter()
.find(|i| i.name == name)
.unwrap_or_else(|| panic!("`{name}` missing from {:?}", report.items))
}
#[test]
fn the_inventory_reports_presence_and_redaction_not_values() {
let report = config_secrets(&configured(), 0).expect("config_secrets");
assert_eq!(report.config_keys, 5, "the population it drew from");
assert_eq!(report.secret_named, 3, "{:?}", report.items);
assert_eq!(report.files, 3);
assert_eq!(report.schema, SCHEMA);
assert_eq!(secret(&report, "API_TOKEN").path.as_deref(), Some(".env"));
assert_eq!(
secret(&report, "db.password").key,
"cfgkey:config.toml#db.password"
);
assert_eq!(
secret(&report, "API_TOKEN").state,
RedactionState::Redacted,
"the placeholder extraction wrote is recognised as a redaction"
);
assert_eq!(report.redacted, 2, "{report:?}");
let json = serde_json::to_value(&report).expect("json");
let text = serde_json::to_string(&report).expect("json");
assert!(
json["items"][0].get("value").is_none(),
"an item carries no value field: {text}"
);
assert!(
!text.contains("<redacted>"),
"not even the placeholder is echoed back: {text}"
);
assert_eq!(
report.items.iter().map(|i| &i.name).collect::<Vec<_>>(),
["API_TOKEN", "db.password", "serve.api_key"]
);
}
#[test]
fn a_struct_declared_key_is_neither_redacted_nor_a_leak() {
let report = config_secrets(&configured(), 0).expect("config_secrets");
let declared = secret(&report, "serve.api_key");
assert_eq!(declared.state, RedactionState::Declared);
assert_eq!(declared.source.as_deref(), Some("struct"));
assert_eq!(report.redacted, 2, "the two file-derived keys");
assert_eq!(report.declared, 1);
assert_eq!(
report.unredacted, 0,
"the invariant extraction maintains: {report:?}"
);
}
#[test]
fn an_unredacted_secret_named_value_is_reported_as_a_finding() {
let mut store = configured();
store
.apply_import_layer(
"other-tool",
&FactSet::new().with_node(cfgkey("imported.env", "AWS_SECRET", "AKIAnot-redacted")),
)
.expect("import");
let report = config_secrets(&store, 0).expect("config_secrets");
assert_eq!(report.unredacted, 1, "{report:?}");
assert_eq!(secret(&report, "AWS_SECRET").state, RedactionState::Present);
let text = serde_json::to_string(&report).expect("json");
assert!(
!text.contains("AKIA"),
"the value is not echoed back: {text}"
);
}
#[test]
fn a_redaction_under_an_innocuous_name_is_counted_but_not_listed() {
let report = config_secrets(&configured(), 0).expect("config_secrets");
assert_eq!(report.redacted_not_secret_named, 1);
assert!(
!report.items.iter().any(|i| i.name == "database-url"),
"not listed: {:?}",
report.items
);
assert_eq!(
report.redacted + report.redacted_not_secret_named,
3,
"and the two figures together account for every redacted value"
);
}
#[test]
fn the_inventory_cannot_see_a_credential_that_is_not_a_config_key() {
let mut store = configured();
let mut hardcoded = Node::new("sym:rust:src/main.rs#connect", NodeKind::Fn, "connect");
hardcoded.path = Some("src/main.rs".into());
hardcoded.meta = serde_json::json!({
"content": concat!("let token = \"AKIA", "IOSFODNN7EXAMPLE\";"),
});
store
.apply_factset(&FactSet::new().with_node(hardcoded))
.expect("apply");
let report = config_secrets(&store, 0).expect("config_secrets");
assert_eq!(
report.secret_named, 3,
"a hardcoded credential does not appear: {:?}",
report.items
);
assert_eq!(report.config_keys, 5, "and is not a config key at all");
}
#[test]
fn the_inventory_reports_truncation_and_is_deterministic() {
let store = configured();
let capped = config_secrets(&store, 1).expect("config_secrets");
assert_eq!(capped.items.len(), 1);
assert_eq!(capped.limit, 1);
assert_eq!(
capped.secret_named, 3,
"the population is reported, so a capped list cannot read as a clean repository"
);
assert_eq!((capped.redacted, capped.declared), (2, 1));
let a = serde_json::to_string(&capped).expect("json");
let b = serde_json::to_string(&config_secrets(&store, 1).expect("config_secrets"))
.expect("json");
assert_eq!(a, b, "deterministic serialisation");
}
#[test]
fn an_empty_report_means_no_secret_named_key_not_no_secret() {
let mut store = Store::open_in_memory().expect("store");
store
.apply_factset(&FactSet::new().with_node(cfgkey(
".env",
"DSN",
"postgres://u:pw@host/db",
)))
.expect("apply");
let report = config_secrets(&store, 0).expect("config_secrets");
assert_eq!(report.secret_named, 0, "nothing is secret-*named*");
assert_eq!(report.redacted_not_secret_named, 0);
assert_eq!(
report.config_keys, 1,
"while the graph does hold a config key with a credential in it"
);
}
#[test]
fn redaction_state_tokens_match_their_serialisation() {
for state in [
RedactionState::Redacted,
RedactionState::Declared,
RedactionState::Present,
] {
let json = serde_json::to_string(&state).expect("json");
assert_eq!(json, format!("\"{}\"", state.as_str()));
}
}
}