Retire legacy reference data and the external command catalog (#302)
- Remove xtask/data/fusion-catalog.tsv and xtask/data/fusion-ids.tsv. - Remove the legacy command-id table (crates/engine/src/legacy_ids.rs) and its use in find_command and in the shortcut, recents and pinned-command loaders. Only SolidCraft's dotted ids name commands; any other id is an unknown command. - Boundary Fill's id becomes solid.boundary_fill. - Replace the parity report with SolidCraft's own feature checklist: `cargo xtask features` writes docs/features.md from the command registry. - docs/oracle.md, docs/step-import.md (corpus section) and docs/iges-import.md: reference results are being rebuilt on analytic and open-source (FreeCAD) values; see #292. `cargo xtask oracle` and `cargo xtask step-corpus [--iges]` write these placeholders. - AGENTS.md, CLAUDE.md, CONTRIBUTING.md, README.md, ROADMAP.md and the book describe SolidCraft as independent, with reference tests on analytic values and open-source tools, and agents do not drive or extract data from commercial CAD products. Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c7f0139ad4
commit
3b8028a040
32 files changed
+695
-2324
No files matched your search
@@ -0,0 +1,104 @@
|
||||
//! `cargo xtask features`: SolidCraft's own feature checklist, `docs/features.md`.
|
||||
//!
|
||||
//! The page lists every registered command by area (the first part of its dotted id), with its
|
||||
//! label, id and toolbar place. It comes only from SolidCraft's command registry
|
||||
//! (`solvecraft-cli commands`), so it can't drift from what the program does; it compares with
|
||||
//! nothing else.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Areas in the order the page lists them (id prefix, heading). Other prefixes follow,
|
||||
/// alphabetically, under their prefix.
|
||||
const AREAS: &[(&str, &str)] = &[
|
||||
("sketch", "Sketch"),
|
||||
("solid", "Solid modelling"),
|
||||
("surface", "Surfaces"),
|
||||
("construct", "Construction geometry"),
|
||||
("sheet", "Sheet metal"),
|
||||
("plastic", "Plastic parts"),
|
||||
("component", "Components"),
|
||||
("joint", "Assembly joints"),
|
||||
("config", "Configurations"),
|
||||
("timeline", "Timeline"),
|
||||
("inspect", "Inspect"),
|
||||
("file", "Files"),
|
||||
("doc", "Documents"),
|
||||
("edit", "Edit"),
|
||||
];
|
||||
|
||||
pub fn run(root: &Path) -> Result<(), String> {
|
||||
let out = crate::cargo()
|
||||
.args(["run", "-q", "--release", "-p", "solvecraft-cli", "--", "commands"])
|
||||
.output()
|
||||
.map_err(|e| format!("solvecraft-cli: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("solvecraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr)));
|
||||
}
|
||||
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
|
||||
let list = cmds.as_array().map(Vec::as_slice).unwrap_or_default();
|
||||
let md = page(list);
|
||||
std::fs::write(root.join("docs/features.md"), md).map_err(|e| format!("docs/features.md: {e}"))?;
|
||||
println!("features: {} commands → docs/features.md", list.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn area(id: &str) -> &str {
|
||||
id.split('.').next().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The checklist page for the registry's commands.
|
||||
pub fn page(cmds: &[Value]) -> String {
|
||||
let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string();
|
||||
let mut prefixes: Vec<String> = cmds.iter().map(|c| area(&s(c, "id")).to_string()).collect();
|
||||
prefixes.sort();
|
||||
prefixes.dedup();
|
||||
let mut order: Vec<(String, String)> =
|
||||
AREAS.iter().filter(|(p, _)| prefixes.iter().any(|x| x == p)).map(|(p, h)| (p.to_string(), h.to_string())).collect();
|
||||
for p in prefixes.iter().filter(|p| !AREAS.iter().any(|(a, _)| a == p)) {
|
||||
order.push((p.clone(), format!("`{p}.*`")));
|
||||
}
|
||||
let mut md = format!(
|
||||
"# Features\n\nSolidCraft's commands by area, generated by `cargo xtask features` from the command registry \
|
||||
(`solvecraft-cli commands`). **{} commands.** Every command runs the same way from the toolbar, a script, \
|
||||
`solvecraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the \
|
||||
[command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).\n",
|
||||
cmds.len()
|
||||
);
|
||||
for (prefix, heading) in &order {
|
||||
let in_area: Vec<&Value> = cmds.iter().filter(|c| area(&s(c, "id")) == prefix).collect();
|
||||
md += &format!("\n## {heading} ({})\n\n| Command | Id | Toolbar |\n|---|---|---|\n", in_area.len());
|
||||
for c in in_area {
|
||||
let place = match (s(c, "tab"), s(c, "panel")) {
|
||||
(t, _) if t.is_empty() => "–".to_string(),
|
||||
(t, p) if p.is_empty() => t,
|
||||
(t, p) => format!("{t} › {p}"),
|
||||
};
|
||||
let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default();
|
||||
md += &format!("| {}{key} | `{}` | {place} |\n", s(c, "label").replace('|', "/"), s(c, "id"));
|
||||
}
|
||||
}
|
||||
md
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn page_groups_by_area() {
|
||||
let cmds: Value = serde_json::from_str(
|
||||
r#"[{"id":"solid.extrude","label":"Extrude","tab":"SOLID","panel":"CREATE","shortcut":"E"},
|
||||
{"id":"sketch.line","label":"Line","tab":"SKETCH","panel":"CREATE"},
|
||||
{"id":"view.fit","label":"Fit","tab":"","panel":""}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let md = page(cmds.as_array().unwrap());
|
||||
assert!(md.contains("**3 commands.**"));
|
||||
let (sk, so, other) = (md.find("## Sketch (1)").unwrap(), md.find("## Solid modelling (1)").unwrap(), md.find("## `view.*` (1)").unwrap());
|
||||
assert!(sk < so && so < other);
|
||||
assert!(md.contains("| Extrude (E) | `solid.extrude` | SOLID › CREATE |"));
|
||||
assert!(md.contains("| Fit | `view.fit` | – |"));
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -5,9 +5,9 @@
|
||||
|
||||
mod assets;
|
||||
mod book;
|
||||
mod features;
|
||||
mod layers;
|
||||
mod licences;
|
||||
mod parity;
|
||||
mod test;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -19,9 +19,7 @@ usage: cargo xtask <command>
|
||||
commands:
|
||||
layers enforce the crate dependency layering (plan/architecture.md §3)
|
||||
assets check that every non-code asset file is attributed in ATTRIBUTION.md
|
||||
parity [--refresh]
|
||||
recompute Fusion command parity in docs/parity.md (--refresh rebuilds
|
||||
xtask/data/fusion-catalog.tsv from plan/fusion/menu-tree.json)
|
||||
features list SolidCraft's commands by area in docs/features.md (from the registry)
|
||||
oracle [--cli BIN]
|
||||
replay the reference tests (tests/oracle/*/recipe.json), compare with each
|
||||
case's expected.json and write docs/oracle.md (--cli: a solvecraft-cli
|
||||
@@ -46,7 +44,7 @@ fn main() -> ExitCode {
|
||||
let result = match args.first().map(String::as_str) {
|
||||
Some("layers") => cmd_layers(),
|
||||
Some("assets") => assets::run(&root()),
|
||||
Some("parity") => parity::run(&root(), rest.contains(&"--refresh")),
|
||||
Some("features") => features::run(&root()),
|
||||
Some("oracle") => cmd_oracle(rest.iter().position(|a| *a == "--cli").and_then(|i| rest.get(i + 1)).copied()),
|
||||
Some("book") => book::run(&root(), !rest.contains(&"--no-build")),
|
||||
Some("licences") => licences::run(&root(), rest.contains(&"--check")),
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
//! `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 SolidCraft registers the command `xtask/data/fusion-ids.tsv` maps its id to
|
||||
//! (SolidCraft uses its own ids).
|
||||
|
||||
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 (now: SOLID and SKETCH).
|
||||
const HEADLINE: &[&str] = &["SOLID", "SKETCH"];
|
||||
|
||||
/// The owner's scope: these tabs (and the PLASTIC subset below) count toward the in-scope
|
||||
/// figure; the rest of Fusion is deferred and listed separately.
|
||||
const IN_SCOPE_TABS: &[&str] = &["SOLID", "SKETCH", "ASSEMBLY", "SHEET METAL"];
|
||||
/// The PLASTIC commands in scope (enclosure features).
|
||||
/// (SolidCraft ids, matched through the catalog-id map.)
|
||||
const PLASTIC_SUBSET: &[&str] = &["plastic.boss", "solid.rib", "solid.web", "plastic.lip", "plastic.snap_fit"];
|
||||
|
||||
/// Catalog id → SolidCraft id.
|
||||
fn id_map(s: &str) -> std::collections::HashMap<String, String> {
|
||||
s.lines()
|
||||
.filter(|l| !l.starts_with('#') && !l.trim().is_empty())
|
||||
.filter_map(|l| l.split_once('\t').map(|(a, b)| (a.trim().to_string(), b.trim().to_string())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn in_plastic_subset(e: &Entry, map: &std::collections::HashMap<String, String>) -> bool {
|
||||
e.tab == "PLASTIC" && map.get(&e.id).is_some_and(|ours| PLASTIC_SUBSET.contains(&ours.as_str()))
|
||||
}
|
||||
|
||||
#[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 map_path = root.join("xtask/data/fusion-ids.tsv");
|
||||
let map = id_map(&std::fs::read_to_string(&map_path).map_err(|e| format!("{}: {e}", map_path.display()))?);
|
||||
let live = |e: &Entry| map.get(&e.id).is_some_and(|ours| ids.contains(ours));
|
||||
let in_scope = |e: &Entry| IN_SCOPE_TABS.contains(&e.tab.as_str()) || in_plastic_subset(e, &map);
|
||||
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 scope: Vec<&Entry> = catalog.iter().filter(|e| in_scope(e)).collect();
|
||||
let scope_live = scope.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 SolidCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.\n\n**In scope (SOLID, SKETCH, ASSEMBLY, SHEET METAL, PLASTIC enclosure subset): {scope_live} / {} commands live ({:.0}%).** SOLID + SKETCH: {head_live} / {} ({:.0}%). Registered commands: {}.\n\nDeferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE beyond what solids need, PCB, render, animation, simulation, manufacture, drawings, MANAGE, UTILITIES and the rest of PLASTIC. All Design tabs together: {all_live} / {} ({:.0}%).\n\n| Tab | Panel | Live | Total | % |\n|---|---|---:|---:|---:|\n",
|
||||
scope.len(),
|
||||
pct(scope_live, scope.len()),
|
||||
head.len(),
|
||||
pct(head_live, head.len()),
|
||||
ids.len(),
|
||||
catalog.len(),
|
||||
pct(all_live, catalog.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.iter().filter(|g| IN_SCOPE_TABS.contains(&g.0.as_str())) {
|
||||
md += &format!("| {t} | {p} | {l} | {n} | {:.0}% |\n", pct(*l, *n));
|
||||
}
|
||||
let plastic: Vec<&Entry> = catalog.iter().filter(|e| in_plastic_subset(e, &map)).collect();
|
||||
md += &format!(
|
||||
"| PLASTIC | enclosure subset | {} | {} | {:.0}% |\n",
|
||||
plastic.iter().filter(|e| live(e)).count(),
|
||||
plastic.len(),
|
||||
pct(plastic.iter().filter(|e| live(e)).count(), plastic.len())
|
||||
);
|
||||
md += "\n### Deferred tabs\n\n| Tab | Panel | Live | Total | % |\n|---|---|---:|---:|---:|\n";
|
||||
for (t, p, l, n) in groups.iter().filter(|g| !IN_SCOPE_TABS.contains(&g.0.as_str())) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
std::fs::write(root.join("docs/parity.md"), md).map_err(|e| format!("docs/parity.md: {e}"))?;
|
||||
println!(
|
||||
"parity: in scope {scope_live}/{} ({:.0}%), SOLID+SKETCH {head_live}/{} ({:.0}%), all tabs {all_live}/{} → docs/parity.md",
|
||||
scope.len(),
|
||||
pct(scope_live, scope.len()),
|
||||
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