use teksilo_tokens::TargetDensity;
use super::{AllowedViolation, Owner, TargetFixture, TargetViolation, audit_fixtures};
use crate::styles::Theme;
pub const ALL_DENSITIES: &[TargetDensity] = &[
TargetDensity::Compact,
TargetDensity::Comfortable,
TargetDensity::Touch,
];
const THEME_OWNED_PREFIXES: &[&str] = &["Fluent", "MacOs", "Material3", "M3"];
const ANONYMOUS_THEME: &str = "custom";
const SC_2_5_8_EXCEPTIONS: &[&str] = &[
"Spacing",
"Equivalent",
"Inline",
"User agent control",
"Essential",
];
#[derive(Clone, Copy)]
pub struct ThemeSubject {
pub id: &'static str,
pub theme: fn() -> Theme,
}
#[derive(Clone, Copy)]
pub struct Roster {
pub themes: &'static [ThemeSubject],
pub allow: &'static [AllowedViolation],
}
pub fn conformance_census(fixtures: &[TargetFixture], roster: &Roster) -> Vec<TargetViolation> {
let mut out = Vec::new();
for subject in roster.themes {
let under_theme: Vec<TargetFixture> = fixtures
.iter()
.map(|f| (*f).with_theme(subject.theme))
.collect();
for &density in ALL_DENSITIES {
out.extend(
audit_fixtures(&under_theme, density)
.into_iter()
.filter(|v| v.rule.is_conformance_failure()),
);
}
}
out
}
pub struct CensusCell(std::sync::OnceLock<Vec<TargetViolation>>);
impl CensusCell {
pub const fn new() -> Self {
Self(std::sync::OnceLock::new())
}
pub fn get(&self, fixtures: &[TargetFixture], roster: &Roster) -> &[TargetViolation] {
self.0.get_or_init(|| conformance_census(fixtures, roster))
}
}
impl Default for CensusCell {
fn default() -> Self {
Self::new()
}
}
pub fn reported_failures(fixtures: &[TargetFixture], roster: &Roster) -> Vec<String> {
conformance_failures(&conformance_census(fixtures, roster), roster)
}
pub fn conformance_failures(census: &[TargetViolation], roster: &Roster) -> Vec<String> {
census
.iter()
.filter(|v| !roster.allow.iter().any(|e| e.matches(v)))
.map(|v| v.to_string())
.collect()
}
pub fn stale_entries(census: &[TargetViolation], roster: &Roster) -> Vec<String> {
let mut out = Vec::new();
for entry in roster.allow {
for (index, pin) in entry.measured.iter().enumerate() {
for &density in pin.densities {
for theme in pin.themes {
let met = census.iter().any(|v| {
v.density == density
&& v.theme.as_str() == *theme
&& v.path.contains(entry.path)
&& pin.covers(v)
});
if !met {
out.push(format!(
"`{}` pin {index} matches no conformance failure at {density:?} \
under {theme} any more. Either the geometry moved — re-measure it \
and rewrite the pin — or the control now conforms there, in which \
case narrow the pin's densities or themes, or delete it and its \
justification with it.",
entry.path,
));
}
}
}
}
}
out
}
pub fn non_narrowing(
census: &[TargetViolation],
roster: &Roster,
min_seeds_per_theme: usize,
) -> Vec<String> {
let mut out = Vec::new();
let mut floors: std::collections::HashMap<(&str, TargetDensity), f32> =
std::collections::HashMap::new();
for v in census {
floors
.entry((v.theme.as_str(), v.density))
.or_insert(v.conformance_floor);
}
for subject in roster.themes {
let mut seeded = 0_usize;
for violation in census.iter().filter(|v| v.theme.as_str() == subject.id) {
if !roster.allow.iter().any(|e| e.matches(violation)) {
continue;
}
for axis in 0..4 {
let mut broken = violation.clone();
match axis {
0 => broken.size.width = 2.0,
1 => broken.size.height = 2.0,
2 => broken.expanded.width = 2.0,
_ => broken.expanded.height = 2.0,
}
if roster.allow.iter().any(|e| e.matches(&broken)) {
out.push(format!(
"a regression seeded on axis {axis} of a real violation is still \
excused, so the entry covering it is a blanket over its region rather \
than a pin on a geometry:\n real: {violation}\n seeded: {broken}",
));
}
seeded += 1;
}
for other in roster.themes {
if other.id == subject.id {
continue;
}
let mut moved = violation.clone();
moved.theme = crate::styles::ThemeId::new(other.id);
moved.conformance_floor = *floors
.entry((other.id, violation.density))
.or_insert_with(|| {
(other.theme)()
.with_density(violation.density)
.input
.min_target_conformance
});
for entry in roster.allow.iter().filter(|e| e.matches(&moved)) {
for pin in entry.measured.iter().filter(|p| p.covers(&moved)) {
let exhibited = census.iter().any(|v| {
v.theme.as_str() == other.id
&& v.path.contains(entry.path)
&& pin.covers(v)
});
if !exhibited {
out.push(format!(
"`{}` excuses this geometry under {} as well, where the census \
produces no such failure — so the theme axis decorates the pin \
rather than narrowing it:\n real: {violation}\n seeded: \
{moved}",
entry.path, other.id,
));
}
}
}
seeded += 1;
}
}
if seeded < min_seeds_per_theme {
out.push(format!(
"only {seeded} regressions were seeded under {} — the census shrank, so this \
test is proving less than it reads as proving",
subject.id,
));
}
}
out
}
pub fn roster_defects(census: &[TargetViolation], roster: &Roster) -> Vec<String> {
let mut out = Vec::new();
for subject in roster.themes {
let built = (subject.theme)();
if built.id.as_str() != subject.id {
out.push(format!(
"the roster calls this theme `{}` and the theme calls itself `{}`, so every \
pin naming the first excuses nothing",
subject.id, built.id,
));
}
if subject.id == ANONYMOUS_THEME {
out.push(format!(
"`{ANONYMOUS_THEME}` is the id every theme built from raw tokens carries, so a \
roster naming it measures no particular preset",
));
}
}
let roster_ids: Vec<&str> = roster.themes.iter().map(|s| s.id).collect();
for entry in roster.allow {
if entry.why.is_empty() {
out.push(format!(
"allow-list entry `{}` carries no justification",
entry.path,
));
}
if entry.owner.text().is_empty() {
out.push(format!(
"allow-list entry `{}` leaves its {} empty — an entry is a debt with an \
address, or a written reason there is no debt",
entry.path,
match entry.owner {
Owner::Named(_) => "owner",
Owner::NobodyBecause(_) => "reason for having no owner",
},
));
}
if let Some(name) = entry.exception {
if !SC_2_5_8_EXCEPTIONS.contains(&name) {
out.push(format!(
"`{}` claims `{name}`, which is not one of SC 2.5.8's exceptions \
(Spacing / Equivalent / Inline / User agent control / Essential)",
entry.path,
));
}
if !entry.why.contains(name) {
out.push(format!(
"`{}` claims the SC 2.5.8 *{name}* exception in a field its justification \
never mentions",
entry.path,
));
}
}
if entry.measured.is_empty() {
out.push(format!(
"allow-list entry `{}` pins no measurement, so it excuses whatever its path \
happens to match",
entry.path,
));
}
for segment in entry.path.split(" > ") {
let segment = segment.rsplit(": ").next().unwrap_or(segment).trim();
if THEME_OWNED_PREFIXES.iter().any(|p| segment.starts_with(p)) {
out.push(format!(
"allow-list entry `{}` names `{segment}`, a type one preset owns. A path \
across a theme's private chrome goes silently stale the day that theme \
restacks it — the entry stops matching and nothing reports it. Name \
framework structure instead, and put the theme on the pin.",
entry.path,
));
}
}
for (index, pin) in entry.measured.iter().enumerate() {
if pin.densities.is_empty() {
out.push(format!("`{}` pin {index} claims no density", entry.path));
}
if pin.themes.is_empty() {
out.push(format!(
"`{}` pin {index} claims no theme, so it excuses a geometry under every \
preset the gate ever gains",
entry.path,
));
}
for theme in pin.themes {
if *theme == ANONYMOUS_THEME {
out.push(format!(
"`{}` pin {index} names `{ANONYMOUS_THEME}`, which every app theme \
shares — a pin on it is a pin on no preset",
entry.path,
));
}
if !roster_ids.contains(theme) {
out.push(format!(
"`{}` pin {index} names `{theme}`, which this gate does not measure",
entry.path,
));
}
}
}
}
for subject in roster.themes {
let reached = roster
.allow
.iter()
.any(|e| e.measured.iter().any(|m| m.themes.contains(&subject.id)));
let clean = !census.iter().any(|v| v.theme.as_str() == subject.id);
if !reached && !clean {
out.push(format!(
"{} produces conformance failures and no entry names it, so this preset is \
absent from the list without being clean",
subject.id,
));
}
}
out
}
#[cfg(test)]
mod tests;