Files
deckcraft/xtask/src/parity.rs
T
Brandon ThomasandClaude Opus 5.5 f44857624e Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#89)
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, hardware, UI and localization parity docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:09:21 -04:00

100 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo xtask parity`: recompute the feature-parity summary in docs/parity-checklist.md from its row
//! table (each PowerPoint feature scored D / P / M, weighted by priority).
use std::path::Path;
const START: &str = "<!-- SUMMARY -->";
const ROWS: &str = "\n## Rows";
/// (area, feature, priority 0–2, value 0 / 0.5 / 1) for each table row.
pub fn rows(text: &str) -> Vec<(String, String, u8, f64)> {
let mut out = Vec::new();
for l in text.lines() {
let cells: Vec<&str> = l.trim().trim_matches('|').split('|').map(str::trim).collect();
if cells.len() != 4 {
continue;
}
let pri = match cells[2] {
"P0" => 0,
"P1" => 1,
"P2" => 2,
_ => continue,
};
let v = match cells[3] {
"D" => 1.0,
"P" => 0.5,
"M" => 0.0,
_ => continue,
};
out.push((cells[0].to_string(), cells[1].to_string(), pri, v));
}
out
}
fn weight(p: u8) -> f64 {
[3.0, 2.0, 1.0][p as usize]
}
pub fn summary(rows: &[(String, String, u8, f64)]) -> String {
let pct = |rs: &[&(String, String, u8, f64)]| {
let tot: f64 = rs.iter().map(|r| weight(r.2)).sum();
if tot == 0.0 { 0.0 } else { 100.0 * rs.iter().map(|r| weight(r.2) * r.3).sum::<f64>() / tot }
};
let all: Vec<_> = rows.iter().collect();
let mut s = format!(
"**Weighted breadth parity: {:.0}%** over {} features.\n\n| Priority | Features | Done | Partial | Missing | Parity |\n|---|---|---|---|---|---|\n",
pct(&all),
rows.len()
);
for p in 0..3u8 {
let rr: Vec<_> = rows.iter().filter(|r| r.2 == p).collect();
let n = |v: f64| rr.iter().filter(|r| r.3 == v).count();
let unweighted = if rr.is_empty() { 0.0 } else { 100.0 * rr.iter().map(|r| r.3).sum::<f64>() / rr.len() as f64 };
s += &format!("| P{p} | {} | {} | {} | {} | {unweighted:.0}% |\n", rr.len(), n(1.0), n(0.5), n(0.0));
}
s += "\n| Area | Parity |\n|---|---|\n";
let mut areas: Vec<&str> = Vec::new();
for r in rows {
if !areas.contains(&r.0.as_str()) {
areas.push(&r.0);
}
}
for a in areas {
let rr: Vec<_> = rows.iter().filter(|r| r.0 == a).collect();
s += &format!("| {a} | {:.0}% |\n", pct(&rr));
}
let open: Vec<&str> = rows.iter().filter(|r| r.2 == 0 && r.3 < 1.0).map(|r| r.1.as_str()).collect();
s += &format!("\nOpen P0 items: {}.\n", open.join(", "));
s
}
pub fn run(root: &Path) -> Result<(), String> {
let path = root.join("docs/parity-checklist.md");
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
let (Some(a), Some(b)) = (text.find(START), text.find(ROWS)) else {
return Err("docs/parity-checklist.md needs a `<!-- SUMMARY -->` marker before `## Rows`".into());
};
let rows = rows(&text[b..]);
let s = summary(&rows);
let new = format!("{}{START}\n{s}{}", &text[..a], &text[b..]);
std::fs::write(&path, new).map_err(|e| e.to_string())?;
print!("{s}");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scores_rows() {
let t = "| Area | Feature | Priority | Status |\n|---|---|---|---|\n| A | x | P0 | D |\n| A | y | P2 | M |\n| B | z | P1 | P |\n";
let r = rows(t);
assert_eq!(r.len(), 3);
let s = summary(&r);
// (3·1 + 1·0 + 2·0.5) / 6 = 67%
assert!(s.contains("**Weighted breadth parity: 67%**"), "{s}");
assert!(s.contains("Open P0 items: ."));
}
}