M0: xtask layers, parity, oracle, assets, ci; Fusion command catalog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0983f80f01
commit
3714342737
6 files changed
+2285
-1
No files matched your search
@@ -0,0 +1,173 @@
|
||||
//! `cargo xtask parity`: Fusion Design-workspace command parity.
|
||||
//!
|
||||
//! `xtask/data/fusion-catalog.tsv` lists the toolbar commands of Fusion's Design workspace as
|
||||
//! `tab \t panel \t command id \t command name` (ids and names only, nothing else). It is
|
||||
//! derived from the local, uncommitted `plan/fusion/menu-tree.json` with `--refresh`. A catalog
|
||||
//! entry is live when SolveCraft registers a command with the same id.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Design workspace tabs in the catalog (menu-tree tab id, display name).
|
||||
const TABS: &[(&str, &str)] = &[
|
||||
("SolidTab", "SOLID"),
|
||||
("SketchTab", "SKETCH"),
|
||||
("SurfaceTab", "SURFACE"),
|
||||
("ParaMeshOuterTab", "MESH"),
|
||||
("FormTab", "FORM"),
|
||||
("SheetMetalTab", "SHEET METAL"),
|
||||
("PlasticTab", "PLASTIC"),
|
||||
("AssemblyTab", "ASSEMBLY"),
|
||||
("ManageTab", "MANAGE"),
|
||||
("ToolsTab", "UTILITIES"),
|
||||
];
|
||||
|
||||
/// The tabs the headline number counts (M0 scope).
|
||||
const HEADLINE: &[&str] = &["SOLID", "SKETCH"];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entry {
|
||||
pub tab: String,
|
||||
pub panel: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
fn clean(s: &str) -> String {
|
||||
s.replace(['\t', '\n', '\r'], " ").trim().to_string()
|
||||
}
|
||||
|
||||
fn walk(controls: &[Value], defs: &[Value], tab: &str, panel: &str, out: &mut Vec<Entry>) {
|
||||
for c in controls {
|
||||
match c["type"].as_str() {
|
||||
Some("CommandControl") => {
|
||||
let id = c["command"]["id"].as_str().or(c["id"].as_str()).unwrap_or("");
|
||||
if id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let name = c["command"]["name"]
|
||||
.as_str()
|
||||
.filter(|n| !n.is_empty())
|
||||
.or_else(|| defs.iter().find(|d| d["id"].as_str() == Some(id)).and_then(|d| d["name"].as_str()))
|
||||
.unwrap_or(id);
|
||||
if !out.iter().any(|e| e.tab == tab && e.id == id) {
|
||||
out.push(Entry { tab: tab.into(), panel: panel.into(), id: clean(id), name: clean(name) });
|
||||
}
|
||||
}
|
||||
Some("DropDownControl") => walk(c["controls"].as_array().map(Vec::as_slice).unwrap_or(&[]), defs, tab, panel, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Catalog entries from Fusion's menu-tree JSON.
|
||||
pub fn from_menu_tree(v: &Value) -> Vec<Entry> {
|
||||
let defs = v["commandDefinitions"].as_array().map(Vec::as_slice).unwrap_or(&[]);
|
||||
let mut out = Vec::new();
|
||||
for (tid, tname) in TABS {
|
||||
let tab = &v["tabs"][format!("DesignProductType/{tid}")];
|
||||
for p in tab["panels"].as_array().into_iter().flatten() {
|
||||
let panel = clean(p["name"].as_str().unwrap_or("")).to_uppercase();
|
||||
walk(p["controls"].as_array().map(Vec::as_slice).unwrap_or(&[]), defs, tname, &panel, &mut out);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn parse_catalog(s: &str) -> Vec<Entry> {
|
||||
s.lines()
|
||||
.filter(|l| !l.starts_with('#') && !l.trim().is_empty())
|
||||
.filter_map(|l| {
|
||||
let mut it = l.split('\t');
|
||||
Some(Entry { tab: it.next()?.into(), panel: it.next()?.into(), id: it.next()?.into(), name: it.next().unwrap_or("").into() })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn run(root: &Path, refresh: bool) -> Result<(), String> {
|
||||
let cat_path = root.join("xtask/data/fusion-catalog.tsv");
|
||||
if refresh {
|
||||
let mt = root.join("plan/fusion/menu-tree.json");
|
||||
let s = std::fs::read_to_string(&mt).map_err(|e| format!("{}: {e}", mt.display()))?;
|
||||
let v: Value = serde_json::from_str(&s).map_err(|e| format!("menu-tree.json: {e}"))?;
|
||||
let entries = from_menu_tree(&v);
|
||||
let mut out = String::from("# Fusion Design workspace toolbar: tab, panel, command id, command name (ids and names only).\n# Regenerate with `cargo xtask parity --refresh` (needs the local plan/fusion/menu-tree.json).\n");
|
||||
for e in &entries {
|
||||
out += &format!("{}\t{}\t{}\t{}\n", e.tab, e.panel, e.id, e.name);
|
||||
}
|
||||
std::fs::create_dir_all(root.join("xtask/data")).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&cat_path, out).map_err(|e| format!("{}: {e}", cat_path.display()))?;
|
||||
println!("parity: catalog refreshed with {} entries", entries.len());
|
||||
}
|
||||
let catalog = parse_catalog(&std::fs::read_to_string(&cat_path).map_err(|e| format!("{}: {e}", cat_path.display()))?);
|
||||
let out = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
|
||||
.current_dir(root)
|
||||
.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--", "commands"])
|
||||
.output()
|
||||
.map_err(|e| format!("solvecraft-cli: {e}"))?;
|
||||
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
|
||||
let ids: Vec<String> = cmds.as_array().into_iter().flatten().filter_map(|c| c["id"].as_str().map(str::to_string)).collect();
|
||||
let live = |e: &Entry| ids.iter().any(|i| *i == e.id);
|
||||
let pct = |a: usize, b: usize| if b == 0 { 0.0 } else { 100.0 * a as f64 / b as f64 };
|
||||
|
||||
let head: Vec<&Entry> = catalog.iter().filter(|e| HEADLINE.contains(&e.tab.as_str())).collect();
|
||||
let head_live = head.iter().filter(|e| live(e)).count();
|
||||
let all_live = catalog.iter().filter(|e| live(e)).count();
|
||||
let mut md = format!(
|
||||
"# Fusion command parity\n\nGenerated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolveCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.\n\n**SOLID + SKETCH: {head_live} / {} commands live ({:.0}%).** All Design tabs: {all_live} / {} ({:.0}%). Registered commands: {}.\n\n| Tab | Panel | Live | Total | % |\n|---|---|---:|---:|---:|\n",
|
||||
head.len(),
|
||||
pct(head_live, head.len()),
|
||||
catalog.len(),
|
||||
pct(all_live, catalog.len()),
|
||||
ids.len()
|
||||
);
|
||||
let mut groups: Vec<(String, String, usize, usize)> = Vec::new();
|
||||
for e in &catalog {
|
||||
let l = usize::from(live(e));
|
||||
match groups.iter_mut().find(|g| g.0 == e.tab && g.1 == e.panel) {
|
||||
Some(g) => {
|
||||
g.2 += l;
|
||||
g.3 += 1;
|
||||
}
|
||||
None => groups.push((e.tab.clone(), e.panel.clone(), l, 1)),
|
||||
}
|
||||
}
|
||||
for (t, p, l, n) in &groups {
|
||||
md += &format!("| {t} | {p} | {l} | {n} | {:.0}% |\n", pct(*l, *n));
|
||||
}
|
||||
md += "\n## Live (SOLID, SKETCH)\n\n";
|
||||
for e in head.iter().filter(|e| live(e)) {
|
||||
md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id);
|
||||
}
|
||||
md += "\n## Not yet (SOLID, SKETCH)\n\n";
|
||||
for e in head.iter().filter(|e| !live(e)) {
|
||||
md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id);
|
||||
}
|
||||
std::fs::write(root.join("docs/parity.md"), md).map_err(|e| format!("docs/parity.md: {e}"))?;
|
||||
println!("parity: SOLID+SKETCH {head_live}/{} ({:.0}%), all tabs {all_live}/{} → docs/parity.md", head.len(), pct(head_live, head.len()), catalog.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn catalog_from_menu_tree() {
|
||||
let v: Value = serde_json::from_str(
|
||||
r#"{"commandDefinitions":[{"id":"B","name":"Bee"}],"tabs":{"DesignProductType/SolidTab":{"panels":[{"name":"Create","controls":[
|
||||
{"type":"CommandControl","id":"A","command":{"id":"A","name":"Aye"}},
|
||||
{"type":"SeparatorControl","id":"s"},
|
||||
{"type":"DropDownControl","id":"d","controls":[{"type":"CommandControl","id":"B","command":{"id":"B"}}]}]}]}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let e = from_menu_tree(&v);
|
||||
assert_eq!(e.len(), 2);
|
||||
assert_eq!((e[0].tab.as_str(), e[0].panel.as_str(), e[0].id.as_str(), e[0].name.as_str()), ("SOLID", "CREATE", "A", "Aye"));
|
||||
assert_eq!(e[1].name, "Bee");
|
||||
let tsv = "# c\nSOLID\tCREATE\tA\tAye\n";
|
||||
assert_eq!(parse_catalog(tsv).len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user