use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::cache::ScanCache;
use crate::extract::{Symbol, Vis};
use crate::frontmatter::{self, Frontmatter};
use crate::graph::Ranking;
use crate::place::Placement;
pub const BUDGET_ROOT: usize = 2400;
pub const BUDGET_INNER: usize = 1600;
pub const BUDGET_LEAF: usize = 1200;
pub const CHAIN_BYTE_CAP: usize = 7200;
#[derive(Clone, Debug)]
pub struct MapPlan {
pub scope: String,
pub parent: Option<String>,
pub children: Vec<String>,
pub uses: Vec<String>,
pub api_hash: String,
pub kids_hash: Option<String>,
pub api: Vec<(String, Vec<Symbol>)>,
pub api_tail: usize,
pub api_tail_names: Vec<String>,
pub jumps: Vec<(String, Vec<String>, Vec<String>)>,
pub routes: Vec<(String, String)>,
compact_routes: bool,
route_tail: usize,
pub tests: Vec<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Emit {
Written,
Unchanged,
}
pub fn map_path(root: &Path, scope: &str) -> PathBuf {
if scope.is_empty() {
root.join("MAP.md")
} else {
root.join(scope).join("MAP.md")
}
}
pub fn approx_tokens(bytes: usize) -> usize {
(bytes * 3).div_ceil(10)
}
const USES_MAX: usize = 6;
const ROUTES_PREFIX_MIN_ROUTES: usize = 4;
const ROUTES_PREFIX_MIN_LEN: usize = 8;
fn common_route_prefix(routes: &[(String, String)]) -> String {
if std::env::var_os("RADAR_TEST_NO_ROUTE_PREFIX").is_some() {
return String::new();
}
if routes.len() < ROUTES_PREFIX_MIN_ROUTES {
return String::new();
}
let mut prefix = routes[0].0.clone();
for (scope, _) in &routes[1..] {
while !scope.starts_with(prefix.as_str()) {
prefix.pop();
}
if prefix.is_empty() {
return prefix;
}
}
match prefix.rfind('/') {
Some(i) => prefix.truncate(i + 1),
None => return String::new(),
}
if prefix.len() < ROUTES_PREFIX_MIN_LEN {
return String::new();
}
prefix
}
fn budget_for(scope: &str, has_children: bool) -> usize {
if scope.is_empty() {
BUDGET_ROOT
} else if has_children {
BUDGET_INNER
} else {
BUDGET_LEAF
}
}
fn rel_link(from_scope: &str, to_scope: &str) -> String {
let from: Vec<&str> = if from_scope.is_empty() {
vec![]
} else {
from_scope.split('/').collect()
};
let to: Vec<&str> = if to_scope.is_empty() {
vec![]
} else {
to_scope.split('/').collect()
};
let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let mut parts: Vec<String> = vec!["..".to_string(); from.len() - common];
parts.extend(to[common..].iter().map(|s| s.to_string()));
parts.push("MAP.md".to_string());
parts.join("/")
}
pub fn api_hash(symbols: impl Iterator<Item = Symbol>) -> String {
let mut lines: Vec<String> = symbols
.filter(|s| s.vis == Vis::Pub)
.map(|s| format!("{} {} {}", s.kind.name(), s.name, s.sig))
.collect();
lines.sort();
lines.dedup();
let digest = blake3::hash(lines.join("\n").as_bytes());
digest.to_hex()[..16].to_string()
}
pub fn scope_symbols<'c>(
cache: &'c ScanCache,
placement: &'c Placement,
scope: &'c str,
) -> impl Iterator<Item = (&'c String, &'c Symbol)> {
cache.files.iter().flat_map(move |(rel, entry)| {
let owned = placement.owner.get(rel).is_some_and(|s| s == scope);
let symbols = if owned
&& let Some(lang) = entry.lang
&& let Some(x) = cache.parses.get(&(lang, entry.hash))
{
x.defs.as_slice()
} else {
&[]
};
symbols.iter().map(move |s| (rel, s))
})
}
pub fn plan_maps(cache: &ScanCache, placement: &Placement, ranking: &Ranking) -> Vec<MapPlan> {
let mut hashes: BTreeMap<&str, String> = BTreeMap::new();
for scope in &placement.anchors {
let h = api_hash(
scope_symbols(cache, placement, scope)
.filter(|(_, s)| s.vis == Vis::Pub)
.map(|(_, s)| s.clone()),
);
hashes.insert(scope, h);
}
let mut top_symbol: BTreeMap<&str, (String, String)> = BTreeMap::new();
for scope in &placement.anchors {
let mut best: Option<(u64, &String, &Symbol)> = None;
let mut second_score = 0u64;
for (rel, sym) in scope_symbols(cache, placement, scope) {
if sym.vis != Vis::Pub {
continue;
}
let score = ranking.name_refs.get(&sym.name).copied().unwrap_or(0);
let better = match &best {
None => true,
Some((s, r, b)) => score > *s || (score == *s && (rel, sym.line) < (*r, b.line)),
};
if better {
if let Some((s, _, _)) = &best {
second_score = second_score.max(*s);
}
best = Some((score, rel, sym));
} else {
second_score = second_score.max(score);
}
}
if let Some((score, rel, sym)) = best
&& score > 0
&& score > second_score
{
top_symbol.insert(scope, (rel.clone(), sym.name.clone()));
}
}
let mut pub_owner: BTreeMap<&str, &str> = BTreeMap::new();
for (rel, entry) in &cache.files {
let (Some(lang), Some(owner)) = (entry.lang, placement.owner.get(rel)) else {
continue;
};
let Some(x) = cache.parses.get(&(lang, entry.hash)) else {
continue;
};
for d in &x.defs {
if d.vis == Vis::Pub {
pub_owner.entry(&d.name).or_insert(owner.as_str());
}
}
}
placement
.anchors
.iter()
.map(|scope| {
let children: Vec<String> = placement
.children_of(scope)
.iter()
.map(|s| s.to_string())
.collect();
let kids_hash = if children.is_empty() {
None
} else {
let mut ks: Vec<&str> = children
.iter()
.filter_map(|c| hashes.get(c.as_str()).map(|s| s.as_str()))
.collect();
ks.sort();
let digest = blake3::hash(ks.join("\n").as_bytes());
Some(digest.to_hex()[..16].to_string())
};
let mut use_counts: BTreeMap<&str, u64> = BTreeMap::new();
for (rel, entry) in &cache.files {
if placement.owner.get(rel).is_none_or(|s| s != scope) {
continue;
}
let Some(lang) = entry.lang else { continue };
let Some(x) = cache.parses.get(&(lang, entry.hash)) else {
continue;
};
for r in &x.refs {
if let Some(owner) = pub_owner.get(r.name.as_str())
&& !owner.is_empty()
&& *owner != scope.as_str()
{
*use_counts.entry(owner).or_insert(0) += 1;
}
}
}
let mut uses: Vec<(&str, u64)> = use_counts.into_iter().collect();
uses.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
let uses: Vec<String> = uses
.into_iter()
.take(USES_MAX)
.map(|(s, _)| s.to_string())
.collect();
let mut scored: Vec<(u64, String, Symbol)> = scope_symbols(cache, placement, scope)
.filter(|(_, s)| s.vis == Vis::Pub)
.map(|(rel, s)| {
let score = ranking.name_refs.get(&s.name).copied().unwrap_or(0);
(score, rel.clone(), s.clone())
})
.collect();
scored.sort_by(|a, b| {
b.0.cmp(&a.0)
.then_with(|| a.1.cmp(&b.1))
.then_with(|| a.2.line.cmp(&b.2.line))
});
let ranked: Vec<(String, Symbol)> =
scored.into_iter().map(|(_, rel, s)| (rel, s)).collect();
let mut jump_syms: Vec<(u64, String)> = ranked
.iter()
.map(|(_, s)| {
(
ranking.name_refs.get(&s.name).copied().unwrap_or(0),
s.name.clone(),
)
})
.filter(|(score, _)| *score > 0)
.collect();
jump_syms.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
jump_syms.dedup_by(|a, b| a.1 == b.1);
let jumps: Vec<(String, Vec<String>, Vec<String>)> = jump_syms
.into_iter()
.take(3)
.map(|(_, name)| {
let mut users: Vec<(u64, &String)> = ranking
.name_users
.get(&name)
.map(|files| {
files
.iter()
.filter(|(rel, _)| {
placement.owner.get(*rel).is_some_and(|s| s != scope)
})
.map(|(rel, n)| (*n, rel))
.collect()
})
.unwrap_or_default();
users.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(b.1)));
let users: Vec<String> = users
.into_iter()
.take(2)
.map(|(_, rel)| rel.clone())
.collect();
let calls: Vec<String> = ranked
.iter()
.find(|(_, s)| s.name == name)
.map(|(rel, s)| {
let mut out: Vec<String> = Vec::new();
if let Some(entry) = cache.files.get(rel)
&& let Some(lang) = entry.lang
&& let Some(x) = cache.parses.get(&(lang, entry.hash))
{
for r in &x.refs {
if r.kind == crate::extract::RefKind::Call
&& r.line >= s.line
&& r.line <= s.end_line
&& r.name != name
&& pub_owner
.get(r.name.as_str())
.is_some_and(|o| *o != scope.as_str())
&& !out.contains(&r.name)
{
out.push(r.name.clone());
}
if out.len() >= 3 {
break;
}
}
}
out
})
.unwrap_or_default();
(name, users, calls)
})
.filter(|(_, users, calls)| !users.is_empty() || !calls.is_empty())
.collect();
let tests: Vec<String> = cache
.files
.keys()
.filter(|rel| placement.owner.get(*rel).is_some_and(|s| s == scope))
.filter(|rel| {
let name = rel.rsplit('/').next().unwrap_or(rel);
name.contains("test") || name.contains("spec")
})
.cloned()
.collect();
let routes: Vec<(String, String)> = if scope.is_empty() {
placement
.anchors
.iter()
.filter(|a| !a.is_empty())
.map(|a| {
let glue = top_symbol
.get(a.as_str())
.map(|(rel, name)| format!("{rel}#{name}"))
.unwrap_or_default();
(a.clone(), glue)
})
.collect()
} else {
Vec::new()
};
let scope_api_hash = hashes.get(scope.as_str()).cloned().unwrap_or_else(|| {
api_hash(
scope_symbols(cache, placement, scope)
.filter(|(_, symbol)| symbol.vis == Vis::Pub)
.map(|(_, symbol)| symbol.clone()),
)
});
MapPlan {
scope: scope.clone(),
parent: placement.parent_scope(scope).map(|s| s.to_string()),
children,
uses,
api_hash: scope_api_hash,
kids_hash,
api: Vec::new(),
api_tail: 0,
api_tail_names: Vec::new(),
tests,
jumps,
routes,
compact_routes: false,
route_tail: 0,
}
.packed(&ranked)
})
.collect()
}
impl MapPlan {
fn packed(mut self, ranked: &[(String, Symbol)]) -> MapPlan {
let budget = budget_for(&self.scope, !self.children.is_empty());
let total = ranked.len();
let reserve = "x".repeat(crate::slots::PURPOSE_MAX_CHARS);
if !self.routes.is_empty() && self.render_body(&reserve).len() > budget {
self.compact_routes = true;
if self.render_body(&reserve).len() > budget {
let all_routes = self.routes.clone();
let mut lo = 0usize;
let mut hi = all_routes.len();
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
let mut probe = self.clone();
probe.routes = all_routes[..mid].to_vec();
probe.route_tail = all_routes.len() - mid;
if probe.render_body(&reserve).len() <= budget {
lo = mid;
} else {
hi = mid - 1;
}
}
self.routes = all_routes[..lo].to_vec();
self.route_tail = all_routes.len() - lo;
}
}
let fits = |k: usize| -> bool {
self.trimmed(ranked, k, total).render_body(&reserve).len() <= budget
};
if fits(total) {
return self.trimmed(ranked, total, total);
}
let (mut lo, mut hi) = (0usize, total);
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if fits(mid) {
lo = mid;
} else {
hi = mid - 1;
}
}
self.trimmed(ranked, lo, total)
}
fn trimmed(&self, ranked: &[(String, Symbol)], keep: usize, total: usize) -> MapPlan {
let mut api: BTreeMap<String, Vec<Symbol>> = BTreeMap::new();
for (f, s) in ranked.iter().take(keep) {
api.entry(f.clone()).or_default().push(s.clone());
}
let mut tail_names: Vec<String> = ranked
.iter()
.skip(keep)
.map(|(_, s)| s.name.clone())
.collect();
tail_names.sort();
tail_names.dedup();
MapPlan {
scope: self.scope.clone(),
parent: self.parent.clone(),
children: self.children.clone(),
uses: self.uses.clone(),
api_hash: self.api_hash.clone(),
kids_hash: self.kids_hash.clone(),
api: api.into_iter().collect(),
api_tail: total - keep,
api_tail_names: tail_names,
jumps: self.jumps.clone(),
tests: self.tests.clone(),
routes: self.routes.clone(),
compact_routes: self.compact_routes,
route_tail: self.route_tail,
}
}
fn render_body(&self, purpose: &str) -> String {
let title = if self.scope.is_empty() {
"."
} else {
&self.scope
};
let mut b = format!("# {title}\n\n");
b.push_str("<!-- radar:slot purpose max=160 -->\n");
let fallback;
let text = if purpose.trim().is_empty() {
fallback = self.fallback_purpose();
&fallback
} else {
purpose
};
b.push_str(text.trim_end());
b.push_str("\n<!-- /radar:slot -->\n");
if !self.routes.is_empty() || self.route_tail > 0 {
let prefix = if self.routes.is_empty() {
String::new()
} else {
common_route_prefix(&self.routes)
};
if self.compact_routes {
if prefix.is_empty() {
b.push_str("\n## Routes (append /MAP.md)\n");
} else {
b.push_str(&format!(
"\n## Routes (all under {prefix}; append /MAP.md)\n"
));
}
let mut column = 0usize;
for (scope, _) in &self.routes {
let scope = scope.strip_prefix(&prefix).unwrap_or(scope);
let separator = usize::from(column > 0) * 2;
if column > 0 && column + separator + scope.len() > 100 {
b.push('\n');
column = 0;
}
if column > 0 {
b.push_str(", ");
column += 2;
}
b.push_str(scope);
column += scope.len();
}
b.push('\n');
if self.route_tail > 0 {
b.push_str(&format!(
"+{} more scopes: use `radar tree` or the children frontmatter.\n",
self.route_tail
));
}
} else if prefix.is_empty() {
b.push_str("\n## Routes\n");
} else {
b.push_str(&format!("\n## Routes (all under {prefix})\n"));
}
if !self.compact_routes {
for (scope, glue) in &self.routes {
let scope = scope.strip_prefix(&prefix).unwrap_or(scope);
let glue = glue.strip_prefix(&prefix).unwrap_or(glue);
if glue.is_empty() {
b.push_str(&format!("- {scope}/MAP.md\n"));
} else {
b.push_str(&format!("- {scope}/MAP.md · {glue}\n"));
}
}
}
}
if !self.api.is_empty() {
b.push_str("\n## API\n");
for (file, syms) in &self.api {
let short = file
.strip_prefix(&format!("{}/", self.scope))
.unwrap_or(file);
b.push_str(&format!("{short}\n"));
for s in syms {
b.push_str(&format!("- {}\n", s.sig));
}
}
if self.api_tail > 0 {
const TAIL_RESERVE: usize = 400;
let mut listed = Vec::new();
let mut used = 0usize;
for name in &self.api_tail_names {
if used + name.len() + 2 > TAIL_RESERVE {
break;
}
used += name.len() + 2;
listed.push(name.as_str());
}
let unlisted = self.api_tail - listed.len().min(self.api_tail);
if !listed.is_empty() {
b.push_str(&format!("- also: {}\n", listed.join(", ")));
}
if unlisted > 0 {
b.push_str(&format!(
"- +{unlisted} more public symbols omitted by the map budget\n"
));
}
}
}
if !self.jumps.is_empty() {
b.push_str("\n## Jump\n");
for (name, users, calls) in &self.jumps {
let mut row = format!("- {name}");
if !users.is_empty() {
row.push_str(&format!(" ← used by {}", users.join(", ")));
}
if !calls.is_empty() {
row.push_str(&format!(" · calls {}", calls.join(", ")));
}
b.push_str(&row);
b.push('\n');
}
}
if !self.children.is_empty() && self.routes.is_empty() {
b.push_str("\n## Children\n");
for child in &self.children {
let link = rel_link(&self.scope, child);
let label = child
.strip_prefix(&format!("{}/", self.scope))
.unwrap_or(child);
b.push_str(&format!("- [{label}/]({link})\n"));
}
}
if !self.tests.is_empty() {
b.push_str("\n## Tests\n");
for t in &self.tests {
let short = t.strip_prefix(&format!("{}/", self.scope)).unwrap_or(t);
b.push_str(&format!("- {short}\n"));
}
}
b
}
fn fallback_purpose(&self) -> String {
if !self.children.is_empty() {
let kids: Vec<&str> = self
.children
.iter()
.map(|c| c.rsplit('/').next().unwrap_or(c))
.take(6)
.collect();
return format!(
"Routes {} child unit(s): {}. (radar: fill this slot)",
self.children.len(),
kids.join(", ")
);
}
let files = self.api.len();
let names: Vec<&str> = self
.api
.iter()
.flat_map(|(_, syms)| syms.iter().take(1))
.map(|s| s.name.as_str())
.take(4)
.collect();
if names.is_empty() {
format!("Contains {files} source files. (radar: fill this slot)")
} else {
format!(
"Contains {files} files around {}. (radar: fill this slot)",
names.join(", ")
)
}
}
pub fn emit(&self, root: &Path, now_iso: &str) -> io::Result<Emit> {
let path = map_path(root, &self.scope);
let existing = fs::read_to_string(&path).ok();
let (old_fm, old_slot) = match existing.as_deref().and_then(frontmatter::parse) {
Some((fm, body)) => (Some(fm), slot_text(body, "purpose")),
None => (None, None),
};
let body = self.render_body(old_slot.as_deref().unwrap_or(""));
let mut fm = Frontmatter::new();
fm.set("type", "Code Repository Map");
fm.set(
"title",
if self.scope.is_empty() {
"."
} else {
&self.scope
},
);
fm.set("description", "Radar source-navigation map.");
fm.set("map", "1");
fm.set(
"scope",
if self.scope.is_empty() {
"."
} else {
&self.scope
},
);
if let Some(parent) = &self.parent {
fm.set("parent", rel_link(&self.scope, parent));
}
if !self.children.is_empty() {
let links: Vec<String> = self
.children
.iter()
.map(|c| rel_link(&self.scope, c))
.collect();
fm.set_list("children", &links);
}
if !self.uses.is_empty() {
fm.set_list("uses", &self.uses);
}
fm.set("fidelity", "syntax");
fm.set("api_hash", &self.api_hash);
if let Some(kh) = &self.kids_hash {
fm.set("kids_hash", kh);
}
fm.set("tokens", format!("~{}", approx_tokens(body.len())));
const KNOWN: [&str; 16] = [
"type",
"title",
"description",
"map",
"scope",
"parent",
"children",
"uses",
"peers",
"fidelity",
"lang",
"api_hash",
"kids_hash",
"stamped",
"bytes",
"tokens",
];
if let Some(old) = &old_fm {
let extras: Vec<(String, String)> = old
.keys()
.filter(|k| !KNOWN.contains(k))
.filter_map(|k| old.get(k).map(|v| (k.to_string(), v.to_string())))
.collect();
for (k, v) in extras {
fm.set(&k, v);
}
}
if let (Some(old), Some(old_doc)) = (&old_fm, &existing) {
let old_body = frontmatter::parse(old_doc).map(|(_, b)| b).unwrap_or("");
let stamp = old.get("stamped").unwrap_or_default().to_string();
let mut probe = fm.clone();
probe.set("stamped", stamp);
if probe_equal(&probe, old) && old_body == body {
return Ok(Emit::Unchanged);
}
}
fm.set("stamped", now_iso);
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
fs::write(&path, fm.render() + &body)?;
Ok(Emit::Written)
}
}
fn probe_equal(a: &Frontmatter, b: &Frontmatter) -> bool {
let keys: Vec<&str> = a.keys().chain(b.keys()).collect();
keys.iter().all(|k| a.get(k) == b.get(k))
}
pub fn slot_text(body: &str, slot: &str) -> Option<String> {
let open = format!("<!-- radar:slot {slot}");
let start = body.find(&open)?;
let after_open = body[start..].find("-->")? + start + 3;
let close = body[after_open..].find("<!-- /radar:slot -->")? + after_open;
let text = body[after_open..close].trim();
if text.ends_with("(radar: fill this slot)") {
None } else {
Some(text.to_string())
}
}
pub fn now_iso() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let days = secs / 86_400;
let (h, m, s) = ((secs % 86_400) / 3600, (secs % 3600) / 60, secs % 60);
let z = days as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mo <= 2 { y + 1 } else { y };
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::SymKind;
fn sym(name: &str, vis: Vis, line: u32, sig: &str) -> Symbol {
Symbol {
line,
end_line: line + 1,
name: name.into(),
kind: SymKind::Fn,
vis,
sig: sig.into(),
terms: Vec::new(),
}
}
#[test]
fn api_hash_is_order_path_and_line_invariant() {
let a = vec![
sym("alpha", Vis::Pub, 10, "def alpha()"),
sym("beta", Vis::Pub, 20, "def beta()"),
];
let mut b = a.clone();
b.reverse();
b[0].line = 99; assert_eq!(api_hash(a.into_iter()), api_hash(b.into_iter()));
}
#[test]
fn api_hash_ignores_private_and_detects_pub_changes() {
let base = vec![sym("alpha", Vis::Pub, 1, "def alpha()")];
let with_priv = vec![
sym("alpha", Vis::Pub, 1, "def alpha()"),
sym("_hidden", Vis::Priv, 2, "def _hidden()"),
];
assert_eq!(
api_hash(base.clone().into_iter()),
api_hash(with_priv.into_iter()),
"private symbols never affect the hash"
);
let changed = vec![sym("alpha", Vis::Pub, 1, "def alpha(x)")];
assert_ne!(
api_hash(base.into_iter()),
api_hash(changed.into_iter()),
"signature change moves the hash"
);
}
#[test]
fn rel_links_resolve_up_and_down() {
assert_eq!(rel_link("", "auth"), "auth/MAP.md");
assert_eq!(rel_link("auth", ""), "../MAP.md");
assert_eq!(rel_link("auth/jwt", "auth"), "../MAP.md");
assert_eq!(rel_link("auth", "auth/jwt"), "jwt/MAP.md");
assert_eq!(rel_link("a/b", "a/c"), "../c/MAP.md");
assert_eq!(
rel_link("src/main/java/com/acme", ""),
"../../../../../MAP.md"
);
}
#[test]
fn slot_text_extraction_and_fallback_detection() {
let body =
"# t\n\n<!-- radar:slot purpose max=160 -->\nReal user text.\n<!-- /radar:slot -->\n";
assert_eq!(slot_text(body, "purpose"), Some("Real user text.".into()));
let fallback = "# t\n\n<!-- radar:slot purpose max=160 -->\nContains 3 files. (radar: fill this slot)\n<!-- /radar:slot -->\n";
assert_eq!(slot_text(fallback, "purpose"), None);
assert_eq!(slot_text("no slot here", "purpose"), None);
}
#[test]
fn route_prefix_factoring_gates() {
let deep: Vec<(String, String)> = ["auth", "billing", "orders", "payments"]
.iter()
.map(|m| {
(
format!("services/api/internal/{m}"),
format!("services/api/internal/{m}/mod.py#{m}_entry"),
)
})
.collect();
assert_eq!(common_route_prefix(&deep), "services/api/internal/");
assert_eq!(common_route_prefix(&deep[..3]), "");
let flat: Vec<(String, String)> = ["auth", "billing", "orders", "payments"]
.iter()
.map(|m| (m.to_string(), String::new()))
.collect();
assert_eq!(common_route_prefix(&flat), "");
let short: Vec<(String, String)> = ["src/a", "src/b", "src/c", "src/d"]
.iter()
.map(|m| (m.to_string(), String::new()))
.collect();
assert_eq!(common_route_prefix(&short), "");
}
fn root_with_routes(count: usize) -> MapPlan {
MapPlan {
scope: String::new(),
parent: None,
children: (0..count).map(|index| format!("p{index:03}")).collect(),
uses: Vec::new(),
api_hash: "hash".to_string(),
kids_hash: None,
api: Vec::new(),
api_tail: 0,
api_tail_names: Vec::new(),
jumps: Vec::new(),
routes: (0..count)
.map(|index| (format!("p{index:03}"), String::new()))
.collect(),
compact_routes: false,
route_tail: 0,
tests: Vec::new(),
}
.packed(&[])
}
#[test]
fn oversized_root_routes_compact_within_budget() {
let plan = root_with_routes(200);
let body = plan.render_body("");
assert!(plan.compact_routes, "dense form selected");
assert_eq!(plan.route_tail, 0, "all 200 scopes still visible");
assert!(body.contains("append /MAP.md"), "{body}");
assert!(body.len() <= BUDGET_ROOT, "{} > {BUDGET_ROOT}", body.len());
}
#[test]
fn enormous_root_routes_emit_an_explicit_tail() {
let plan = root_with_routes(1000);
let body = plan.render_body("");
assert!(plan.route_tail > 0, "overflow is counted");
assert!(body.contains("more scopes"), "{body}");
assert!(body.len() <= BUDGET_ROOT, "{} > {BUDGET_ROOT}", body.len());
}
#[test]
fn now_iso_shape() {
let s = now_iso();
assert_eq!(s.len(), 20, "{s}");
assert!(s.ends_with('Z'));
assert_eq!(&s[4..5], "-");
assert!(s.starts_with("20"), "sane century: {s}");
}
}