1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! **Coverage-metrics grid SVG (S6 step 5)** — the cross-workspace completeness
//! dashboard as a self-contained static SVG.
//!
//! Where [`crate::autonom::megagate`] computes the verdict and
//! [`crate::viz::metro_feed`] draws the per-button metro lines, THIS renders the
//! whole served dimension at a glance: one ROW per workspace, three metric CELLS
//! per row — **ran** (the completeness gate: surface covered/total), **utfallsrum**
//! (outcome-space classes swept), **reachable** (the resolved metro / LAW-9 walk).
//! Each cell is GREEN when its metric is fully met, RED when it falls short, and a
//! neutral NA when the metric wasn't measured for that workspace.
//!
//! House style mirrors [`crate::viz::diagram`] / `docs::svg` / the `dep_graph_svg`
//! MCP tool: a hand-emitted, self-contained SVG — no JavaScript, no diagram
//! engine, no egui — so it renders verbatim in the Codeberg/GitHub web UI, a
//! markdown viewer, or the docs PDF. Every cell carries a `class` (`green`/`red`/
//! `na`) so the headless matrix can assert the grid as DATA (LAW 6).
use std::fmt::Write as _;
use crate::autonom::megagate::{MegaGateReport, WorkspaceGate};
/// XML-escape a label for safe inclusion in SVG text.
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
/// The metric columns, in render order. `bencher` (Task #39) is the perf-coverage
/// dimension: how many hot-paths carry a bencher vs total discovered.
const COLUMNS: &[&str] = &["ran", "utfallsrum", "reachable", "bencher"];
/// One rendered cell: its display text + its class (`green`/`red`/`na`).
struct Cell {
text: String,
class: &'static str,
}
/// A `(met, total)` rollup → a cell: GREEN iff `met == total` (and measured),
/// else RED, rendered `met/total`.
fn ratio_cell(rollup: Option<(usize, usize)>) -> Cell {
match rollup {
Some((met, total)) => Cell {
text: format!("{met}/{total}"),
class: if met == total { "green" } else { "red" },
},
None => Cell { text: "—".into(), class: "na" },
}
}
/// The `ran` (completeness) cell for a workspace: covered/total surface nodes,
/// GREEN iff the gate is green (gap empty AND no stale allowlist entry).
fn ran_cell(wg: &WorkspaceGate) -> Cell {
let g = &wg.coverage.gap;
Cell {
text: format!("{}/{}", g.covered + g.allowlisted.len(), g.total),
class: if wg.coverage.is_green() { "green" } else { "red" },
}
}
/// The three cells for one workspace, in [`COLUMNS`] order.
fn cells_for(wg: &WorkspaceGate) -> Vec<Cell> {
vec![
ran_cell(wg),
ratio_cell(wg.utfallsrum),
ratio_cell(wg.reachable),
ratio_cell(wg.bencher),
]
}
/// Render `report` to a self-contained grid SVG string: a header row of
/// [`COLUMNS`] and one row per workspace with its three metric cells. Each cell
/// `<rect>` carries `class="cell green|red|na"` and the value text; each row also
/// carries the workspace name. Pure (no egui / no warehouse) — feed a report, get
/// the SVG, assert the cells.
pub fn render_metrics_svg(report: &MegaGateReport) -> String {
let label_w = 130.0f64;
let col_w = 120.0f64;
let row_h = 30.0f64;
let header_h = 28.0f64;
let margin = 14.0f64;
let title_h = 26.0f64;
let ncols = COLUMNS.len();
let nrows = report.workspaces.len();
let width = margin * 2.0 + label_w + col_w * ncols as f64;
let height = margin * 2.0 + title_h + header_h + row_h * nrows.max(1) as f64;
let mut s = String::new();
let _ = write!(
s,
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width:.0}\" height=\"{height:.0}\" \
viewBox=\"0 0 {width:.0} {height:.0}\" font-family=\"sans-serif\" font-size=\"11\">\n"
);
// Style: the green/red/na cell classes (the assertable verdict surface).
s.push_str(
"<style>\
.cell.green{fill:#e6f3ea;stroke:#3c8a52;}\
.cell.red{fill:#f7e6e6;stroke:#b3434a;}\
.cell.na{fill:#eeeeee;stroke:#9a9aa2;}\
</style>\n",
);
// Title.
let _ = write!(
s,
"<text x=\"{:.0}\" y=\"{:.0}\" font-size=\"13\" font-weight=\"bold\">coverage metrics — {}</text>\n",
margin,
margin + 14.0,
xml_escape(&report.summary()),
);
// Header row (column labels).
let header_y = margin + title_h;
for (ci, col) in COLUMNS.iter().enumerate() {
let x = margin + label_w + ci as f64 * col_w;
let _ = write!(
s,
"<text x=\"{:.0}\" y=\"{:.0}\" font-weight=\"bold\">{}</text>\n",
x + col_w / 2.0 - 4.0,
header_y + header_h / 2.0 + 4.0,
xml_escape(col),
);
}
// One row per workspace.
for (ri, wg) in report.workspaces.iter().enumerate() {
let row_y = header_y + header_h + ri as f64 * row_h;
let _ = write!(
s,
"<text x=\"{:.0}\" y=\"{:.0}\" font-weight=\"bold\">{}</text>\n",
margin,
row_y + row_h / 2.0 + 4.0,
xml_escape(&wg.workspace),
);
for (ci, cell) in cells_for(wg).into_iter().enumerate() {
let x = margin + label_w + ci as f64 * col_w;
let _ = write!(
s,
"<rect class=\"cell {}\" x=\"{x:.0}\" y=\"{row_y:.0}\" width=\"{:.0}\" height=\"{:.0}\" \
rx=\"3\" stroke-width=\"0.8\"/>\n",
cell.class,
col_w - 8.0,
row_h - 6.0,
);
let _ = write!(
s,
"<text x=\"{:.0}\" y=\"{:.0}\">{}</text>\n",
x + 8.0,
row_y + row_h / 2.0 + 3.0,
xml_escape(&cell.text),
);
}
}
s.push_str("</svg>\n");
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::autonom::megagate::WorkspaceGate;
use nornir_testmatrix::coverage::GateReport;
use nornir_testmatrix::discover::{cli_commands, Surface, SurfaceNode};
use std::collections::BTreeSet;
fn surface_ab() -> Surface {
let mut s = Surface::new();
s.extend(cli_commands(["a", "b"]));
s
}
fn green_report(ws: &str) -> GateReport {
let surface = surface_ab();
let covered: BTreeSet<String> = surface.nodes.iter().map(SurfaceNode::key_str).collect();
GateReport::compute("r", ws, &surface, &covered, &Default::default())
}
fn red_report(ws: &str) -> GateReport {
let surface = surface_ab();
let covered: BTreeSet<String> = ["cli_command:a@na".to_string()].into_iter().collect();
GateReport::compute("r", ws, &surface, &covered, &Default::default())
}
/// Inject a known two-workspace report and assert the rendered grid: a row per
/// workspace, three cells each, with the exact green/red/na classes + values.
#[test]
fn renders_per_workspace_rows_and_cells_with_classes() {
let report = MegaGateReport {
run_id: "r".into(),
workspaces: vec![
// alpha: gate green (2/2 covered), utfallsrum 2/2, reachable 1/1, bencher 1/1.
WorkspaceGate::from_report("alpha", green_report("alpha"))
.with_utfallsrum(2, 2)
.with_reachable(1, 1)
.with_bencher(1, 1),
// beta: gate red (1/2, b uncovered), no utfallsrum, reachable 1/2 (red),
// bencher 0/1 (a hot-path with no bencher → red).
WorkspaceGate::from_report("beta", red_report("beta"))
.with_reachable(1, 2)
.with_bencher(0, 1),
],
};
let svg = render_metrics_svg(&report);
assert!(svg.starts_with("<svg"), "an svg root: {}", &svg[..40.min(svg.len())]);
assert!(svg.ends_with("</svg>\n"));
// Both workspace rows are present.
assert!(svg.contains(">alpha<"), "alpha row label");
assert!(svg.contains(">beta<"), "beta row label");
// The three column headers.
for col in COLUMNS {
assert!(svg.contains(&format!(">{col}<")), "header `{col}` present");
}
// alpha: ran/utfallsrum/reachable/bencher all green (4 cells).
let alpha_greens = svg.matches("class=\"cell green\"").count();
let reds = svg.matches("class=\"cell red\"").count();
let nas = svg.matches("class=\"cell na\"").count();
// alpha: 4 green. beta: ran(red) + reachable(red) + bencher(red) = 3 red,
// and 1 na (utfallsrum unmeasured).
assert_eq!(alpha_greens, 4, "alpha's four cells are green");
assert_eq!(reds, 3, "beta's ran + reachable + bencher cells are red");
assert_eq!(nas, 1, "beta's unmeasured utfallsrum is na");
// Values: alpha ran shows 2/2, beta ran shows 1/2, beta reachable 1/2,
// beta bencher 0/1 (the no-bencher hot-path).
assert!(svg.contains(">2/2<"), "alpha covered/total");
assert!(svg.contains(">1/2<"), "beta covered/total + reachable shortfall");
assert!(svg.contains(">0/1<"), "beta bencher shortfall (a hot-path with no bencher)");
assert!(svg.contains(">—<"), "beta utfallsrum is the NA dash");
}
/// A report with NO measured rollups renders every secondary cell as NA, and
/// the ran cell reflects the gate verdict.
#[test]
fn unmeasured_rollups_render_na() {
let report = MegaGateReport {
run_id: "r".into(),
workspaces: vec![WorkspaceGate::from_report("solo", green_report("solo"))],
};
let svg = render_metrics_svg(&report);
// ran green; utfallsrum + reachable + bencher all na → 3 na cells, 1 green, 0 red.
assert_eq!(svg.matches("class=\"cell green\"").count(), 1);
assert_eq!(svg.matches("class=\"cell na\"").count(), 3);
assert_eq!(svg.matches("class=\"cell red\"").count(), 0);
}
}