use std::fmt::Write;
use crate::model::capability::CapabilityMatrix;
use crate::party::Results;
use crate::verdict::Evidence;
use crate::vocab::Tier;
const STYLE: &str = "<style>\n\
text { fill: #52514e; font: 12px -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif; }\n\
.title { fill: #0b0b0b; font-weight: 600; }\n\
.muted { fill: #8a8880; font-size: 11px; }\n\
.grid { stroke: #e4e2dd; stroke-width: 1; }\n\
.cell-label { fill: #ffffff; font-size: 11px; }\n\
.cell-label-dim { fill: #0b0b0b; font-size: 11px; }\n\
.ev-passed { fill: #0072b2; }\n\
.ev-failed { fill: #d55e00; }\n\
.ev-inconclusive { fill: #e69f00; }\n\
.ev-not-evidenced { fill: #cbc9c2; }\n\
.ev-not-claimed { fill: #edece8; }\n\
.cell-required { stroke: #52514e; stroke-width: 1.2; }\n\
.cell-optional { stroke: #cbc9c2; stroke-width: 1; stroke-dasharray: 3 2; }\n\
.bar-passed { fill: #0072b2; }\n\
.bar-failed { fill: #d55e00; }\n\
.bar-errored { fill: #e69f00; }\n\
.bar-na { fill: #cbc9c2; }\n\
.seg-label { fill: #ffffff; font-size: 10.5px; }\n\
.seg-label-dim { fill: #0b0b0b; font-size: 10.5px; }\n\
@media (prefers-color-scheme: dark) {\n\
text { fill: #c3c2b7; }\n\
.title { fill: #ffffff; }\n\
.muted { fill: #8f8e85; }\n\
.grid { stroke: #3a3a38; }\n\
.cell-label { fill: #ffffff; }\n\
.cell-label-dim { fill: #e8e6e0; }\n\
.ev-passed { fill: #3987e5; }\n\
.ev-failed { fill: #e5484d; }\n\
.ev-inconclusive { fill: #f5a623; }\n\
.ev-not-evidenced { fill: #4a4a47; }\n\
.ev-not-claimed { fill: #2b2b29; }\n\
.cell-required { stroke: #c3c2b7; }\n\
.cell-optional { stroke: #4a4a47; }\n\
.bar-passed { fill: #3987e5; }\n\
.bar-failed { fill: #e5484d; }\n\
.bar-errored { fill: #d9a514; }\n\
.bar-na { fill: #4a4a47; }\n\
.seg-label-dim { fill: #e8e6e0; }\n\
}\n\
</style>\n";
fn svg_open(out: &mut String, width: f64, height: f64) {
let _ = writeln!(
out,
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" \
width=\"{width}\" height=\"{height}\" role=\"img\">\n{STYLE}"
);
}
type TierBand<'a> = (Tier, Vec<(&'a str, bool, Evidence)>);
fn evidence_encoding(evidence: Evidence) -> (&'static str, &'static str, &'static str) {
match evidence {
Evidence::Passed => ("ev-passed", "✓", "passed"),
Evidence::Failed => ("ev-failed", "✕", "FAILED"),
Evidence::Inconclusive => ("ev-inconclusive", "?", "INCONCLUSIVE"),
Evidence::NotEvidenced => ("ev-not-evidenced", "○", "not evidenced"),
Evidence::NotClaimed => ("ev-not-claimed", "–", "not claimed"),
}
}
fn label_class(evidence: Evidence) -> &'static str {
match evidence {
Evidence::Passed | Evidence::Failed => "cell-label",
_ => "cell-label-dim",
}
}
fn tier_title(tier: Tier) -> &'static str {
match tier {
Tier::Core => "CORE",
Tier::Standard => "STANDARD",
Tier::Options => "OPTIONS",
Tier::SecBasic => "SEC-BASIC",
Tier::EnterpriseD => "ENTERPRISE-D",
Tier::EnterpriseM => "ENTERPRISE-M",
Tier::EnterpriseX => "ENTERPRISE-X",
}
}
const TIER_ORDER: [Tier; 4] = [Tier::Core, Tier::Standard, Tier::Options, Tier::SecBasic];
const CELL_W: f64 = 190.0;
const CELL_H: f64 = 26.0;
const CELL_GAP: f64 = 6.0;
const GRID_COLS: usize = 5;
const MARGIN: f64 = 24.0;
#[must_use]
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "grid counts/cell coordinates are far below 2^52"
)]
pub fn heat_grid_svg(
sut_label: &str,
matrix: &CapabilityMatrix,
capabilities: &[(String, Evidence)],
) -> String {
let evidence_of = |name: &str| {
capabilities
.iter()
.find(|(n, _)| n == name)
.map_or(Evidence::NotEvidenced, |(_, e)| *e)
};
let mut bands: Vec<TierBand<'_>> = Vec::new();
for tier in TIER_ORDER {
let entries: Vec<(&str, bool, Evidence)> = matrix
.entries()
.iter()
.filter(|(_, e)| e.tier == tier)
.map(|(name, e)| (name.as_str(), e.required, evidence_of(name.as_str())))
.collect();
if !entries.is_empty() {
bands.push((tier, entries));
}
}
let width = MARGIN * 2.0 + GRID_COLS as f64 * (CELL_W + CELL_GAP) - CELL_GAP;
let mut height = 64.0; for (_, entries) in &bands {
let rows = entries.len().div_ceil(GRID_COLS);
height += 26.0 + rows as f64 * (CELL_H + CELL_GAP) + 10.0;
}
height += 8.0;
let mut out = String::new();
svg_open(&mut out, width, height);
let _ = writeln!(
out,
"<text x=\"{MARGIN}\" y=\"28\" class=\"title\">Capability conformance — {}</text>",
xml_escape(sut_label)
);
let mut lx = MARGIN;
for evidence in [
Evidence::Passed,
Evidence::Failed,
Evidence::Inconclusive,
Evidence::NotEvidenced,
Evidence::NotClaimed,
] {
let (class, glyph, label) = evidence_encoding(evidence);
let _ = write!(
out,
"<rect x=\"{lx}\" y=\"38\" width=\"14\" height=\"14\" rx=\"3\" class=\"{class}\"/>\
<text x=\"{gx}\" y=\"49\" class=\"{lc}\" text-anchor=\"middle\" font-size=\"10\">{glyph}</text>\
<text x=\"{tx}\" y=\"49\" class=\"muted\">{label}</text>",
gx = lx + 7.0,
lc = label_class(evidence),
tx = lx + 18.0,
);
lx = ((lx + 20.0 + 7.2 * label.chars().count() as f64 + 16.0) * 10.0).round() / 10.0;
}
let _ = writeln!(
out,
"<text x=\"{x}\" y=\"49\" class=\"muted\" text-anchor=\"end\">solid border = required in tier · dashed = optional</text>",
x = width - MARGIN,
);
let mut y = 64.0;
for (tier, entries) in &bands {
y += 18.0;
let _ = writeln!(
out,
"<text x=\"{MARGIN}\" y=\"{y}\" class=\"title\" font-size=\"12\">{}</text>",
tier_title(*tier),
);
y += 8.0;
for (i, (name, required, evidence)) in entries.iter().enumerate() {
let col = i % GRID_COLS;
#[expect(
clippy::integer_division,
reason = "grid row = index / columns: exact integer arithmetic is the intent"
)]
let row = i / GRID_COLS;
let x = MARGIN + col as f64 * (CELL_W + CELL_GAP);
let cy = y + row as f64 * (CELL_H + CELL_GAP);
let (class, glyph, _) = evidence_encoding(*evidence);
let border = if *required {
"cell-required"
} else {
"cell-optional"
};
let _ = writeln!(
out,
"<rect x=\"{x}\" y=\"{cy}\" width=\"{CELL_W}\" height=\"{CELL_H}\" rx=\"5\" \
class=\"{class} {border}\"/>\
<text x=\"{gx}\" y=\"{ty}\" class=\"{lc}\" text-anchor=\"middle\">{glyph}</text>\
<text x=\"{nx}\" y=\"{ty}\" class=\"{lc}{sm}\">{name}</text>",
gx = x + 13.0,
nx = x + 26.0,
ty = cy + 17.0,
lc = label_class(*evidence),
sm = if name.chars().count() > 24 { " sm" } else { "" },
);
}
let rows = entries.len().div_ceil(GRID_COLS);
y += rows as f64 * (CELL_H + CELL_GAP) + 10.0;
}
out.push_str("</svg>\n");
out
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct BandCounts {
pub passed: u64,
pub failed: u64,
pub errored: u64,
pub cited_na: u64,
}
impl BandCounts {
#[must_use]
pub fn total(self) -> u64 {
self.passed + self.failed + self.errored + self.cited_na
}
#[must_use]
pub fn is_empty(self) -> bool {
self.total() == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChapterRow {
pub chapter: &'static str,
pub total: BandCounts,
pub bands: Vec<(&'static str, BandCounts)>,
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
pub const TAXONOMY: &[(&str, &[&str])] = &[
(
"EHR",
&[
"EHR resource",
"EHR_STATUS",
"COMPOSITION",
"DIRECTORY",
"CONTRIBUTION",
"Item tags",
"Revision history",
],
),
(
"Definitions",
&["ADL 1.4 templates", "ADL 2 artefacts", "Stored queries"],
),
("Query", &["Ad-hoc AQL", "Stored query execution"]),
(
"Demographic",
&["Parties", "Party relationships", "Versioned party"],
),
("Messaging", &["EHR Extract", "TDD"]),
("Admin", &["Admin service", "Archive", "Dump & load"]),
("System", &["Conformance manifest"]),
(
"Content validation",
&[
"Data types",
"Interval data types",
"Structure & cardinality",
],
),
(
"Simplified formats",
&[
"FLAT & STRUCTURED",
"Web Template",
"Path mapping",
"Scope & legacy media",
],
),
(
"Security & privacy",
&[
"Authenticated access",
"Authorization separation",
"Audit accountability",
"Anonymous EHRs",
"EHR/demographic separation",
],
),
("Signing", &["Version signing"]),
("SMART App Launch", &["Discovery", "Resource scopes"]),
("Performance", &["Hospital simulation"]),
];
#[derive(Debug, thiserror::Error)]
pub enum TaxonomyError {
#[error(
"conformance-assets: case id `{0}` maps to no (chapter, band) pair — \
extend TAXONOMY + band_of in src/conf_assets.rs \
(the taxonomy is total by contract; there is no `Other` bucket)"
)]
UnmappedCase(String),
#[error(
"conformance-assets: case id `{case}` maps to band `{chapter}` / `{band}`, \
which TAXONOMY does not declare — the mapping and the table have drifted"
)]
UndeclaredBand {
case: String,
chapter: &'static str,
band: &'static str,
},
}
fn interface_band(interface: &str, operation: &str) -> Option<(&'static str, &'static str)> {
let band = match interface {
"I_EHR_SERVICE" => ("EHR", "EHR resource"),
"I_EHR_STATUS" => ("EHR", "EHR_STATUS"),
"I_EHR_COMPOSITION" => ("EHR", "COMPOSITION"),
"I_EHR_DIRECTORY" => ("EHR", "DIRECTORY"),
"I_EHR_CONTRIBUTION" => ("EHR", "CONTRIBUTION"),
"I_ITS_REST_ITEM_TAGS" => ("EHR", "Item tags"),
"I_ITS_REST_REVISION_HISTORY" => ("EHR", "Revision history"),
"I_DEFINITION_ADL14" => ("Definitions", "ADL 1.4 templates"),
"I_DEFINITION_ADL2" => ("Definitions", "ADL 2 artefacts"),
"I_DEFINITION_QUERY" => ("Definitions", "Stored queries"),
"I_QUERY_SERVICE" => match operation {
"execute_stored_query" => ("Query", "Stored query execution"),
"execute_ad_hoc_query" | "smoke_test" => ("Query", "Ad-hoc AQL"),
_ => return None,
},
"I_DEMOGRAPHIC_SERVICE" => {
if operation.starts_with("versioned_party") {
("Demographic", "Versioned party")
} else if operation.contains("party_relationship") {
("Demographic", "Party relationships")
} else if operation.contains("party") {
("Demographic", "Parties")
} else {
return None;
}
}
"I_ITS_REST_VERSIONED_PARTY" => ("Demographic", "Versioned party"),
"I_EHR_EXTRACT_SERVICE" => ("Messaging", "EHR Extract"),
"I_TDD_SERVICE" => ("Messaging", "TDD"),
"I_ADMIN_SERVICE" => ("Admin", "Admin service"),
"I_ADMIN_ARCHIVE" => ("Admin", "Archive"),
"I_ADMIN_DUMP_LOAD" => ("Admin", "Dump & load"),
"I_ITS_REST_SYSTEM" => ("System", "Conformance manifest"),
_ => return None,
};
Some(band)
}
fn family_band(family: &str, topic: &str) -> Option<(&'static str, &'static str)> {
let band = match family {
"CONT" => {
if topic.starts_with("DV_INTERVAL_") {
("Content validation", "Interval data types")
} else if topic.starts_with("DV_") {
("Content validation", "Data types")
} else {
match topic {
"CLUSTER" | "COMP" | "COMPOSITION" | "ELEMENT" | "EVENT" | "HIST"
| "HISTORY" | "ITEM" | "ITEM_LIST" | "ITEM_STR" | "ITEM_STRUCTURE"
| "ITEM_TABLE" | "ITEM_TREE" | "OBS" | "OBSERVATION" => {
("Content validation", "Structure & cardinality")
}
_ => return None,
}
}
}
"SF" => match topic {
"FLAT" | "STRUCT" | "STRUCTURED" | "CONTRIB" | "EXAMPLE" => {
("Simplified formats", "FLAT & STRUCTURED")
}
"WT" | "NODEID" | "FIELDID" => ("Simplified formats", "Web Template"),
"MAP" | "INDEX" | "RMATTR" | "LEVELS" | "RAW" | "CTX" => {
("Simplified formats", "Path mapping")
}
"SCOPE" | "DEPRECATED" | "LEGACY" => ("Simplified formats", "Scope & legacy media"),
_ => return None,
},
"SEC" => match topic {
"AUTHENTICATED_ACCESS" => ("Security & privacy", "Authenticated access"),
"AUTHORIZATION_SEPARATION" => ("Security & privacy", "Authorization separation"),
"AUDIT_ACCOUNTABILITY" => ("Security & privacy", "Audit accountability"),
"ANONYMOUS_EHRS" => ("Security & privacy", "Anonymous EHRs"),
"EHR_DEMOGRAPHIC_SEPARATION" => ("Security & privacy", "EHR/demographic separation"),
_ => return None,
},
"SIG" => match topic {
"VERSION" => ("Signing", "Version signing"),
_ => return None,
},
"SMART" => match topic {
"DISCOVERY" => ("SMART App Launch", "Discovery"),
"RESOURCE_SCOPES" => ("SMART App Launch", "Resource scopes"),
_ => return None,
},
"PERF" => match topic {
"hospital_sim" => ("Performance", "Hospital simulation"),
_ => return None,
},
_ => return None,
};
Some(band)
}
pub fn band_of(case_id: &str) -> Result<(&'static str, &'static str), TaxonomyError> {
let band = if let Some((interface, rest)) = case_id.split_once('.') {
let operation = rest.split('-').next().unwrap_or(rest);
interface_band(interface, operation)
} else if let Some((family, rest)) = case_id.split_once('-') {
let topic = rest.split('-').next().unwrap_or(rest);
family_band(family, topic)
} else {
None
};
band.ok_or_else(|| TaxonomyError::UnmappedCase(case_id.to_owned()))
}
pub fn chapter_counts(results: &Results) -> Result<Vec<ChapterRow>, TaxonomyError> {
let mut rows: Vec<ChapterRow> = TAXONOMY
.iter()
.map(|(chapter, bands)| ChapterRow {
chapter,
total: BandCounts::default(),
bands: bands
.iter()
.map(|band| (*band, BandCounts::default()))
.collect(),
})
.collect();
for outcome in &results.outcomes {
let (chapter, band) = band_of(outcome.case.as_str())?;
let entry = rows
.iter_mut()
.find(|row| row.chapter == chapter)
.and_then(|row| {
row.bands
.iter_mut()
.find(|(name, _)| *name == band)
.map(|(_, counts)| counts)
})
.ok_or_else(|| TaxonomyError::UndeclaredBand {
case: outcome.case.as_str().to_owned(),
chapter,
band,
})?;
match outcome.status {
crate::party::OutcomeStatus::Passed => entry.passed += 1,
crate::party::OutcomeStatus::Failed => entry.failed += 1,
crate::party::OutcomeStatus::Errored => entry.errored += 1,
crate::party::OutcomeStatus::NotApplicable | crate::party::OutcomeStatus::Skipped => {
entry.cited_na += 1;
}
}
}
for row in &mut rows {
let mut total = BandCounts::default();
for (_, counts) in &row.bands {
total.passed += counts.passed;
total.failed += counts.failed;
total.errored += counts.errored;
total.cited_na += counts.cited_na;
}
row.total = total;
}
Ok(rows)
}
const BARS_STYLE: &str = "<style>\n\
.chap-row { fill: #f1efe9; }\n\
.chap-name { fill: #0b0b0b; font-size: 12.5px; font-weight: 600; }\n\
.band-label { fill: #52514e; font-size: 11px; }\n\
.band-empty { fill: #a5a39b; font-size: 10.5px; font-style: italic; }\n\
.count { font-size: 10.5px; }\n\
.count-strong { font-weight: 600; }\n\
.count-passed { fill: #00639b; }\n\
.count-failed { fill: #c0530b; font-weight: 600; }\n\
.count-errored { fill: #9a7100; }\n\
.count-na { fill: #7d7b74; }\n\
.na-line { stroke: #9d9b93; stroke-width: 2; }\n\
@media (prefers-color-scheme: dark) {\n\
.chap-row { fill: #2b2b29; }\n\
.chap-name { fill: #ffffff; }\n\
.band-label { fill: #c3c2b7; }\n\
.band-empty { fill: #6f6e68; }\n\
.count-passed { fill: #4f97ee; }\n\
.count-failed { fill: #f2696d; }\n\
.count-errored { fill: #d9a514; }\n\
.count-na { fill: #8f8e85; }\n\
.na-line { stroke: #6d6d69; }\n\
}\n\
</style>\n";
const NA_DEFS: &str = "<defs>\n\
<pattern id=\"na-hatch\" width=\"6\" height=\"6\" patternUnits=\"userSpaceOnUse\" \
patternTransform=\"rotate(45)\">\n\
<rect width=\"6\" height=\"6\" class=\"bar-na\"/>\n\
<line x1=\"3\" y1=\"0\" x2=\"3\" y2=\"6\" class=\"na-line\"/>\n\
</pattern>\n\
</defs>\n";
const LABEL_W: f64 = 206.0;
const BAR_MAX_W: f64 = 470.0;
const SLOT_W: f64 = 42.0;
const CHAP_H: f64 = 24.0;
const BAND_H: f64 = 18.0;
const BAND_BAR_H: f64 = 12.0;
const CHAP_GAP: f64 = 8.0;
const BARS_HEAD_H: f64 = 66.0;
const BAR_X: f64 = MARGIN + LABEL_W;
const COUNTS_X: f64 = BAR_X + BAR_MAX_W + 16.0;
const BARS_W: f64 = COUNTS_X + 4.0 * SLOT_W + MARGIN;
const OUTCOMES: [(&str, &str, &str, &str); 4] = [
("bar-passed", "count-passed", "✓", "passed"),
("bar-failed", "count-failed", "✕", "FAILED"),
("bar-errored", "count-errored", "?", "errored"),
("bar-na", "count-na", "○", "cited N/A (not executed)"),
];
fn outcome_values(counts: BandCounts) -> [u64; 4] {
[
counts.passed,
counts.failed,
counts.errored,
counts.cited_na,
]
}
fn write_count_strip(out: &mut String, baseline: f64, counts: BandCounts, strong: bool) {
let weight = if strong { " count-strong" } else { "" };
for (slot, (value, (_, text_class, glyph, _))) in
outcome_values(counts).into_iter().zip(OUTCOMES).enumerate()
{
if value == 0 {
continue;
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "four slots"
)]
let slot_end = COUNTS_X + (slot as f64 + 1.0) * SLOT_W - 6.0;
let _ = write!(
out,
"<text x=\"{slot_end}\" y=\"{baseline}\" class=\"count {text_class}{weight}\" \
text-anchor=\"end\">{glyph} {value}</text>"
);
}
}
fn cases_phrase(total: u64) -> String {
match total {
0 => "no cases".to_owned(),
1 => "1 case".to_owned(),
n => format!("{n} cases"),
}
}
#[must_use]
#[expect(
clippy::as_conversions,
clippy::too_many_lines,
clippy::cast_precision_loss,
reason = "one linear chart emitter; counts/rows << 2^52"
)]
pub fn chapter_bars_svg(sut_label: &str, chapters: &[ChapterRow]) -> String {
let max_band: u64 = chapters
.iter()
.flat_map(|row| row.bands.iter().map(|(_, counts)| counts.total()))
.max()
.unwrap_or(1)
.max(1);
let scale = BAR_MAX_W / max_band as f64;
let band_rows: usize = chapters.iter().map(|row| row.bands.len()).sum();
let height =
BARS_HEAD_H + chapters.len() as f64 * (CHAP_H + CHAP_GAP) + band_rows as f64 * BAND_H + 8.0;
let mut out = String::new();
svg_open(&mut out, BARS_W, height);
out.push_str(BARS_STYLE);
out.push_str(NA_DEFS);
let _ = writeln!(
out,
"<text x=\"{MARGIN}\" y=\"28\" class=\"title\">Schedule outcomes by chapter — {}</text>",
xml_escape(sut_label)
);
let mut lx = MARGIN;
for (fill_class, _, glyph, label) in OUTCOMES {
let (fill, glyph_class) = match fill_class {
"bar-na" => ("fill=\"url(#na-hatch)\"".to_owned(), "seg-label-dim"),
"bar-errored" => (format!("class=\"{fill_class}\""), "seg-label-dim"),
_ => (format!("class=\"{fill_class}\""), "seg-label"),
};
let _ = write!(
out,
"<rect x=\"{lx}\" y=\"38\" width=\"14\" height=\"14\" rx=\"3\" {fill}/>\
<text x=\"{gx}\" y=\"49\" class=\"{glyph_class}\" \
text-anchor=\"middle\">{glyph}</text>\
<text x=\"{tx}\" y=\"49\" class=\"muted\">{label}</text>",
gx = lx + 7.0,
tx = lx + 20.0,
);
lx = ((lx + 20.0 + 7.2 * label.chars().count() as f64 + 16.0) * 10.0).round() / 10.0;
}
let _ = writeln!(
out,
"<text x=\"{x}\" y=\"49\" class=\"muted\" text-anchor=\"end\">\
this chart's scale: {max_band} cases = full bar</text>",
x = BARS_W - MARGIN,
);
let mut y = BARS_HEAD_H;
for row in chapters {
let _ = writeln!(
out,
"<rect x=\"{MARGIN}\" y=\"{y}\" width=\"{w}\" height=\"20\" rx=\"4\" \
class=\"chap-row\"/>\
<text x=\"{nx}\" y=\"{ty}\" class=\"chap-name\">{name}</text>\
<text x=\"{tx}\" y=\"{ty}\" class=\"muted\" text-anchor=\"end\">{phrase}</text>",
w = BARS_W - 2.0 * MARGIN,
nx = MARGIN + 10.0,
tx = BAR_X + BAR_MAX_W,
ty = y + 14.5,
name = xml_escape(row.chapter),
phrase = cases_phrase(row.total.total()),
);
write_count_strip(&mut out, y + 14.5, row.total, true);
out.push('\n');
y += CHAP_H;
for (band, counts) in &row.bands {
let baseline = y + 12.5;
let _ = write!(
out,
"<text x=\"{label_x}\" y=\"{baseline}\" class=\"band-label\" \
text-anchor=\"end\">{name}</text>",
label_x = BAR_X - 10.0,
name = xml_escape(band),
);
if counts.is_empty() {
let _ = writeln!(
out,
"<text x=\"{BAR_X}\" y=\"{baseline}\" class=\"band-empty\">no cases</text>"
);
} else {
let mut x = BAR_X;
for (value, (fill_class, _, _, _)) in
outcome_values(*counts).into_iter().zip(OUTCOMES)
{
if value == 0 {
continue;
}
let w = value as f64 * scale;
let fill = if fill_class == "bar-na" {
"fill=\"url(#na-hatch)\"".to_owned()
} else {
format!("class=\"{fill_class}\"")
};
let _ = write!(
out,
"<rect x=\"{x}\" y=\"{by}\" width=\"{w}\" height=\"{BAND_BAR_H}\" {fill}/>",
by = y + (BAND_H - BAND_BAR_H) / 2.0,
);
x += w;
}
write_count_strip(&mut out, baseline, *counts, false);
out.push('\n');
}
y += BAND_H;
}
y += CHAP_GAP;
}
out.push_str("</svg>\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::party::OutcomeStatus;
fn matrix() -> CapabilityMatrix {
serde_saphyr::from_str(
"EhrOperations: { family: Platform, tier: CORE, required: true, source: s }\n\
AqlBasic: { family: Platform, tier: STANDARD, required: true, source: s }\n\
EhrExtract: { family: Platform, tier: OPTIONS, required: false, source: s }\n\
AuthenticatedAccess: { family: Security, tier: SEC-BASIC, required: true, source: s }\n",
)
.unwrap()
}
#[test]
fn the_heat_grid_is_deterministic_and_encodes_twice() {
let m = matrix();
let caps = vec![
("EhrOperations".to_owned(), Evidence::Passed),
("AqlBasic".to_owned(), Evidence::Failed),
("AuthenticatedAccess".to_owned(), Evidence::NotEvidenced),
];
let a = heat_grid_svg("FerroEHR 3.7.0", &m, &caps);
let b = heat_grid_svg("FerroEHR 3.7.0", &m, &caps);
assert_eq!(a, b);
assert!(a.contains("ev-passed"));
assert!(a.contains('✓'));
assert!(a.contains("ev-failed"));
assert!(a.contains('✕'));
assert!(a.contains("ev-not-evidenced"));
assert!(a.contains('○'));
for label in ["passed", "FAILED", "INCONCLUSIVE", "not evidenced"] {
assert!(a.contains(label), "legend label {label} missing");
}
assert!(a.contains("ev-inconclusive"));
assert!(a.contains('?'));
for band in ["CORE", "STANDARD", "OPTIONS", "SEC-BASIC"] {
assert!(a.contains(band), "band {band} missing");
}
assert!(a.contains("cell-required"));
assert!(a.contains("cell-optional"));
assert!(a.contains("prefers-color-scheme: dark"));
assert!(!a.contains("Date"));
}
fn crate_dir() -> std::path::PathBuf {
std::path::PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
}
fn committed_case_ids() -> Vec<String> {
fn walk(dir: &std::path::Path, out: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "yaml") {
let text = std::fs::read_to_string(&path).unwrap();
for line in text.lines() {
if let Some(id) = line.strip_prefix("id: ") {
out.push(id.trim().to_owned());
break;
}
}
}
}
}
let mut ids = Vec::new();
walk(&crate_dir().join("artifacts/schedule"), &mut ids);
assert!(ids.len() > 500, "case-core walk found only {}", ids.len());
ids.sort();
ids
}
const RETIRED_CASE_IDS: &[&str] = &[];
#[test]
fn the_taxonomy_is_total_over_every_committed_case_id() {
let declared: Vec<(&str, &str)> = TAXONOMY
.iter()
.flat_map(|(chapter, bands)| bands.iter().map(move |band| (*chapter, *band)))
.collect();
let mut unmapped: Vec<String> = Vec::new();
for id in committed_case_ids()
.into_iter()
.chain(RETIRED_CASE_IDS.iter().map(|id| (*id).to_owned()))
{
let mapped = band_of(&id).ok();
match mapped {
Some(band) => assert!(
declared.contains(&band),
"case `{id}` maps to undeclared band {band:?}"
),
None => unmapped.push(id),
}
}
assert!(
unmapped.is_empty(),
"these case ids map to no band (the taxonomy must be total — \
there is no `Other` bucket): {unmapped:#?}"
);
}
#[test]
fn the_taxonomy_names_are_unique() {
let mut chapters: Vec<&str> = TAXONOMY.iter().map(|(chapter, _)| *chapter).collect();
let before = chapters.len();
chapters.sort_unstable();
chapters.dedup();
assert_eq!(chapters.len(), before, "duplicate chapter name");
for (chapter, bands) in TAXONOMY {
let mut names: Vec<&str> = bands.to_vec();
let before = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), before, "duplicate band name in {chapter}");
}
}
#[test]
fn the_two_levels_come_from_the_case_id() {
assert_eq!(
band_of("I_EHR_COMPOSITION.create_composition-event").unwrap(),
("EHR", "COMPOSITION")
);
assert_eq!(
band_of("I_EHR_SERVICE.create_ehr").unwrap(),
("EHR", "EHR resource")
);
assert_eq!(
band_of("I_ITS_REST_ITEM_TAGS.upsert_tags-x").unwrap(),
("EHR", "Item tags")
);
assert_eq!(
band_of("I_ITS_REST_REVISION_HISTORY.get-x").unwrap(),
("EHR", "Revision history")
);
assert_eq!(
band_of("I_DEMOGRAPHIC_SERVICE.create_party-x").unwrap(),
("Demographic", "Parties")
);
assert_eq!(
band_of("I_DEMOGRAPHIC_SERVICE.get_party_relationship-x").unwrap(),
("Demographic", "Party relationships")
);
assert_eq!(
band_of("I_DEMOGRAPHIC_SERVICE.versioned_party_version_read").unwrap(),
("Demographic", "Versioned party")
);
assert_eq!(
band_of("I_ITS_REST_VERSIONED_PARTY.get-x").unwrap(),
("Demographic", "Versioned party")
);
assert_eq!(
band_of("I_QUERY_SERVICE.execute_stored_query-x").unwrap(),
("Query", "Stored query execution")
);
assert_eq!(
band_of("I_QUERY_SERVICE.smoke_test").unwrap(),
("Query", "Ad-hoc AQL")
);
assert_eq!(
band_of("I_ITS_REST_SYSTEM.get_conformance-options").unwrap(),
("System", "Conformance manifest")
);
assert_eq!(
band_of("I_EHR_EXTRACT_SERVICE.request-x").unwrap(),
("Messaging", "EHR Extract")
);
assert_eq!(
band_of("I_TDD_SERVICE.convert-x").unwrap(),
("Messaging", "TDD")
);
assert_eq!(
band_of("I_ADMIN_DUMP_LOAD.export_ehrs-export_all").unwrap(),
("Admin", "Dump & load")
);
assert_eq!(
band_of("CONT-DV_TEXT-validate_open").unwrap(),
("Content validation", "Data types")
);
assert_eq!(
band_of("CONT-DV_INTERVAL_DV_COUNT-validate_open").unwrap(),
("Content validation", "Interval data types")
);
assert_eq!(
band_of("SF-FLAT-commit_roundtrip_ctx_defaults").unwrap(),
("Simplified formats", "FLAT & STRUCTURED")
);
assert_eq!(
band_of("SF-WT-web_template_get").unwrap(),
("Simplified formats", "Web Template")
);
assert_eq!(
band_of("SEC-AUDIT_ACCOUNTABILITY-server_set_commit_audit").unwrap(),
("Security & privacy", "Audit accountability")
);
assert_eq!(
band_of("SIG-VERSION-verifiable").unwrap(),
("Signing", "Version signing")
);
assert_eq!(
band_of("SMART-DISCOVERY-document_shape").unwrap(),
("SMART App Launch", "Discovery")
);
assert_eq!(
band_of("PERF-hospital_sim-class_POC").unwrap(),
("Performance", "Hospital simulation")
);
}
#[test]
fn every_structural_content_topic_shares_one_band() {
for id in [
"CONT-OBS-state_ex_opt-protocol_ex_opt",
"CONT-OBSERVATION-state_protocol_existence",
"CONT-ITEM_STR-type_any",
"CONT-ITEM_STRUCTURE-type_narrowing",
"CONT-COMP-content_card_any-context_any",
"CONT-COMPOSITION-context_existence",
"CONT-HIST-events_card_any-summary_ex_opt",
"CONT-HISTORY-events_cardinality_count6",
"CONT-EVENT-type_any",
"CONT-ITEM_TREE-items_cardinality",
"CONT-ITEM_LIST-items_cardinality",
"CONT-ITEM_TABLE-rows_cardinality",
"CONT-CLUSTER-items_cardinality",
"CONT-ELEMENT-value_null_flavour_existence",
"CONT-ITEM-type_cluster",
] {
assert_eq!(
band_of(id).unwrap(),
("Content validation", "Structure & cardinality"),
"{id}"
);
}
}
#[test]
fn an_unmapped_case_id_fails_the_render() {
for id in [
"SOMETHING-else-entirely",
"I_NOT_AN_INTERFACE.do_something",
"CONT-NOT_A_CONSTRUCT-validate",
"I_DEMOGRAPHIC_SERVICE.list_widgets",
"bare_id",
] {
let err = band_of(id).unwrap_err();
assert!(
matches!(&err, TaxonomyError::UnmappedCase(seen) if seen == id),
"{id} produced {err:?}"
);
assert!(err.to_string().contains(id));
}
let results = results_with(&[("SOMETHING-else-entirely", OutcomeStatus::Passed)]);
let err = chapter_counts(&results).unwrap_err();
assert!(matches!(err, TaxonomyError::UnmappedCase(_)));
}
fn results_with(outcomes: &[(&str, OutcomeStatus)]) -> Results {
Results {
sut: crate::party::Sut {
name: "sut".to_owned(),
version: "0".to_owned(),
},
runner: crate::party::Runner {
name: "veredictum".to_owned(),
version: "0".to_owned(),
verification_pack_status: crate::party::VerificationPackStatus::Passed,
},
schedule_release: "test".to_owned(),
tech_profile: crate::party::TechProfile {
its: crate::vocab::ItsName::ItsRest,
formats: Vec::new(),
},
ixit_digest: "test".to_owned(),
restapi_specs_version: None,
outcomes: outcomes
.iter()
.map(|(case, status)| crate::party::OutcomeRecord {
case: crate::ids::CaseId::parse(case).unwrap(),
format: None,
status: *status,
rows_driven: 1,
rows_total: 1,
failing_step: None,
reason: None,
citation: None,
failed_rows: Vec::new(),
})
.collect(),
measurements: Vec::new(),
ambiguity_dispositions: Vec::new(),
}
}
#[test]
fn the_counts_aggregate_per_band_and_roll_up_per_chapter() {
let rows = chapter_counts(&results_with(&[
(
"I_EHR_COMPOSITION.create_composition-a",
OutcomeStatus::Passed,
),
(
"I_EHR_COMPOSITION.create_composition-b",
OutcomeStatus::Passed,
),
(
"I_EHR_COMPOSITION.delete_composition-c",
OutcomeStatus::Failed,
),
("I_EHR_STATUS.get_ehr_status-a", OutcomeStatus::Errored),
(
"I_ITS_REST_ITEM_TAGS.list_tags-a",
OutcomeStatus::NotApplicable,
),
("I_ITS_REST_ITEM_TAGS.list_tags-b", OutcomeStatus::Skipped),
("SIG-VERSION-verifiable", OutcomeStatus::Passed),
]))
.unwrap();
assert_eq!(rows.len(), TAXONOMY.len());
for (row, (chapter, bands)) in rows.iter().zip(TAXONOMY) {
assert_eq!(row.chapter, *chapter);
let names: Vec<&str> = row.bands.iter().map(|(name, _)| *name).collect();
assert_eq!(names, bands.to_vec());
}
let ehr = rows.iter().find(|r| r.chapter == "EHR").unwrap();
let band = |name: &str| {
ehr.bands
.iter()
.find(|(b, _)| *b == name)
.map(|(_, c)| *c)
.unwrap()
};
assert_eq!(
band("COMPOSITION"),
BandCounts {
passed: 2,
failed: 1,
..BandCounts::default()
}
);
assert_eq!(
band("EHR_STATUS"),
BandCounts {
errored: 1,
..BandCounts::default()
}
);
assert_eq!(
band("Item tags"),
BandCounts {
cited_na: 2,
..BandCounts::default()
}
);
assert!(band("DIRECTORY").is_empty());
assert_eq!(
ehr.total,
BandCounts {
passed: 2,
failed: 1,
errored: 1,
cited_na: 2
}
);
assert_eq!(ehr.total.total(), 6);
let signing = rows.iter().find(|r| r.chapter == "Signing").unwrap();
assert_eq!(signing.total.passed, 1);
let perf = rows.iter().find(|r| r.chapter == "Performance").unwrap();
assert!(perf.total.is_empty());
}
#[test]
fn the_chapter_bars_render_two_levels_with_printed_counts() {
let rows = chapter_counts(&results_with(&[
(
"I_EHR_COMPOSITION.create_composition-a",
OutcomeStatus::Passed,
),
(
"I_EHR_COMPOSITION.delete_composition-b",
OutcomeStatus::Failed,
),
("I_EHR_DIRECTORY.get_folder-a", OutcomeStatus::Errored),
(
"SF-FLAT-commit_roundtrip_ctx_defaults",
OutcomeStatus::NotApplicable,
),
]))
.unwrap();
let svg = chapter_bars_svg("FerroEHR 3.7.0", &rows);
assert_eq!(svg, chapter_bars_svg("FerroEHR 3.7.0", &rows));
for (chapter, bands) in TAXONOMY {
assert!(
svg.contains(&xml_escape(chapter)),
"chapter {chapter} missing"
);
for band in *bands {
assert!(svg.contains(&xml_escape(band)), "band {band} missing");
}
}
assert!(svg.contains("chap-row"));
assert!(svg.contains("✓ 1"));
assert!(svg.contains("✕ 1"));
assert!(svg.contains("? 1"));
assert!(svg.contains("○ 1"));
assert!(svg.contains(">3 cases<"));
assert!(svg.contains(">1 case<"));
assert!(svg.contains(">no cases<"));
assert!(svg.contains("id=\"na-hatch\""));
assert!(svg.contains("url(#na-hatch)"));
assert!(svg.contains("prefers-color-scheme: dark"));
assert!(!svg.contains("Date"));
}
#[test]
fn the_chart_keeps_its_embedded_width_and_grows_only_downwards() {
const _: () = assert!(BARS_W.to_bits() == 908.0f64.to_bits());
const _: () = assert!(COUNTS_X + 4.0 * SLOT_W <= BARS_W - MARGIN);
const _: () = assert!(BAR_X + BAR_MAX_W < COUNTS_X);
let rows = chapter_counts(&results_with(&[(
"I_EHR_COMPOSITION.create_composition-a",
OutcomeStatus::Passed,
)]))
.unwrap();
let svg = chapter_bars_svg("FerroEHR 3.7.0", &rows);
let bands: usize = TAXONOMY.iter().map(|(_, b)| b.len()).sum();
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "taxonomy rows << 2^52"
)]
let height =
BARS_HEAD_H + TAXONOMY.len() as f64 * (CHAP_H + CHAP_GAP) + bands as f64 * BAND_H + 8.0;
assert!(
svg.contains(&format!("viewBox=\"0 0 908 {height}\" width=\"908\"")),
"unexpected canvas: {}",
svg.lines().next().unwrap_or_default()
);
}
#[test]
fn svg_text_is_xml_escaped() {
let rows = chapter_counts(&results_with(&[])).unwrap();
let bars = chapter_bars_svg("sut & co", &rows);
for raw in [
"Security & privacy",
"Dump & load",
"FLAT & STRUCTURED",
"Structure & cardinality",
"Scope & legacy media",
"sut & co",
] {
assert!(!bars.contains(raw), "raw ampersand in {raw}");
assert!(bars.contains(&xml_escape(raw)), "missing escaped {raw}");
}
}
#[test]
fn every_tier_carries_a_distinct_chart_title() {
let titles: std::collections::BTreeSet<&str> =
Tier::ALL.iter().map(|t| tier_title(*t)).collect();
assert_eq!(titles.len(), Tier::ALL.len());
assert_eq!(tier_title(Tier::EnterpriseD), "ENTERPRISE-D");
assert_eq!(tier_title(Tier::EnterpriseX), "ENTERPRISE-X");
}
#[test]
fn a_case_id_outside_the_taxonomy_refuses_to_publish() {
for id in [
"I_NO_SUCH_SERVICE.create_thing-main",
"I_QUERY_SERVICE.invent_query-main",
"I_DEMOGRAPHIC_SERVICE.create_folder-main",
"CONT-NOT_A_CONSTRUCT-main",
"SF-NOT_A_TOPIC-main",
"SEC-NOT_A_TOPIC-main",
"SIG-NOT_A_TOPIC-main",
"SMART-NOT_A_TOPIC-main",
"PERF-not_a_workload",
"NO_SEPARATOR",
] {
let error = band_of(id).expect_err("no band claims this id");
assert!(
matches!(&error, TaxonomyError::UnmappedCase(named) if named == id),
"{error}"
);
}
let error = chapter_counts(&results_with(&[(
"I_NO_SUCH_SERVICE.create_thing-main",
OutcomeStatus::Passed,
)]))
.expect_err("the chart refuses an unmapped id");
assert!(
error
.to_string()
.contains("maps to no (chapter, band) pair"),
"{error}"
);
}
}