use std::fmt::Write as _;
use rto_graph::Explanation;
pub const HOME_NOTE: &str = "_Home.md";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VaultNote {
pub filename: String,
pub content: String,
}
#[must_use]
pub fn note_name(key: &str) -> String {
const MAX: usize = 200;
let mut out = String::with_capacity(key.len());
let mut prev_dash = false;
for c in key.chars() {
if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
out.push(c);
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
let out = out.trim_matches('-');
if out.len() <= MAX {
out.to_owned()
} else {
format!("{}-{:016x}", &out[..MAX - 17], fnv1a64(key.as_bytes()))
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
fn yaml_double_quoted(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'\\' => out.push_str(r"\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
'\t' => out.push_str(r"\t"),
'\u{0}' => out.push_str(r"\0"),
'\u{7}' => out.push_str(r"\a"),
'\u{8}' => out.push_str(r"\b"),
'\u{b}' => out.push_str(r"\v"),
'\u{c}' => out.push_str(r"\f"),
'\u{1b}' => out.push_str(r"\e"),
c if (c < ' ')
|| c == '\u{7f}'
|| ('\u{80}'..='\u{9f}').contains(&c)
|| matches!(c, '\u{2028}' | '\u{2029}' | '\u{feff}') =>
{
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn yaml_scalar(value: &str) -> String {
if is_plain_safe(value) {
value.to_owned()
} else {
yaml_double_quoted(value)
}
}
fn is_plain_safe(value: &str) -> bool {
const NOT_STRINGS: [&str; 11] = [
"true", "false", "yes", "no", "on", "off", "null", "nil", "none", "y", "n",
];
!value.is_empty()
&& value.starts_with(|c: char| c.is_ascii_alphabetic())
&& value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/'))
&& !NOT_STRINGS.contains(&value.to_ascii_lowercase().as_str())
}
#[derive(Debug, Clone, Copy)]
pub struct VaultScope<'a> {
pub project: Option<&'a str>,
pub members: &'a std::collections::BTreeSet<String>,
}
static NO_MEMBERS: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
impl VaultScope<'_> {
pub const PROJECT: Self = Self {
project: None,
members: &NO_MEMBERS,
};
}
impl Default for VaultScope<'_> {
fn default() -> Self {
Self::PROJECT
}
}
impl VaultScope<'_> {
#[must_use]
pub fn redirects_external_ref(&self, key: &str) -> bool {
key.strip_prefix("extref:")
.and_then(rto_graph::parse_qualified)
.is_some_and(|(project, _)| self.members.contains(project))
}
}
#[must_use]
pub fn scoped_note_name(scope: &VaultScope<'_>, key: &str) -> String {
match scope.project {
None => note_name(key),
Some(project) => note_name(&format!("{project}::{key}")),
}
}
fn link_target(scope: &VaultScope<'_>, key: &str) -> String {
if scope.redirects_external_ref(key) {
return note_name(key.strip_prefix("extref:").unwrap_or(key));
}
scoped_note_name(scope, key)
}
#[must_use]
pub fn render_note(ex: &Explanation, source_base: Option<&str>, body: Option<&str>) -> VaultNote {
render_note_scoped(ex, source_base, body, &VaultScope::PROJECT)
}
#[must_use]
pub fn render_note_scoped(
ex: &Explanation,
source_base: Option<&str>,
body: Option<&str>,
scope: &VaultScope<'_>,
) -> VaultNote {
let meta = &ex.meta;
let status = meta.get("status").and_then(|v| v.as_str());
let content = note_body(meta.get("content").and_then(|v| v.as_str()), body);
let mut c = String::new();
c.push_str("---\n");
let _ = writeln!(c, "key: {}", yaml_double_quoted(&ex.node.key));
let _ = writeln!(c, "kind: {}", yaml_scalar(ex.node.kind.as_str()));
if let Some(project) = scope.project {
let _ = writeln!(c, "project: {}", yaml_double_quoted(project));
}
if let Some(path) = &ex.node.path {
let _ = writeln!(c, "path: {}", yaml_double_quoted(path));
}
if let Some(lang) = &ex.node.lang {
let _ = writeln!(c, "lang: {}", yaml_scalar(lang));
}
if let Some(status) = status {
let _ = writeln!(c, "status: {}", yaml_scalar(status));
}
c.push_str("tags:\n");
let _ = writeln!(c, " - roteiro/kind/{}", tag_slug(&ex.node.kind));
if let Some(project) = scope.project {
let _ = writeln!(c, " - roteiro/project/{}", tag_slug(project));
}
if let Some(lang) = &ex.node.lang {
let _ = writeln!(c, " - roteiro/lang/{}", tag_slug(lang));
}
if let Some(status) = status {
let _ = writeln!(c, " - roteiro/status/{}", tag_slug(status));
}
c.push_str("---\n\n");
let _ = writeln!(c, "# {}", ex.node.name);
if let Some(status) = status {
let _ = writeln!(c, "\n> **Status:** {status}");
}
if let (Some(base), Some(path)) = (source_base, ex.node.path.as_deref()) {
let _ = writeln!(
c,
"\n**Source:** [`{path}`]({}/{path})",
base.trim_end_matches('/')
);
}
if let Some(content) = content.map(str::trim).filter(|s| !s.is_empty()) {
c.push_str("\n## Content\n\n");
c.push_str(content);
c.push('\n');
}
if !ex.outgoing.is_empty() {
c.push_str("\n## Outgoing\n\n");
for e in &ex.outgoing {
let _ = writeln!(
c,
"- {} ({}){} → [[{}]]",
e.kind,
e.provenance,
confidence(e.confidence),
link_target(scope, &e.node)
);
}
}
if !ex.incoming.is_empty() {
c.push_str("\n## Incoming\n\n");
for e in &ex.incoming {
let _ = writeln!(
c,
"- [[{}]] {} ({}){} →",
link_target(scope, &e.node),
e.kind,
e.provenance,
confidence(e.confidence)
);
}
}
VaultNote {
filename: format!("{}.md", scoped_note_name(scope, &ex.node.key)),
content: c,
}
}
fn note_body<'a>(content: Option<&'a str>, body: Option<&'a str>) -> Option<&'a str> {
body.or(content)
}
fn confidence(c: Option<f64>) -> String {
c.map_or_else(String::new, |c| format!(" ({c:.2})"))
}
fn tag_slug(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_dash = false;
for ch in s.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash {
out.push('-');
prev_dash = true;
}
}
out.trim_matches('-').to_owned()
}
#[derive(Debug, Clone)]
pub struct AdrEntry {
pub key: String,
pub name: String,
pub status: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSecretSummary {
pub secret_named: usize,
pub redacted: usize,
pub declared: usize,
pub unredacted: usize,
pub files: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct DensityEntry {
pub path: String,
pub markers: u32,
pub lines: u32,
pub per_kloc: f64,
}
#[derive(Debug, Clone)]
pub struct CouplingEntry {
pub key: String,
pub name: String,
pub fan_in: u32,
pub fan_out: u32,
}
#[derive(Debug, Clone, Default)]
pub struct VaultSummary {
pub project: String,
pub total_nodes: usize,
pub total_edges: usize,
pub node_counts: Vec<(String, usize)>,
pub edge_provenance: Vec<(String, usize)>,
pub adrs: Vec<AdrEntry>,
pub debt: Vec<(String, usize)>,
pub densest_files: Vec<DensityEntry>,
pub config_secrets: Option<ConfigSecretSummary>,
pub most_called: Vec<CouplingEntry>,
pub repo_url: Option<String>,
pub commit: Option<String>,
}
#[must_use]
pub fn render_home(s: &VaultSummary) -> VaultNote {
let mut c = String::new();
c.push_str("---\ntags:\n - roteiro/home\n---\n\n");
let _ = writeln!(c, "# {} — knowledge graph", s.project);
c.push_str(
"\n*A browsable snapshot of this codebase as one **knowledge graph**, \
generated by [Roteiro](https://roteiro.dev). Every symbol, document and \
decision is a note, linked to the things it relates to.*\n",
);
c.push_str(HOW_TO_READ);
let _ = writeln!(
c,
"\n**{} nodes**, **{} edges** across the project.",
s.total_nodes, s.total_edges
);
write_repo_line(&mut c, s);
write_summary_sections(&mut c, s, &VaultScope::PROJECT, 2);
c.push_str(NAVIGATING);
VaultNote {
filename: HOME_NOTE.to_owned(),
content: c,
}
}
const HOW_TO_READ: &str = "\n**How to read it.** Open any note to see what a thing is, the intent or \
docs behind it (its **Content**), where it lives (its **Source** link), \
and how it connects (**Outgoing**/**Incoming** links). Each link is \
labelled with how the fact was established — `derived` (extracted from \
code), `authored` (human intent: ADRs, blueprints, annotations), or \
`inferred` (a scored suggestion). Open Obsidian's **graph view** to see \
the whole thing at once.\n";
const NAVIGATING: &str = "\n## Navigating this vault\n\n\
- Open the **graph view** to see the whole codebase; notes are coloured/\
filterable by their `roteiro/kind/*`, `roteiro/lang/*` and \
`roteiro/status/*` tags.\n\
- Each note carries its captured **content** (doc comments, prose, PDF/\
image text) and its provenance-labelled incoming/outgoing links.\n\
- Start from an ADR above, or search the tag pane for a kind.\n";
fn write_repo_line(c: &mut String, s: &VaultSummary) {
if let Some(repo) = &s.repo_url {
let _ = write!(c, "\n**Repository:** [{repo}]({repo})");
if let Some(commit) = &s.commit {
let short = &commit[..commit.len().min(12)];
let _ = write!(c, " · rendered at commit `{short}`");
}
c.push('\n');
}
}
fn write_summary_sections(c: &mut String, s: &VaultSummary, scope: &VaultScope<'_>, level: usize) {
let hd = &"#".repeat(level);
let sub = &"#".repeat(level + 1);
write_structure(c, s, hd);
write_decisions(c, s, scope, hd);
write_debt(c, s, scope, hd, sub);
write_config_secrets(c, s, scope, hd);
write_coupling(c, s, scope, hd);
}
fn write_structure(c: &mut String, s: &VaultSummary, hd: &str) {
let _ = write!(c, "\n{hd} Structure\n\n| Kind | Count |\n| --- | --- |\n");
for (kind, n) in &s.node_counts {
let _ = writeln!(c, "| {kind} | {n} |");
}
if !s.edge_provenance.is_empty() {
let _ = write!(
c,
"\n{hd} Provenance\n\n| Provenance | Edges |\n| --- | --- |\n"
);
for (prov, n) in &s.edge_provenance {
let _ = writeln!(c, "| {prov} | {n} |");
}
}
}
fn write_decisions(c: &mut String, s: &VaultSummary, scope: &VaultScope<'_>, hd: &str) {
let _ = write!(c, "\n{hd} Decisions (ADRs)\n\n");
if s.adrs.is_empty() {
c.push_str("*No ADRs found.*\n");
} else {
for adr in &s.adrs {
let status = adr.status.as_deref().unwrap_or("—");
let _ = writeln!(
c,
"- **{status}** — [[{}|{}]]",
scoped_note_name(scope, &adr.key),
adr.name
);
}
}
}
fn write_debt(c: &mut String, s: &VaultSummary, scope: &VaultScope<'_>, hd: &str, sub: &str) {
let _ = write!(c, "\n{hd} Intent debt\n\n");
if s.debt.is_empty() {
c.push_str("*None recorded.*\n");
} else {
c.push_str("| Category | Count |\n| --- | --- |\n");
for (cat, n) in &s.debt {
let _ = writeln!(c, "| {cat} | {n} |");
}
}
if !s.densest_files.is_empty() {
let _ = write!(
c,
"\n{sub} Densest files (markers per 1,000 lines)\n\n\
*Where the debt above is concentrated, rather than where there is \
most of it — a raw count ranks the biggest file first by \
construction. The denominator is file length: every line, blanks and \
comments included, not source lines of code. Prose matches (`for \
now`, `tbd`) count too, so a design document can rank high.*\n\n"
);
c.push_str("| File | Markers | Lines | Per 1k |\n| --- | --- | --- | --- |\n");
for e in &s.densest_files {
let _ = writeln!(
c,
"| [[{}\\|{}]] | {} | {} | {:.2} |",
scoped_note_name(scope, &format!("file:{}", e.path)),
e.path,
e.markers,
e.lines,
e.per_kloc
);
}
}
}
fn write_config_secrets(c: &mut String, s: &VaultSummary, scope: &VaultScope<'_>, hd: &str) {
if let Some(cs) = &s.config_secrets {
let _ = write!(c, "\n{hd} Config keys named like secrets\n\n");
let _ = writeln!(
c,
"**{}** secret-named config key(s): {} redacted before storage, {} \
declared in code without a value, {} unredacted.",
cs.secret_named, cs.redacted, cs.declared, cs.unredacted
);
if cs.unredacted > 0 {
let _ = writeln!(
c,
"\n> [!warning] {} key(s) carry an **unredacted** value. Extraction \
always redacts, so these came from an import layer — inspect the \
importing tool, not this repository.",
cs.unredacted
);
}
if !cs.files.is_empty() {
c.push_str("\nIn:\n");
for path in &cs.files {
let _ = writeln!(
c,
"- [[{}\\|{path}]]",
scoped_note_name(scope, &format!("file:{path}"))
);
}
}
c.push_str(
"\n*An inventory of config keys whose **names** look secret, not a secret \
scan. Values are redacted before they are stored, so this reports that \
such keys exist and were redacted — never a value. It cannot see a \
hardcoded credential in source code, cannot judge whether a value is \
valid, and cannot tell a real secret from a placeholder. A credential \
under an innocuous key name (`dsn`, `endpoint`) does not appear here at \
all, so this section being small says nothing about whether this \
repository leaks secrets.*\n",
);
}
}
fn write_coupling(c: &mut String, s: &VaultSummary, scope: &VaultScope<'_>, hd: &str) {
if !s.most_called.is_empty() {
let _ = write!(
c,
"\n{hd} Most depended-on (call fan-in)\n\n\
*Distinct callers and callees over `calls` edges — direction kept, so \
\"everything calls this\" and \"this calls everything\" are not the same \
row. Call targets are resolved by simple name, so a short, generically-\
named function can absorb every call to that name: read a large fan-in on \
one as a question, not a finding.*\n\n"
);
c.push_str("| Symbol | Called by | Calls |\n| --- | --- | --- |\n");
for e in &s.most_called {
let _ = writeln!(
c,
"| [[{}\\|{}]] | {} | {} |",
scoped_note_name(scope, &e.key),
e.name,
e.fan_in,
e.fan_out
);
}
}
}
#[derive(Debug, Clone)]
pub struct CrossLink {
pub from_project: String,
pub from_key: String,
pub from_name: String,
pub kind: String,
pub confidence: Option<f64>,
pub to_qualified: String,
pub resolves: bool,
}
#[derive(Debug, Clone, Default)]
pub struct WorkspaceSummary {
pub name: String,
pub members: Vec<VaultSummary>,
pub cross_links: Vec<CrossLink>,
pub cross_links_total: usize,
}
#[must_use]
pub fn render_workspace_home(ws: &WorkspaceSummary) -> VaultNote {
let members: std::collections::BTreeSet<String> =
ws.members.iter().map(|m| m.project.clone()).collect();
let mut c = String::new();
c.push_str("---\ntags:\n - roteiro/home\n - roteiro/workspace\n---\n\n");
let _ = writeln!(c, "# {} — workspace knowledge graph", ws.name);
c.push_str(
"\n*A browsable snapshot of a whole **workspace** as one **knowledge \
graph**, generated by [Roteiro](https://roteiro.dev). Every symbol, \
document and decision in every member repository is a note, linked to the \
things it relates to — including across repositories.*\n",
);
c.push_str(HOW_TO_READ);
c.push_str(
"\n**Notes are named `<project>-<key>`**, because a node key is \
repository-relative: every member has a `README.md`, and without the \
project each would overwrite the last. Filter the graph view by a \
member's `roteiro/project/*` tag to see one repository at a time.\n",
);
let total_nodes: usize = ws.members.iter().map(|m| m.total_nodes).sum();
let total_edges: usize = ws.members.iter().map(|m| m.total_edges).sum();
let _ = writeln!(
c,
"\n**{total_nodes} nodes**, **{total_edges} edges** across **{}** member \
repositor{}.",
ws.members.len(),
if ws.members.len() == 1 { "y" } else { "ies" }
);
c.push_str("\n## Members\n\n| Project | Nodes | Edges | Repository | Commit |\n| --- | --- | --- | --- | --- |\n");
for m in &ws.members {
let repo = m
.repo_url
.as_ref()
.map_or_else(|| "—".to_owned(), |u| format!("[{u}]({u})"));
let commit = m.commit.as_ref().map_or_else(
|| "—".to_owned(),
|c| format!("`{}`", &c[..c.len().min(12)]),
);
let _ = writeln!(
c,
"| [[#{}\\|{}]] | {} | {} | {repo} | {commit} |",
m.project, m.project, m.total_nodes, m.total_edges
);
}
c.push_str(
"\n*The `Repository` and `Commit` columns say where each member came from \
and what was read. They are **not** a replication manifest — reconstructing \
a workspace from a vault is issue #442 part 2, and nothing here is designed \
to be handed to someone else.*\n",
);
write_cross_links(&mut c, ws);
for m in &ws.members {
let _ = writeln!(c, "\n## {}", m.project);
let _ = writeln!(
c,
"\n**{} nodes**, **{} edges** in this member.",
m.total_nodes, m.total_edges
);
write_repo_line(&mut c, m);
let scope = VaultScope {
project: Some(&m.project),
members: &members,
};
write_summary_sections(&mut c, m, &scope, 3);
}
c.push_str(NAVIGATING);
VaultNote {
filename: HOME_NOTE.to_owned(),
content: c,
}
}
fn write_cross_links(c: &mut String, ws: &WorkspaceSummary) {
c.push_str("\n## Cross-repo links\n\n");
if ws.cross_links.is_empty() {
c.push_str(
"*None. These are the `inferred` cross-repo links `roteiro links \
--infer --write` persists (ADR-0009); a workspace whose members have \
never been inferred over has none recorded yet.*\n",
);
return;
}
c.push_str(
"*A spoke's config key and the hub key it corresponds to, across \
repositories — the one thing a per-project vault structurally cannot show. \
These are `inferred` matches persisted by `roteiro links --infer --write` \
(ADR-0009), not authored facts: read a row as a candidate correspondence.*\n\n",
);
c.push_str("| From | | To | Kind |\n| --- | --- | --- | --- |\n");
for l in &ws.cross_links {
let from_scope = VaultScope {
project: Some(&l.from_project),
members: &NO_MEMBERS,
};
let to = if l.resolves {
format!("[[{}\\|{}]]", note_name(&l.to_qualified), l.to_qualified)
} else {
format!("`{}` *(outside this workspace)*", l.to_qualified)
};
let _ = writeln!(
c,
"| [[{}\\|{}]] | {} | {to} | {}{} |",
scoped_note_name(&from_scope, &l.from_key),
l.from_name,
l.from_project,
l.kind,
confidence(l.confidence)
);
}
if ws.cross_links_total > ws.cross_links.len() {
let _ = writeln!(
c,
"\n*Showing {} of {} — the full report is `roteiro links --matrix`.*",
ws.cross_links.len(),
ws.cross_links_total
);
}
c.push_str(
"\n*Shown in one direction only. The edge lives in the spoke's store, \
pointing at a local placeholder for the hub's node, so the hub's own note \
carries no matching **Incoming** entry — Obsidian's **Backlinks** pane \
still shows it, because the link is in the vault.*\n",
);
}
#[cfg(test)]
mod tests {
use super::{
AdrEntry, ConfigSecretSummary, CouplingEntry, CrossLink, DensityEntry, HOME_NOTE,
VaultScope, VaultSummary, WorkspaceSummary, note_name, render_home, render_note,
render_note_scoped, render_workspace_home, scoped_note_name,
};
use rto_graph::{EdgeRef, Explanation, NodeSummary};
#[test]
fn note_name_is_safe_and_stable() {
assert_eq!(
note_name("sym:rust:src/a.rs#Store"),
"sym-rust-src-a.rs-Store"
);
assert_eq!(note_name("adr:0001"), "adr-0001");
assert_eq!(note_name("file:src/main.rs"), "file-src-main.rs");
}
#[test]
fn render_note_emits_frontmatter_and_wikilinks() {
let ex = Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: "sym:rust:a.rs#main".into(),
kind: "fn".into(),
name: "main".into(),
path: Some("a.rs".into()),
lang: Some("rust".into()),
},
meta: serde_json::Value::Null,
outgoing: vec![EdgeRef {
kind: "calls".into(),
provenance: "derived",
confidence: None,
node: "sym:rust:a.rs#helper".into(),
}],
incoming: vec![EdgeRef {
kind: "references".into(),
provenance: "authored",
confidence: None,
node: "adr:0001".into(),
}],
};
let note = render_note(&ex, None, None);
assert_eq!(note.filename, "sym-rust-a.rs-main.md");
assert!(note.content.contains("kind: fn"));
assert!(!note.content.contains("**Source:**"));
assert!(note.content.contains("# main"));
assert!(
note.content
.contains("- calls (derived) → [[sym-rust-a.rs-helper]]")
);
assert!(
note.content
.contains("- [[adr-0001]] references (authored) →")
);
assert!(note.content.contains("- roteiro/kind/fn"));
assert!(note.content.contains("- roteiro/lang/rust"));
}
#[test]
fn note_name_bounds_long_keys_deterministically() {
let long = format!("import:rust:{}", "a::b::c,".repeat(60));
let a = note_name(&long);
let b = note_name(&long);
assert_eq!(a, b, "deterministic");
assert!(
a.len() <= 205,
"bounded under the filename limit: {}",
a.len()
);
assert_ne!(
note_name(&format!("{long}x")),
a,
"different keys stay distinct after truncation"
);
}
#[test]
fn render_note_surfaces_content_and_status() {
let ex = Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: "adr:0001".into(),
kind: "adr".into(),
name: "Build Roteiro".into(),
path: Some("docs/adr/0001.md".into()),
lang: None,
},
meta: serde_json::json!({ "status": "Accepted", "content": "The decision text." }),
outgoing: vec![],
incoming: vec![],
};
let note = render_note(&ex, Some("https://github.com/org/repo/blob/abc123"), None);
assert!(note.content.contains("status: Accepted"));
assert!(note.content.contains("- roteiro/status/accepted"));
assert!(note.content.contains("> **Status:** Accepted"));
assert!(note.content.contains("## Content\n\nThe decision text."));
assert!(
note.content.contains(
"**Source:** [`docs/adr/0001.md`](https://github.com/org/repo/blob/abc123/docs/adr/0001.md)"
),
"{}",
note.content
);
}
const DOC: &str = "# Working offline\n\nRoteiro is **offline-capable**.\n\n| Host | What |\n| --- | --- |\n| `example.com` | models |\n\n```sh\nroteiro model pull\n```\n";
fn prose_note(content: Option<&str>) -> Explanation {
Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: "file:docs/OFFLINE_SETUP.md".into(),
kind: "file".into(),
name: "OFFLINE_SETUP.md".into(),
path: Some("docs/OFFLINE_SETUP.md".into()),
lang: None,
},
meta: content.map_or(
serde_json::Value::Null,
|c| serde_json::json!({ "content": c }),
),
outgoing: vec![],
incoming: vec![],
}
}
#[test]
fn a_supplied_body_supersedes_the_collapsed_stored_content() {
let collapsed = DOC.split_whitespace().collect::<Vec<_>>().join(" ");
let ex = prose_note(Some(&collapsed));
let note = render_note(&ex, None, Some(DOC));
assert!(
note.content.contains(DOC.trim()),
"the source document is reproduced verbatim: {}",
note.content
);
assert!(
!note.content.contains(&collapsed),
"the collapsed rendering is replaced, not appended: {}",
note.content
);
assert!(
note.content.contains("\n| Host | What |\n"),
"a table needs its own lines to be a table: {}",
note.content
);
assert!(
note.content.contains("\n```sh\n"),
"a fenced block needs its own lines to be a fence: {}",
note.content
);
let flat = render_note(&ex, None, None);
assert!(
flat.content.contains(&collapsed),
"without a body the stored content is still shown: {}",
flat.content
);
assert!(
content_lines(¬e.content) > content_lines(&flat.content),
"structure restored: {} line(s) with a body vs {} without",
content_lines(¬e.content),
content_lines(&flat.content)
);
assert_eq!(
content_lines(&flat.content),
1,
"the defect: the stored content is a single line"
);
}
#[test]
fn a_note_with_no_body_is_unchanged() {
let ex = Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: "sym:rust:a.rs#main".into(),
kind: "fn".into(),
name: "main".into(),
path: Some("a.rs".into()),
lang: Some("rust".into()),
},
meta: serde_json::json!({ "content": "Entry point." }),
outgoing: vec![],
incoming: vec![],
};
assert!(
render_note(&ex, None, None)
.content
.contains("## Content\n\nEntry point.")
);
}
fn content_lines(note: &str) -> usize {
let body = note
.split_once("## Content\n\n")
.map_or("", |(_, rest)| rest);
let body = body.split_once("\n## ").map_or(body, |(head, _)| head);
body.trim_end().lines().count()
}
#[test]
fn render_note_shows_inferred_confidence() {
let ex = Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: "file:a.md".into(),
kind: "file".into(),
name: "a.md".into(),
path: Some("a.md".into()),
lang: None,
},
meta: serde_json::Value::Null,
outgoing: vec![EdgeRef {
kind: "related".into(),
provenance: "inferred",
confidence: Some(0.82),
node: "file:b.md".into(),
}],
incoming: vec![],
};
let note = render_note(&ex, None, None);
assert!(
note.content
.contains("related (inferred) (0.82) → [[file-b.md]]"),
"{}",
note.content
);
}
#[test]
fn render_home_summarises_the_graph() {
let summary = VaultSummary {
project: "demo".into(),
total_nodes: 3,
total_edges: 2,
node_counts: vec![("fn".into(), 2), ("adr".into(), 1)],
edge_provenance: vec![("derived".into(), 1), ("authored".into(), 1)],
adrs: vec![AdrEntry {
key: "adr:0001".into(),
name: "First".into(),
status: Some("Accepted".into()),
}],
debt: vec![("todo".into(), 4)], densest_files: vec![DensityEntry {
path: "src/small.rs".into(),
markers: 3,
lines: 120,
per_kloc: 25.0,
}],
config_secrets: Some(ConfigSecretSummary {
secret_named: 4,
redacted: 3,
declared: 1,
unredacted: 0,
files: vec![".env".into()],
}),
most_called: vec![CouplingEntry {
key: "sym:rust:a.rs#helper".into(),
name: "helper".into(),
fan_in: 7,
fan_out: 1,
}],
repo_url: Some("https://github.com/org/repo".into()),
commit: Some("abcdef0123456789".into()),
};
let note = render_home(&summary);
assert_eq!(note.filename, HOME_NOTE);
assert!(note.content.contains("# demo — knowledge graph"));
assert!(note.content.contains("**3 nodes**, **2 edges**"));
assert!(note.content.contains("| fn | 2 |"));
assert!(note.content.contains("| derived | 1 |"));
assert!(note.content.contains("**Accepted** — [[adr-0001|First]]"));
assert!(note.content.contains("| todo | 4 |")); assert!(
note.content
.contains("| [[sym-rust-a.rs-helper\\|helper]] | 7 | 1 |"),
"{}",
note.content
);
assert!(
note.content.contains("resolved by simple name"),
"the precision caveat travels with the figures"
);
assert!(
note.content
.contains("| [[file-src-small.rs\\|src/small.rs]] | 3 | 120 | 25.00 |"),
"{}",
note.content
);
assert!(
note.content.contains("not source lines of code"),
"the denominator caveat travels with the figures"
);
assert!(
note.content.contains(
"**4** secret-named config key(s): 3 redacted before storage, 1 \
declared in code without a value, 0 unredacted."
),
"{}",
note.content
);
assert!(
note.content.contains("- [[file-.env\\|.env]]"),
"{}",
note.content
);
assert!(
note.content.contains("not a secret scan")
&& note.content.contains("cannot see a hardcoded credential"),
"the limitation travels with the figures: {}",
note.content
);
assert!(
!note.content.contains("[!warning]"),
"no warning when nothing is unredacted: {}",
note.content
);
assert!(
note.content
.contains("**Repository:** [https://github.com/org/repo](https://github.com/org/repo) · rendered at commit `abcdef012345`"),
"{}",
note.content
);
}
#[test]
fn render_home_omits_density_for_a_graph_with_no_markers() {
let note = render_home(&VaultSummary {
project: "clean".into(),
total_nodes: 1,
..VaultSummary::default()
});
assert!(
!note.content.contains("Densest files"),
"no heading without rows: {}",
note.content
);
assert!(note.content.contains("## Intent debt"));
assert!(note.content.contains("*None recorded.*"));
}
#[test]
fn render_home_omits_config_secrets_rather_than_rendering_zeroes() {
let note = render_home(&VaultSummary {
project: "clean".into(),
total_nodes: 1,
..VaultSummary::default()
});
assert!(
!note.content.contains("named like secrets"),
"no heading without figures: {}",
note.content
);
}
#[test]
fn render_home_warns_loudly_about_an_unredacted_value() {
let note = render_home(&VaultSummary {
project: "imported".into(),
total_nodes: 1,
config_secrets: Some(ConfigSecretSummary {
secret_named: 1,
redacted: 0,
declared: 0,
unredacted: 1,
files: vec!["imported.env".into()],
}),
..VaultSummary::default()
});
assert!(
note.content.contains("[!warning]") && note.content.contains("**unredacted**"),
"{}",
note.content
);
assert!(
note.content.contains("came from an import layer"),
"and it points at the importing tool, not the repository: {}",
note.content
);
}
#[test]
fn render_home_omits_coupling_for_a_graph_with_no_calls() {
let note = render_home(&VaultSummary {
project: "docs".into(),
total_nodes: 1,
..VaultSummary::default()
});
assert!(
!note.content.contains("Most depended-on"),
"no heading without rows: {}",
note.content
);
assert!(note.content.contains("# docs — knowledge graph"));
}
fn node_linking_to(key: &str, name: &str, to: &str) -> Explanation {
Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: key.into(),
kind: "config_key".into(),
name: name.into(),
path: Some("config.toml".into()),
lang: None,
},
meta: serde_json::Value::Null,
outgoing: vec![EdgeRef {
kind: "links".into(),
provenance: "inferred",
confidence: Some(0.91),
node: to.into(),
}],
incoming: vec![],
}
}
fn members(names: &[&str]) -> std::collections::BTreeSet<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn a_project_scope_leaves_every_note_name_exactly_as_it_was() {
for key in [
"file:README.md",
"adr:0001",
"sym:rust:src/a.rs#Store",
"extref:other::file:README.md",
"cfgkey:config.toml#serve.addr",
] {
assert_eq!(
scoped_note_name(&VaultScope::PROJECT, key),
note_name(key),
"single-project name moved for `{key}`"
);
}
}
#[test]
fn render_note_is_the_project_scoped_render_byte_for_byte() {
let ex = node_linking_to("cfgkey:config.toml#addr", "addr", "sym:rust:a.rs#A");
assert_eq!(
render_note(&ex, Some("https://h/b"), Some("body")),
render_note_scoped(&ex, Some("https://h/b"), Some("body"), &VaultScope::PROJECT),
"the unscoped entry point must stay the scoped one at PROJECT, so the \
two cannot drift apart"
);
}
#[test]
fn each_member_gets_its_own_note_for_the_same_key() {
let ms = members(&["api", "sdk"]);
let names: Vec<String> = ["api", "sdk"]
.iter()
.map(|p| {
scoped_note_name(
&VaultScope {
project: Some(p),
members: &ms,
},
"file:README.md",
)
})
.collect();
assert_eq!(names, ["api-file-README.md", "sdk-file-README.md"]);
assert_ne!(names[0], names[1], "two members must not share one note");
}
#[test]
fn the_qualified_key_and_the_note_name_are_different_strings() {
let ms = members(&["app"]);
let scope = VaultScope {
project: Some("app"),
members: &ms,
};
let qualified = "app::file:README.md";
assert_eq!(
scoped_note_name(&scope, "file:README.md"),
"app-file-README.md"
);
assert_eq!(note_name(qualified), "app-file-README.md");
assert!(
!scoped_note_name(&scope, "file:README.md").contains("::"),
"no note name ever contains `::`"
);
let note = render_note_scoped(
&node_with("file:README.md", Some("README.md"), None),
None,
None,
&scope,
);
assert_eq!(note.filename, "app-file-README.md.md");
}
#[test]
fn a_member_note_declares_which_member_it_came_from() {
let ms = members(&["api"]);
let ex = node_linking_to("cfgkey:config.toml#addr", "addr", "sym:rust:a.rs#A");
let note = render_note_scoped(
&ex,
None,
None,
&VaultScope {
project: Some("api"),
members: &ms,
},
);
assert_eq!(note.filename, "api-cfgkey-config.toml-addr.md");
assert!(
note.content.contains("project: \"api\""),
"{}",
note.content
);
assert!(
note.content.contains("- roteiro/project/api"),
"the tag is what filters the graph view to one repository: {}",
note.content
);
assert!(
note.content.contains("→ [[api-sym-rust-a.rs-A]]"),
"{}",
note.content
);
}
#[test]
fn a_project_note_declares_no_project() {
let ex = node_linking_to("cfgkey:config.toml#addr", "addr", "sym:rust:a.rs#A");
let note = render_note(&ex, None, None);
assert!(!note.content.contains("project:"), "{}", note.content);
assert!(
!note.content.contains("roteiro/project/"),
"a per-project vault would carry one constant on every note — and \
adding it would change every note's bytes: {}",
note.content
);
}
#[test]
fn a_cross_repo_edge_links_straight_to_the_other_members_note() {
let ms = members(&["spoke", "hub"]);
let scope = VaultScope {
project: Some("spoke"),
members: &ms,
};
let ex = node_linking_to(
"cfgkey:config.toml#addr",
"addr",
&rto_graph::external_ref_key("hub::cfgkey:config.toml#addr"),
);
let note = render_note_scoped(&ex, None, None, &scope);
assert!(
note.content.contains("→ [[hub-cfgkey-config.toml-addr]]"),
"the edge must land on the hub's own note: {}",
note.content
);
assert!(
!note.content.contains("extref"),
"and never on the placeholder: {}",
note.content
);
assert!(
scope.redirects_external_ref(&rto_graph::external_ref_key(
"hub::cfgkey:config.toml#addr"
))
);
}
#[test]
fn a_cross_repo_edge_out_of_the_workspace_keeps_its_placeholder() {
let ms = members(&["spoke"]);
let scope = VaultScope {
project: Some("spoke"),
members: &ms,
};
let key = rto_graph::external_ref_key("elsewhere::cfgkey:config.toml#addr");
assert!(!scope.redirects_external_ref(&key));
let ex = node_linking_to("cfgkey:config.toml#addr", "addr", &key);
let note = render_note_scoped(&ex, None, None, &scope);
assert!(
note.content
.contains("→ [[spoke-extref-elsewhere-cfgkey-config.toml-addr]]"),
"{}",
note.content
);
}
#[test]
fn a_single_project_vault_never_redirects_an_external_ref() {
let key = rto_graph::external_ref_key("hub::cfgkey:config.toml#addr");
assert!(!VaultScope::PROJECT.redirects_external_ref(&key));
assert_eq!(
scoped_note_name(&VaultScope::PROJECT, &key),
note_name(&key)
);
}
fn member_summary(project: &str, fan_in: u32) -> VaultSummary {
VaultSummary {
project: project.to_owned(),
total_nodes: 3,
total_edges: 2,
node_counts: vec![("fn".into(), 2)],
edge_provenance: vec![("derived".into(), 2)],
adrs: vec![AdrEntry {
key: "adr:0001".into(),
name: "First".into(),
status: Some("Accepted".into()),
}],
debt: vec![("todo".into(), 4)], densest_files: vec![DensityEntry {
path: "src/small.rs".into(),
markers: 3,
lines: 120,
per_kloc: 25.0,
}],
config_secrets: None,
most_called: vec![CouplingEntry {
key: "sym:rust:a.rs#helper".into(),
name: "helper".into(),
fan_in,
fan_out: 1,
}],
repo_url: Some(format!("https://github.com/org/{project}")),
commit: Some("abcdef0123456789".into()),
}
}
#[test]
fn the_workspace_home_keeps_every_members_own_aggregates() {
let ws = WorkspaceSummary {
name: "platform".into(),
members: vec![member_summary("api", 7), member_summary("sdk", 4)],
cross_links: vec![],
cross_links_total: 0,
};
let note = render_workspace_home(&ws);
assert_eq!(note.filename, HOME_NOTE);
assert!(
note.content
.contains("# platform — workspace knowledge graph")
);
assert!(
note.content
.contains("**6 nodes**, **4 edges** across **2** member")
);
assert!(note.content.contains("| [[#api\\|api]] | 3 | 2 |"));
for project in ["api", "sdk"] {
assert!(
note.content.contains(&format!("\n## {project}\n")),
"each member gets its own section"
);
}
for section in [
"### Structure",
"### Provenance",
"### Decisions (ADRs)",
"### Intent debt",
"#### Densest files",
"### Most depended-on",
] {
assert_eq!(
note.content.matches(section).count(),
2,
"`{section}` must appear once per member: {}",
note.content
);
}
assert!(
note.content
.contains("**Accepted** — [[api-adr-0001|First]]")
);
assert!(
note.content
.contains("**Accepted** — [[sdk-adr-0001|First]]")
);
assert!(
note.content
.contains("[[api-sym-rust-a.rs-helper\\|helper]] | 7 |")
);
assert!(
note.content
.contains("[[sdk-file-src-small.rs\\|src/small.rs]]")
);
}
#[test]
fn the_workspace_home_renders_cross_repo_links_and_marks_the_ones_it_cannot_follow() {
let ws = WorkspaceSummary {
name: "platform".into(),
members: vec![member_summary("api", 7), member_summary("sdk", 4)],
cross_links: vec![
CrossLink {
from_project: "sdk".into(),
from_key: "cfgkey:config.toml#addr".into(),
from_name: "addr".into(),
kind: "links".into(),
confidence: Some(0.91),
to_qualified: "api::cfgkey:config.toml#addr".into(),
resolves: true,
},
CrossLink {
from_project: "sdk".into(),
from_key: "cfgkey:config.toml#other".into(),
from_name: "other".into(),
kind: "links".into(),
confidence: None,
to_qualified: "absent::cfgkey:config.toml#other".into(),
resolves: false,
},
],
cross_links_total: 2,
};
let note = render_workspace_home(&ws);
assert!(
note.content.contains(
"| [[sdk-cfgkey-config.toml-addr\\|addr]] | sdk | \
[[api-cfgkey-config.toml-addr\\|api::cfgkey:config.toml#addr]] | links (0.91) |"
),
"{}",
note.content
);
assert!(
note.content
.contains("`absent::cfgkey:config.toml#other` *(outside this workspace)*"),
"{}",
note.content
);
assert!(
!note.content.contains("[[absent-"),
"a dangling wikilink would read as a note someone forgot to write: {}",
note.content
);
}
#[test]
fn the_workspace_home_says_when_it_has_truncated_the_cross_links() {
let ws = WorkspaceSummary {
name: "platform".into(),
members: vec![member_summary("api", 7)],
cross_links: vec![CrossLink {
from_project: "api".into(),
from_key: "cfgkey:config.toml#addr".into(),
from_name: "addr".into(),
kind: "links".into(),
confidence: None,
to_qualified: "api::cfgkey:config.toml#addr".into(),
resolves: true,
}],
cross_links_total: 40,
};
let note = render_workspace_home(&ws);
assert!(note.content.contains("Showing 1 of 40"), "{}", note.content);
assert!(note.content.contains("roteiro links --matrix"));
}
#[test]
fn a_workspace_with_no_cross_repo_links_says_why_rather_than_showing_nothing() {
let ws = WorkspaceSummary {
name: "platform".into(),
members: vec![member_summary("api", 7)],
cross_links: vec![],
cross_links_total: 0,
};
let note = render_workspace_home(&ws);
assert!(note.content.contains("## Cross-repo links"));
assert!(
note.content.contains("links --infer --write"),
"an empty section must name what would fill it, or it reads as \
\"these repos are unrelated\": {}",
note.content
);
assert!(note.content.contains("**1** member repository."));
}
fn frontmatter_field(note: &str, field: &str) -> Result<Option<String>, String> {
let block = note
.strip_prefix("---\n")
.and_then(|rest| rest.split_once("\n---\n"))
.map(|(block, _)| block)
.expect("note must open with a frontmatter block");
let docs = yaml_rust2::YamlLoader::load_from_str(block).map_err(|e| e.to_string())?;
Ok(docs[0][field].as_str().map(ToOwned::to_owned))
}
fn node_with(key: &str, path: Option<&str>, lang: Option<&str>) -> Explanation {
Explanation {
schema: rto_graph::SCHEMA,
node: NodeSummary {
key: key.into(),
kind: "fn".into(),
name: "n".into(),
path: path.map(ToOwned::to_owned),
lang: lang.map(ToOwned::to_owned),
},
meta: serde_json::Value::Null,
outgoing: vec![],
incoming: vec![],
}
}
#[test]
fn a_backslash_or_quote_in_a_path_still_parses_back_to_itself() {
for path in [
r"foo\bar", r"foo\dir", "say\"hi\".rs", r"a\\b",
"trailing-backslash\\",
] {
let note = render_note(&node_with("file:x", Some(path), None), None, None);
assert_eq!(
frontmatter_field(¬e.content, "path"),
Ok(Some(path.to_owned())),
"path {path:?} must round-trip"
);
}
}
#[test]
fn a_node_key_round_trips_whatever_punctuation_it_carries() {
for key in [
"sym:rust:src/a.rs#Store",
r"sym:rust:src\weird.rs#Thing",
"sym:rust:a.rs#say\"hi\"",
"cfgkey:config.toml#serve.addr",
] {
let note = render_note(&node_with(key, None, None), None, None);
assert_eq!(
frontmatter_field(¬e.content, "key"),
Ok(Some(key.to_owned())),
"key {key:?} must round-trip"
);
}
let note = render_note(
&node_with("sym:rust:a.rs#say\"hi\"", None, None),
None,
None,
);
assert!(
!note.content.contains("say'hi'"),
"a quotation mark must be escaped, not rewritten: {}",
note.content
);
}
#[test]
fn a_member_project_name_round_trips() {
let ms: std::collections::BTreeSet<String> =
std::iter::once(r"odd\name".to_owned()).collect();
let note = render_note_scoped(
&node_with("file:x", None, None),
None,
None,
&VaultScope {
project: Some(r"odd\name"),
members: &ms,
},
);
assert_eq!(
frontmatter_field(¬e.content, "project"),
Ok(Some(r"odd\name".to_owned()))
);
}
#[test]
fn a_bare_field_is_quoted_only_when_being_bare_would_change_it() {
let with_status = |status: &str| {
let mut ex = node_with("adr:0001", None, None);
ex.meta = serde_json::json!({ "status": status });
render_note(&ex, None, None)
};
for status in [
"Accepted: superseded by 0012",
"Accepted # pending",
"{draft}",
"",
] {
let note = with_status(status);
assert_eq!(
frontmatter_field(¬e.content, "status"),
Ok(Some(status.to_owned())),
"status {status:?} must round-trip"
);
}
let note = with_status("Accepted");
assert!(
note.content.contains("\nstatus: Accepted\n"),
"a plain-safe status must not gain quotes: {}",
note.content
);
}
#[test]
fn a_language_that_spells_a_yaml_boolean_is_quoted() {
let note = render_note(&node_with("file:x", None, Some("no")), None, None);
assert!(
note.content.contains("\nlang: \"no\"\n"),
"a bare `no` is `false` to a 1.1 parser and must be quoted: {}",
note.content
);
assert_eq!(
frontmatter_field(¬e.content, "lang"),
Ok(Some("no".to_owned())),
"and it must still read back as the string: {}",
note.content
);
let rust = render_note(&node_with("file:x", None, Some("rust")), None, None);
assert!(rust.content.contains("\nlang: rust\n"), "{}", rust.content);
}
#[test]
fn control_characters_cannot_break_out_of_the_block() {
for path in [
"a\nb",
"a\tb",
"a\u{0}b",
"a\u{2028}b",
"a\u{7f}b",
"a\u{85}b",
] {
let note = render_note(&node_with("file:x", Some(path), None), None, None);
assert_eq!(
frontmatter_field(¬e.content, "path"),
Ok(Some(path.to_owned())),
"path {path:?} must round-trip"
);
assert_eq!(
note.content.matches("\npath: ").count(),
1,
"the value must stay on one line: {}",
note.content
);
}
}
#[test]
fn an_ordinary_value_is_emitted_exactly_as_before() {
let note = render_note(
&node_with("sym:rust:src/a.rs#Store", Some("src/a.rs"), Some("rust")),
None,
None,
);
assert!(
note.content
.contains("\nkey: \"sym:rust:src/a.rs#Store\"\n")
);
assert!(note.content.contains("\nkind: fn\n"));
assert!(note.content.contains("\npath: \"src/a.rs\"\n"));
assert!(note.content.contains("\nlang: rust\n"));
}
#[test]
fn the_plain_style_decision_agrees_with_a_real_yaml_parser() {
for value in [
"fn",
"config_key",
"rust",
"Accepted",
"a.b",
"a/b",
"a-b_c",
"no",
"yes",
"true",
"null",
"y",
"N",
"",
" lead",
"trail ",
"a: b",
"a #c",
"{x}",
"[x]",
"*x",
"&x",
"!x",
"#x",
">x",
"|x",
"%x",
"@x",
"`x",
"\"x",
"'x",
",x",
"123",
"1.5",
"-x",
".x",
"a\\b",
] {
let emitted = super::yaml_scalar(value);
let doc = format!("v: {emitted}");
let parsed = yaml_rust2::YamlLoader::load_from_str(&doc)
.unwrap_or_else(|e| panic!("{value:?} emitted {emitted:?}: {e}"));
assert_eq!(
parsed[0]["v"].as_str(),
Some(value),
"{value:?} emitted as {emitted:?} did not round-trip"
);
}
}
}