use std::collections::BTreeMap;
use rto_graph::Provenance;
pub struct MatchInput {
pub hub_key: String,
pub file: String,
pub spoke_key: String,
pub spoke_value: String,
pub confidence: f64,
pub provenance: Provenance,
}
pub struct SpokeInput {
pub name: String,
pub matches: Vec<MatchInput>,
pub orphans: Vec<(String, String)>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Cell {
pub value: String,
pub spoke_key: String,
pub confidence: f64,
pub provenance: Provenance,
pub differs: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Row {
pub hub_key: String,
pub file: String,
pub hub_value: String,
pub cells: BTreeMap<String, Cell>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DriftCell {
pub value: String,
pub conflict: bool,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct Drift {
pub key: String,
pub cells: BTreeMap<String, DriftCell>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct OverrideMatrix {
pub hub: String,
pub spokes: Vec<String>,
pub rows: Vec<Row>,
pub drift: Vec<Drift>,
}
#[must_use]
pub fn build(
hub: &str,
hub_values: &BTreeMap<String, String>,
spokes: Vec<SpokeInput>,
) -> OverrideMatrix {
type DriftValues = std::collections::BTreeMap<String, std::collections::BTreeSet<String>>;
let mut rows: BTreeMap<String, Row> = BTreeMap::new();
let mut columns: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut drift: BTreeMap<String, DriftValues> = BTreeMap::new();
let mut ambiguous_file: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for spoke in spokes {
for m in spoke.matches {
let hub_value = hub_values.get(&m.hub_key).cloned().unwrap_or_default();
let differs = hub_value != m.spoke_value;
let row = rows.entry(m.hub_key.clone()).or_insert_with(|| Row {
hub_key: m.hub_key.clone(),
file: String::new(),
hub_value: hub_value.clone(),
cells: BTreeMap::new(),
});
if !m.file.is_empty() && !ambiguous_file.contains(&m.hub_key) {
if row.file.is_empty() {
row.file.clone_from(&m.file);
} else if row.file != m.file {
row.file.clear();
ambiguous_file.insert(m.hub_key.clone());
}
}
let cell = Cell {
value: m.spoke_value,
spoke_key: m.spoke_key,
confidence: m.confidence,
provenance: m.provenance,
differs,
};
match row.cells.entry(spoke.name.clone()) {
std::collections::btree_map::Entry::Vacant(v) => {
v.insert(cell);
}
std::collections::btree_map::Entry::Occupied(mut o) => {
if cell.differs && !o.get().differs {
o.insert(cell);
}
}
}
columns.insert(spoke.name.clone());
}
for (key, value) in spoke.orphans {
let vals = drift
.entry(key)
.or_default()
.entry(spoke.name.clone())
.or_default();
if !value.is_empty() {
vals.insert(value);
}
columns.insert(spoke.name.clone());
}
}
let drift = drift
.into_iter()
.map(|(key, spokes)| Drift {
cells: spokes
.into_iter()
.map(|(spoke, values)| (spoke, drift_cell(values)))
.collect(),
key,
})
.collect();
OverrideMatrix {
hub: hub.to_owned(),
spokes: columns.into_iter().collect(),
rows: rows.into_values().collect(),
drift,
}
}
fn drift_cell(values: std::collections::BTreeSet<String>) -> DriftCell {
let conflict = values.len() > 1;
DriftCell {
value: values.into_iter().collect::<Vec<_>>().join(" | "),
conflict,
}
}
#[must_use]
pub fn is_empty(m: &OverrideMatrix) -> bool {
m.rows.is_empty() && m.drift.is_empty()
}
#[must_use]
pub fn render_text(m: &OverrideMatrix) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(
out,
"cross-repo config overrides (hub: {}, {} spoke(s))",
m.hub,
m.spokes.len()
);
for row in &m.rows {
let _ = writeln!(out, "\n {} = {}", row.hub_key, row.hub_value);
for spoke in &m.spokes {
if let Some(cell) = row.cells.get(spoke) {
let flag = if cell.differs { "≠" } else { "=" };
let _ = writeln!(
out,
" {flag} {spoke}: {} ({:.2})",
cell.value, cell.confidence
);
}
}
}
if !m.drift.is_empty() {
let _ = writeln!(out, "\n drift — {} orphan key(s):", m.drift.len());
for d in &m.drift {
let _ = writeln!(out, "\n {}", d.key);
for (spoke, cell) in &d.cells {
let flag = if cell.conflict { " (conflict)" } else { "" };
let _ = writeln!(out, " {spoke}: {}{flag}", cell.value);
}
}
}
out
}
#[must_use]
pub fn render_html(m: &OverrideMatrix) -> String {
use std::fmt::Write as _;
let mut thead = String::from("<th scope=\"col\">config key</th><th scope=\"col\">hub</th>");
for s in &m.spokes {
let _ = write!(thead, "<th scope=\"col\">{}</th>", esc(s));
}
let mut tbody = String::new();
for row in &m.rows {
let _ = write!(
tbody,
"<tr><th scope=\"row\"><code>{}</code></th><td class=\"hub\"><code>{}</code></td>",
esc(&row.hub_key),
esc(&row.hub_value)
);
for spoke in &m.spokes {
match row.cells.get(spoke) {
Some(cell) => {
let cls = if cell.differs {
"cell over"
} else {
"cell same"
};
let _ = write!(
tbody,
"<td class=\"{cls}\"><code>{}</code>\
<span class=\"conf\" title=\"confidence\">{:.2}</span></td>",
esc(&cell.value),
cell.confidence
);
}
None => tbody.push_str("<td class=\"cell none\">·</td>"),
}
}
tbody.push_str("</tr>");
}
let drift = if m.drift.is_empty() {
String::new()
} else {
let mut dhead = String::from("<th scope=\"col\">key</th>");
for s in &m.spokes {
let _ = write!(dhead, "<th scope=\"col\">{}</th>", esc(s));
}
let mut rows = String::new();
for d in &m.drift {
let _ = write!(rows, "<tr><td><code>{}</code></td>", esc(&d.key));
for spoke in &m.spokes {
match d.cells.get(spoke) {
Some(cell) if cell.conflict => {
let _ = write!(
rows,
"<td class=\"cell over conflict\" \
title=\"conflict: this deploy sets the key to multiple values\">\
<code>{}</code></td>",
esc(&cell.value)
);
}
Some(cell) => {
let _ = write!(
rows,
"<td class=\"cell over\"><code>{}</code></td>",
esc(&cell.value)
);
}
None => rows.push_str("<td class=\"cell none\">·</td>"),
}
}
rows.push_str("</tr>");
}
format!(
"<h2>Drift — {} orphan key(s)</h2>\
<p class=\"muted\">Spoke keys with no hub counterpart: the app doesn't \
define these, so a rename or removal in the hub can't warn you.</p>\
<table class=\"drift\"><thead><tr>{dhead}</tr></thead>\
<tbody>{rows}</tbody></table>",
m.drift.len()
)
};
let body = if is_empty(m) {
"<p class=\"muted\">No cross-repo config overrides or drift found.</p>".to_owned()
} else {
format!(
"<table class=\"matrix\"><thead><tr>{thead}</tr></thead><tbody>{tbody}</tbody></table>\
<p class=\"legend\"><span class=\"swatch over\"></span> overrides the hub value \
<span class=\"swatch same\"></span> matches it (redundant) \
<span class=\"swatch none\"></span> not set</p>{drift}"
)
};
format!(
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<title>Cross-repo config overrides — {hub}</title><style>{CSS}</style></head><body>\
<main><h1>Cross-repo config overrides</h1>\
<p class=\"muted\">Hub <strong>{hub}</strong> · {nspokes} spoke(s) · ADR-0009</p>\
{body}</main></body></html>",
hub = esc(&m.hub),
nspokes = m.spokes.len(),
)
}
const CSS: &str = "\
:root{--bg:#fff;--fg:#1a1a2e;--muted:#6b7280;--line:#e5e7eb;--hub:#f3f4f6;\
--over:#fef3c7;--over-fg:#92400e;--same:#ecfdf5;--same-fg:#065f46;--accent:#4f46e5}\
@media(prefers-color-scheme:dark){:root{--bg:#0f1117;--fg:#e5e7eb;--muted:#9ca3af;\
--line:#262b36;--hub:#1a1d27;--over:#3b2f10;--over-fg:#fcd34d;--same:#0f2a1f;\
--same-fg:#6ee7b7;--accent:#a5b4fc}}\
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);\
font:15px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}\
main{max-width:1100px;margin:0 auto;padding:2rem 1.25rem}\
h1{font-size:1.5rem;margin:0 0 .25rem}h2{font-size:1.15rem;margin:2rem 0 .5rem}\
.muted{color:var(--muted);margin:.25rem 0 1.5rem}\
code{font:13px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace}\
table{border-collapse:collapse;width:100%;overflow-x:auto;display:block}\
@media(min-width:720px){table{display:table}}\
th,td{border:1px solid var(--line);padding:.4rem .6rem;text-align:left;vertical-align:top}\
thead th{position:sticky;top:0;background:var(--bg);font-size:.8rem;\
text-transform:uppercase;letter-spacing:.03em;color:var(--muted)}\
tbody th[scope=row]{background:var(--hub);white-space:nowrap}\
td.hub{background:var(--hub);color:var(--muted)}\
td.cell{white-space:nowrap}td.over{background:var(--over);color:var(--over-fg)}\
td.same{background:var(--same);color:var(--same-fg)}td.none{color:var(--muted);text-align:center}\
.conf{display:inline-block;margin-left:.4rem;font-size:.7rem;opacity:.7;\
font-variant-numeric:tabular-nums}\
.legend{color:var(--muted);font-size:.85rem;margin:1rem 0}\
.swatch{display:inline-block;width:.8rem;height:.8rem;border-radius:3px;\
vertical-align:-1px;border:1px solid var(--line)}\
.swatch.over{background:var(--over)}.swatch.same{background:var(--same)}\
.swatch.none{background:var(--bg)}\
table.drift td:first-child{white-space:nowrap;color:var(--muted)}\
td.conflict{outline:2px solid var(--over-fg);outline-offset:-2px;font-weight:600}";
fn esc(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
fn matrix() -> OverrideMatrix {
let hub_values = BTreeMap::from([
("serve.addr".to_owned(), "127.0.0.1:8017".to_owned()),
("serve.tools".to_owned(), "true".to_owned()),
]);
let spokes = vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![
MatchInput {
hub_key: "serve.addr".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_ADDR".to_owned(),
spoke_value: "0.0.0.0:8443".to_owned(), confidence: 0.9,
provenance: Provenance::Inferred,
},
MatchInput {
hub_key: "serve.tools".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_TOOLS".to_owned(),
spoke_value: "true".to_owned(), confidence: 0.98,
provenance: Provenance::Inferred,
},
],
orphans: vec![("MAX_CONNECTIONS".to_owned(), "512".to_owned())],
}];
build("app", &hub_values, spokes)
}
#[test]
fn build_pivots_matches_into_rows_and_flags_real_overrides() {
let m = matrix();
assert_eq!(m.hub, "app");
assert_eq!(m.spokes, vec!["deploy".to_owned()]);
assert_eq!(m.rows.len(), 2);
let addr = m.rows.iter().find(|r| r.hub_key == "serve.addr").unwrap();
assert!(
addr.cells["deploy"].differs,
"different value is an override"
);
let tools = m.rows.iter().find(|r| r.hub_key == "serve.tools").unwrap();
assert!(!tools.cells["deploy"].differs, "equal value is redundant");
assert_eq!(m.drift.len(), 1);
assert_eq!(m.drift[0].key, "MAX_CONNECTIONS");
}
#[test]
fn a_redundant_restatement_never_hides_a_real_override() {
let hub_values = BTreeMap::from([("serve.addr".to_owned(), "127.0.0.1:8017".to_owned())]);
let same = || MatchInput {
hub_key: "serve.addr".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "serve.addr".to_owned(),
spoke_value: "127.0.0.1:8017".to_owned(),
confidence: 0.98,
provenance: Provenance::Inferred,
};
let over = || MatchInput {
hub_key: "serve.addr".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_ADDR".to_owned(),
spoke_value: "0.0.0.0:8443".to_owned(),
confidence: 0.9,
provenance: Provenance::Inferred,
};
for matches in [vec![same(), over()], vec![over(), same()]] {
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches,
orphans: vec![],
}],
);
let cell = &m.rows[0].cells["deploy"];
assert!(
cell.differs,
"override must survive a redundant restatement"
);
assert_eq!(cell.value, "0.0.0.0:8443");
}
}
#[test]
fn build_carries_real_per_cell_provenance() {
let hub_values = BTreeMap::from([
("serve.addr".to_owned(), "127.0.0.1:8017".to_owned()),
("serve.tools".to_owned(), "true".to_owned()),
]);
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![
MatchInput {
hub_key: "serve.addr".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_ADDR".to_owned(),
spoke_value: "0.0.0.0:8443".to_owned(),
confidence: 0.0, provenance: Provenance::Authored,
},
MatchInput {
hub_key: "serve.tools".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_TOOLS".to_owned(),
spoke_value: "false".to_owned(),
confidence: 0.9,
provenance: Provenance::Inferred,
},
],
orphans: vec![],
}],
);
let addr = m.rows.iter().find(|r| r.hub_key == "serve.addr").unwrap();
assert_eq!(addr.cells["deploy"].provenance, Provenance::Authored);
let tools = m.rows.iter().find(|r| r.hub_key == "serve.tools").unwrap();
assert_eq!(tools.cells["deploy"].provenance, Provenance::Inferred);
let json = serde_json::to_value(&m).unwrap();
let addr_cell = &json["rows"]
.as_array()
.unwrap()
.iter()
.find(|r| r["hub_key"] == "serve.addr")
.unwrap()["cells"]["deploy"];
assert_eq!(addr_cell["provenance"], "authored");
}
#[test]
fn build_carries_the_hub_source_file_onto_each_row() {
let hub_values = BTreeMap::from([
("serve.addr".to_owned(), "127.0.0.1:8017".to_owned()),
("package.name".to_owned(), "roteiro".to_owned()),
]);
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![
MatchInput {
hub_key: "serve.addr".to_owned(),
file: "config.toml".to_owned(),
spoke_key: "SERVE_ADDR".to_owned(),
spoke_value: "0.0.0.0:8443".to_owned(),
confidence: 0.9,
provenance: Provenance::Inferred,
},
MatchInput {
hub_key: "package.name".to_owned(),
file: "Cargo.toml".to_owned(),
spoke_key: "PACKAGE_NAME".to_owned(),
spoke_value: "deploy".to_owned(),
confidence: 0.9,
provenance: Provenance::Inferred,
},
],
orphans: vec![],
}],
);
let app = m.rows.iter().find(|r| r.hub_key == "serve.addr").unwrap();
assert_eq!(app.file, "config.toml");
let tooling = m.rows.iter().find(|r| r.hub_key == "package.name").unwrap();
assert_eq!(tooling.file, "Cargo.toml");
let json = serde_json::to_value(&m).unwrap();
let tooling_json = json["rows"]
.as_array()
.unwrap()
.iter()
.find(|r| r["hub_key"] == "package.name")
.unwrap();
assert_eq!(tooling_json["file"], "Cargo.toml");
}
#[test]
fn a_hub_key_from_two_different_files_yields_an_ambiguous_empty_row_file() {
let hub_values = BTreeMap::from([("shared.key".to_owned(), "v".to_owned())]);
let m = |file: &str, spoke_key: &str| MatchInput {
hub_key: "shared.key".to_owned(),
file: file.to_owned(),
spoke_key: spoke_key.to_owned(),
spoke_value: "x".to_owned(),
confidence: 0.9,
provenance: Provenance::Inferred,
};
for matches in [
vec![
m("config.toml", "A"),
m("Cargo.toml", "B"),
m("config.toml", "C"),
],
vec![m("Cargo.toml", "B"), m("config.toml", "A")],
] {
let spokes = matches
.into_iter()
.enumerate()
.map(|(i, mat)| SpokeInput {
name: format!("spoke{i}"),
matches: vec![mat],
orphans: vec![],
})
.collect();
let built = build("app", &hub_values, spokes);
let row = built
.rows
.iter()
.find(|r| r.hub_key == "shared.key")
.unwrap();
assert_eq!(
row.file, "",
"conflicting hub-key files must leave the row file empty (unclassifiable), not an arbitrary pick"
);
}
let consistent = build(
"app",
&hub_values,
vec![
SpokeInput {
name: "a".to_owned(),
matches: vec![m("Cargo.toml", "A")],
orphans: vec![],
},
SpokeInput {
name: "b".to_owned(),
matches: vec![m("Cargo.toml", "B")],
orphans: vec![],
},
],
);
assert_eq!(consistent.rows[0].file, "Cargo.toml");
}
#[test]
fn render_html_is_self_contained_and_escapes() {
let m = matrix();
let html = render_html(&m);
assert!(html.starts_with("<!doctype html>"));
assert!(html.contains("<style>"), "inline CSS, no external asset");
assert!(!html.contains("href=\"style.css\""));
assert!(html.contains("serve.addr") && html.contains("0.0.0.0:8443"));
assert!(html.contains("MAX_CONNECTIONS"), "drift is shown");
assert!(html.contains("cell over") && html.contains("cell same"));
}
#[test]
fn render_html_escapes_injected_markup() {
let hub_values = BTreeMap::from([("k".to_owned(), "<v>".to_owned())]);
let m = build("app", &hub_values, vec![]);
let html = render_html(&m);
assert!(!html.contains("<v>"), "hub value must be escaped");
}
#[test]
fn text_table_marks_overrides_and_lists_drift() {
let t = render_text(&matrix());
assert!(t.contains("serve.addr = 127.0.0.1:8017"));
assert!(t.contains("≠ deploy: 0.0.0.0:8443"));
assert!(t.contains("= deploy: true"));
assert!(t.contains("drift") && t.contains("MAX_CONNECTIONS"));
}
#[test]
fn a_drift_key_set_by_multiple_spokes_is_one_row_with_a_cell_per_deploy() {
let hub_values = BTreeMap::new();
let m = build(
"app",
&hub_values,
vec![
SpokeInput {
name: "deploy-a".to_owned(),
matches: vec![],
orphans: vec![
("dq.mode".to_owned(), "strict".to_owned()),
("component".to_owned(), "ingest".to_owned()),
],
},
SpokeInput {
name: "deploy-b".to_owned(),
matches: vec![],
orphans: vec![("dq.mode".to_owned(), "lax".to_owned())],
},
],
);
assert_eq!(
m.drift.iter().filter(|d| d.key == "dq.mode").count(),
1,
"a drift key set by 2 deploys must collapse to a single row"
);
assert_eq!(m.drift.len(), 2);
assert_eq!(m.drift[0].key, "component");
assert_eq!(m.drift[1].key, "dq.mode");
let mode = m.drift.iter().find(|d| d.key == "dq.mode").unwrap();
assert_eq!(mode.cells.len(), 2);
assert_eq!(mode.cells["deploy-a"].value, "strict");
assert_eq!(mode.cells["deploy-b"].value, "lax");
let comp = m.drift.iter().find(|d| d.key == "component").unwrap();
assert_eq!(comp.cells.len(), 1);
assert_eq!(comp.cells["deploy-a"].value, "ingest");
assert!(m.spokes.contains(&"deploy-a".to_owned()));
assert!(m.spokes.contains(&"deploy-b".to_owned()));
let json = serde_json::to_value(&m).unwrap();
let drift = json["drift"].as_array().unwrap();
assert_eq!(drift.len(), 2);
assert_eq!(drift[1]["key"], "dq.mode");
assert_eq!(drift[1]["cells"]["deploy-a"]["value"], "strict");
assert_eq!(drift[1]["cells"]["deploy-b"]["value"], "lax");
}
#[test]
fn a_repeated_orphan_key_within_one_spoke_collapses_to_one_cell() {
let hub_values = BTreeMap::new();
for orphans in [
vec![
("component".to_owned(), String::new()),
("component".to_owned(), "ingest".to_owned()),
],
vec![
("component".to_owned(), "ingest".to_owned()),
("component".to_owned(), String::new()),
],
] {
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![],
orphans,
}],
);
assert_eq!(m.drift.len(), 1);
assert_eq!(m.drift[0].cells.len(), 1, "one deploy → one cell");
let cell = &m.drift[0].cells["deploy"];
assert_eq!(cell.value, "ingest");
assert!(!cell.conflict, "a blank restatement is not a conflict");
}
}
#[test]
fn a_spoke_setting_one_drift_key_two_ways_is_a_deterministic_conflict_cell() {
let hub_values = BTreeMap::new();
for orphans in [
vec![
("dq.mode".to_owned(), "strict".to_owned()),
("dq.mode".to_owned(), "lax".to_owned()),
],
vec![
("dq.mode".to_owned(), "lax".to_owned()),
("dq.mode".to_owned(), "strict".to_owned()),
],
] {
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![],
orphans,
}],
);
assert_eq!(m.drift.len(), 1);
let cell = &m.drift[0].cells["deploy"];
assert!(cell.conflict, "two differing non-empty values → a conflict");
assert_eq!(cell.value, "lax | strict");
let json = serde_json::to_value(&m).unwrap();
assert_eq!(json["drift"][0]["cells"]["deploy"]["conflict"], true);
assert_eq!(json["drift"][0]["cells"]["deploy"]["value"], "lax | strict");
}
let m = build(
"app",
&hub_values,
vec![SpokeInput {
name: "deploy".to_owned(),
matches: vec![],
orphans: vec![
("dq.mode".to_owned(), "strict".to_owned()),
("dq.mode".to_owned(), "strict".to_owned()),
],
}],
);
let cell = &m.drift[0].cells["deploy"];
assert!(!cell.conflict);
assert_eq!(cell.value, "strict");
}
}