diff --git a/apps/vectorcraft-cli/tests/it/cli.rs b/apps/vectorcraft-cli/tests/it/cli.rs index f2600d9b4..5bdd2bf63 100644 --- a/apps/vectorcraft-cli/tests/it/cli.rs +++ b/apps/vectorcraft-cli/tests/it/cli.rs @@ -597,7 +597,7 @@ fn exercise_mcp_core( assert!(r["result"]["completion"]["values"].as_array().is_some_and(|v| v.contains(&json!("svg"))), "{r}"); mcp_send(stdin, json!({"jsonrpc":"2.0","id":5,"method":"resources/list","params":{}})); - assert!(mcp_recv(lines, notes, 5)["result"]["resources"].as_array().is_some_and(|v| v.len() == 3)); + assert!(mcp_recv(lines, notes, 5)["result"]["resources"].as_array().is_some_and(|v| v.len() == 4)); mcp_send(stdin, json!({"jsonrpc":"2.0","id":6,"method":"resources/templates/list","params":{}})); assert!(mcp_recv(lines, notes, 6)["result"]["resourceTemplates"].as_array().is_some_and(|v| v.len() >= 4)); diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index a68703f11..7c1c9eea4 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -2,6 +2,7 @@ pub mod actionset; mod addons; +mod apiref; pub(crate) mod appearance; mod assets; mod attributes; @@ -384,6 +385,8 @@ pub fn command_specs() -> &'static [CommandSpec] { v.extend(actionset::specs()); v.extend(importmatches::specs()); v.extend(addons::specs()); + // Last, so the reference it writes ends with its own section. + v.extend(apiref::specs()); v }) } diff --git a/crates/engine/src/cmd/apiref.rs b/crates/engine/src/cmd/apiref.rs new file mode 100644 index 000000000..54e07a1c8 --- /dev/null +++ b/crates/engine/src/cmd/apiref.rs @@ -0,0 +1,461 @@ +//! `engine.apiReference`: the command API reference in Markdown, written when it is asked for from +//! the parameter schemas of the built-in commands ([`crate::schemas`]). Each command that has a +//! schema gets a section, in registry order, with its parameters in the schema's order; a command +//! without a schema, a plug-in's included, is left out. The text depends only on the registry, so +//! every call in one build returns the same bytes. +//! +//! Text from a schema is escaped before it goes into a heading, a list item or a table cell: the +//! Markdown punctuation characters get a backslash (CommonMark 0.31.2, backslash escapes, +//! ), so a description shows as plain text and +//! cannot end a table cell or make a link, an image or HTML; line breaks, tabs and other control +//! characters become spaces. + +use serde_json::{Value, json}; +use vectorcraft_apischema::{Bound, MAX_DEPTH, MAX_ID, Omitted, Param, ParamSchema, Type, Writes}; + +use super::{CommandSpec, Result, Session, always, cmd, command_specs}; +use crate::EngineError; + +const ID: &str = "engine.apiReference"; + +/// The largest reference the command returns, in bytes of UTF-8. +pub const MAX_BYTES: usize = 1 << 20; + +static REFERENCE: ParamSchema = ParamSchema::new( + &[], + "the command API reference in Markdown, written from the built-in commands' parameter schemas: a section for each command that has one, in registry order", +) +.returns("{markdown: string}") +.writes(Writes::Nothing) +.since(2); + +pub fn specs() -> Vec { + vec![cmd!( + query "engine.apiReference", + "Command API Reference", + [], + None, + "{} the command API reference in Markdown, written from the built-in commands' parameter schemas: a section for each command that has one, in registry order → {markdown: string}", + always, + api_reference, + schema: &REFERENCE + )] +} + +fn api_reference(_: &mut Session, _: &Value) -> Result { + Ok(json!({ "markdown": reference(command_specs())? })) +} + +const HEADER: &str = "# VectorCraft command API reference\n\nThis reference lists only the commands that have a parameter schema, in registry order. `engine.commands` lists every command, with its params text and whether it can run now.\n"; + +/// The reference for the commands of `specs` that have a schema, or an error when it would be +/// larger than [`MAX_BYTES`]; a reference that is too large is never cut short. +pub(crate) fn reference(specs: &[CommandSpec]) -> Result { + let mut out = String::from(HEADER); + let mut sections = 0; + for spec in specs { + let Some(schema) = spec.schema else { continue }; + let text = section(spec, schema); + if out.len() + text.len() > MAX_BYTES { + return Err(EngineError::Other(format!("`{ID}`: the reference would be larger than {MAX_BYTES} bytes"))); + } + out.push_str(&text); + sections += 1; + } + if sections == 0 { + out.push_str("\nNo commands have parameter schemas.\n"); + } + Ok(out) +} + +/// One command's section: its heading, a list of what it is, and its parameters. +fn section(spec: &CommandSpec, schema: &ParamSchema) -> String { + let menu = if spec.menu.is_empty() { "None".to_string() } else { spec.menu.iter().map(|m| escape(m)).collect::>().join(" › ") }; + let mut s = format!("\n## {}\n\n", escape(spec.id)); + s.push_str(&format!("- Label: {}\n", escape(spec.label))); + s.push_str(&format!("- Menu: {menu}\n")); + s.push_str(&format!("- API level: {}\n", schema.since)); + s.push_str(&format!("- Deprecated: {}\n", level_text(schema.deprecated))); + s.push_str(&format!("- Writes: {}\n", writes_text(schema.writes))); + s.push_str(&format!("- Cost: {}\n", schema.cost)); + s.push_str(&format!("- Summary: {}\n", text_or_none(schema.summary))); + s.push_str(&format!("- Returns: {}\n\n", text_or_none(schema.returns))); + if schema.params.is_empty() { + s.push_str("No parameters.\n"); + return s; + } + s.push_str("| Parameter | Type | Range | Unit | Default | Required | Since | Deprecated | Description |\n"); + s.push_str("|---|---|---|---|---|---|---|---|---|\n"); + rows("", schema.params, 0, &mut s); + s +} + +/// A table row for each of `params`, and after each one the rows of the members of an object it +/// holds: `edits[].name` for an object in a list, `options.size` for an object. +fn rows(prefix: &str, params: &[Param], depth: usize, out: &mut String) { + for p in params { + let name = if prefix.is_empty() { p.name.to_string() } else { format!("{prefix}.{}", p.name) }; + let (range, unit) = range_and_unit(&p.ty); + out.push_str(&format!( + "| {} | {} | {range} | {unit} | {} | {} | {} | {} | {} |\n", + escape(&name), + type_text(&p.ty, depth), + default_text(p.omitted), + if p.required { "Yes" } else { "No" }, + p.since, + level_text(p.deprecated), + description(p), + )); + if depth < MAX_DEPTH + && let Some((members, path)) = members(&p.ty, name, depth) + { + rows(&path, members, depth + 1, out); + } + } +} + +/// The parameters of the object `ty` holds, directly or as the items of lists, with the path of +/// its members' prefix (`name`, `name[]`, `name[][]`). +fn members(ty: &Type, mut path: String, depth: usize) -> Option<(&'static [Param], String)> { + let mut ty = ty; + for _ in depth..MAX_DEPTH { + match ty { + Type::Object(params) => return Some((params, path)), + Type::Array(item) => { + path.push_str("[]"); + ty = item; + } + _ => return None, + } + } + None +} + +fn type_text(ty: &Type, depth: usize) -> String { + match ty { + Type::Number { .. } => "number".into(), + Type::Integer { .. } => "integer".into(), + Type::Boolean => "boolean".into(), + Type::String => "string".into(), + Type::Enum(names) => format!("one of {}", names.iter().map(|n| format!("\"{}\"", escape(n))).collect::>().join(", ")), + Type::Id => "object id (integer)".into(), + Type::Ids => "list of object ids".into(), + Type::Color => escape("color (a string, [r, g, b] or an object)"), + Type::Point => escape("point ([x, y], two numbers)"), + Type::Object(_) => "object".into(), + Type::Array(_) if depth >= MAX_DEPTH => "list".into(), + Type::Array(item) => format!("list of {}", type_text(item, depth + 1)), + } +} + +/// The range and unit cells of a value of type `ty`. +fn range_and_unit(ty: &Type) -> (String, String) { + let ends = |low: Option, high: Option| { + let both: Vec = low.into_iter().chain(high).collect(); + if both.is_empty() { "None".to_string() } else { both.join(", ") } + }; + match ty { + Type::Number { min, max, unit } => { + let low = min.map(|b| match b { + Bound::Inclusive(a) => format!("at least {a}"), + Bound::Exclusive(a) => format!("more than {a}"), + }); + let high = max.map(|b| match b { + Bound::Inclusive(a) => format!("at most {a}"), + Bound::Exclusive(a) => format!("less than {a}"), + }); + (ends(low, high), unit.map_or("None".to_string(), escape)) + } + Type::Integer { min, max } => (ends(min.map(|a| format!("at least {a}")), max.map(|a| format!("at most {a}"))), "None".into()), + Type::Id => (format!("at least 0, at most {MAX_ID}"), "None".into()), + _ => ("None".into(), "None".into()), + } +} + +fn default_text(omitted: Omitted) -> String { + match omitted { + Omitted::Nothing => "None".into(), + Omitted::Value(json) => format!("{} (JSON)", escape(json)), + Omitted::Note(note) => format!("{} (worked out by the command)", escape(note)), + } +} + +/// The parameter's doc text, and what the schema says about how its value is checked. +fn description(p: &Param) -> String { + let mut parts = Vec::new(); + if !p.doc.is_empty() { + parts.push(escape(p.doc)); + } + if p.null_omitted { + parts.push("null reads as omitted".to_string()); + } + if p.lenient { + parts.push("lenient (a value of another type or out of range is passed to the command, not refused)".to_string()); + } + if parts.is_empty() { "None".to_string() } else { parts.join("; ") } +} + +fn level_text(level: Option) -> String { + level.map_or("None".to_string(), |l| format!("API level {l}")) +} + +fn writes_text(writes: Writes) -> &'static str { + match writes { + Writes::Nothing => "nothing", + Writes::Session => "session", + Writes::Preferences => "preferences", + Writes::Document => "document", + } +} + +fn text_or_none(text: &str) -> String { + if text.is_empty() { "None".to_string() } else { escape(text) } +} + +/// The characters [`escape`] puts a backslash before. +const ESCAPED: &[char] = &['\\', '|', '[', ']', '<', '>', '`', '*', '_', '!', '&', '#', '~', ':', '@']; + +/// `text` as plain Markdown text on one line: each of [`ESCAPED`] with a backslash before it, the +/// `.` after `www` too (so no autolink starts), and each control character as a space. +pub(crate) fn escape(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + for c in text.chars() { + if c.is_control() { + out.push(' '); + continue; + } + let after_www = out.len().checked_sub(3).and_then(|at| out.as_bytes().get(at..)).is_some_and(|b| b.eq_ignore_ascii_case(b"www")); + if ESCAPED.contains(&c) || (c == '.' && after_www) { + out.push('\\'); + } + out.push(c); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn run(_: &mut Session, _: &Value) -> Result { + Ok(Value::Null) + } + + fn spec(id: &'static str, menu: &'static [&'static str], schema: Option<&'static ParamSchema>) -> CommandSpec { + CommandSpec { id, label: "Label", menu, shortcut: None, params: "", enabled: always, run, journal: false, schema } + } + + /// The cells of a table row as a GFM table reads them: split at each `|` that has no backslash + /// before it, with the outer pipes and the spaces around each cell taken off. + fn cells(row: &str) -> Vec { + let inner = row.strip_prefix('|').and_then(|r| r.strip_suffix('|')).unwrap_or_else(|| panic!("not a table row: {row}")); + let (mut out, mut cell, mut chars) = (Vec::new(), String::new(), inner.chars()); + while let Some(c) = chars.next() { + match c { + '\\' => { + cell.push(c); + cell.extend(chars.next()); + } + '|' => out.push(std::mem::take(&mut cell).trim().to_string()), + _ => cell.push(c), + } + } + out.push(cell.trim().to_string()); + out + } + + /// What `text` shows as: each escaped character as itself. Panics on a Markdown punctuation + /// character without its backslash, or a backslash before a character that needs none. + fn literal(text: &str) -> String { + let (mut out, mut chars) = (String::new(), text.chars()); + while let Some(c) = chars.next() { + assert!(!ESCAPED.contains(&c) || c == '\\', "`{c}` is not escaped in {text}"); + if c == '\\' { + let next = chars.next().unwrap_or_else(|| panic!("a backslash ends {text}")); + assert!(ESCAPED.contains(&next) || next == '.', "`\\{next}` in {text}"); + out.push(next); + } else { + out.push(c); + } + } + out + } + + /// The shown cells of the row of parameter `name`. + fn row(markdown: &str, name: &str) -> Vec { + let found = markdown.lines().filter(|l| l.starts_with('|')).find(|l| cells(l).first().map(|c| literal(c)).as_deref() == Some(name)); + let row = found.unwrap_or_else(|| panic!("no row for {name} in\n{markdown}")); + let cells = cells(row); + assert_eq!(cells.len(), 9, "{row}"); + cells.iter().map(|c| literal(c)).collect() + } + + /// The text after `- {label}: ` in the section's list. + fn item(markdown: &str, label: &str) -> String { + let prefix = format!("- {label}: "); + let line = markdown.lines().find_map(|l| l.strip_prefix(prefix.as_str())); + literal(line.unwrap_or_else(|| panic!("no {label} in\n{markdown}"))) + } + + /// Two calls give the same bytes, with no document open, and the reference has one section + /// for each built-in command that has a schema, in registry order, ending with this command's. + #[test] + fn the_reference_lists_each_schema_once_in_registry_order_and_is_the_same_each_time() { + let mut s = Session::new(); + let first = s.execute(ID, &json!({})).unwrap(); + let second = s.execute(ID, &Value::Null).unwrap(); + assert_eq!(first, second); + let markdown = first["markdown"].as_str().unwrap(); + assert_eq!(markdown, reference(command_specs()).unwrap()); + assert!(markdown.len() < MAX_BYTES / 10, "{} bytes", markdown.len()); + let headings: Vec = markdown.lines().filter_map(|l| l.strip_prefix("## ")).map(literal).collect(); + let expected: Vec<&str> = command_specs().iter().filter(|c| c.schema.is_some()).map(|c| c.id).collect(); + assert_eq!(headings, expected); + assert_eq!(headings.last().map(String::as_str), Some(ID)); + assert!(expected.contains(&"view.saved.new") && !headings.iter().any(|h| h == "file.new")); + let new_view = &markdown[markdown.find("## view.saved.new").unwrap()..]; + let names: Vec = new_view + .lines() + .skip_while(|l| !l.starts_with("|---")) + .skip(1) + .take_while(|l| l.starts_with('|')) + .map(|l| literal(&cells(l)[0])) + .collect(); + assert_eq!(names, ["name", "center", "zoom", "rotation"]); + let edit = &markdown[markdown.find("## view.saved.edit").unwrap()..]; + assert_eq!(row(edit, "edits[].newName")[8], "null reads as omitted"); + assert_eq!(row(edit, "newName")[8], "rename; null reads as omitted"); + } + + const INNER: &[Param] = + &[Param::new("size", Type::Integer { min: Some(1), max: Some(10) }), Param::new("mode", Type::Enum(&["a", "b"])).defaults_to("\"a\"")]; + + static FULL: ParamSchema = ParamSchema::new( + &[ + Param::new("zoom", Type::Number { min: Some(Bound::Exclusive(0.0)), max: Some(Bound::Inclusive(64.0)), unit: Some("pt") }) + .doc("1 = 100%"), + Param::new("name", Type::String).default_note("the first free name").null_omitted(), + Param::new("old", Type::Boolean).defaults_to("true").since(2).deprecated(3).lenient(), + Param::new("edits", Type::Array(&Type::Object(INNER))).optional(), + Param::new("ids", Type::Ids).optional(), + Param::new("at", Type::Point).optional(), + Param::new("color", Type::Color).optional(), + Param::new("id", Type::Id).optional(), + Param::new("ratio", Type::Number { min: Some(Bound::Inclusive(-1.5)), max: Some(Bound::Exclusive(2.0)), unit: None }).optional(), + ], + "do the thing", + ) + .returns("{done}") + .writes(Writes::Preferences) + .since(2) + .deprecated(4) + .cost(5); + + /// Each field of a schema shows, nested members in their order under dotted names. + #[test] + fn every_field_of_a_schema_is_shown() { + let markdown = reference(&[spec("test.full", &["View", "Saved"], Some(&FULL))]).unwrap(); + for (label, shown) in [ + ("Label", "Label"), + ("Menu", "View › Saved"), + ("API level", "2"), + ("Deprecated", "API level 4"), + ("Writes", "preferences"), + ("Cost", "5"), + ("Summary", "do the thing"), + ("Returns", "{done}"), + ] { + assert_eq!(item(&markdown, label), shown, "{label}"); + } + let lenient = "lenient (a value of another type or out of range is passed to the command, not refused)"; + let expected: [[&str; 9]; 11] = [ + ["zoom", "number", "more than 0, at most 64", "pt", "None", "Yes", "1", "None", "1 = 100%"], + ["name", "string", "None", "None", "the first free name (worked out by the command)", "No", "1", "None", "null reads as omitted"], + ["old", "boolean", "None", "None", "true (JSON)", "No", "2", "API level 3", lenient], + ["edits", "list of object", "None", "None", "None", "No", "1", "None", "None"], + ["edits[].size", "integer", "at least 1, at most 10", "None", "None", "Yes", "1", "None", "None"], + ["edits[].mode", "one of \"a\", \"b\"", "None", "None", "\"a\" (JSON)", "No", "1", "None", "None"], + ["ids", "list of object ids", "None", "None", "None", "No", "1", "None", "None"], + ["at", "point ([x, y], two numbers)", "None", "None", "None", "No", "1", "None", "None"], + ["color", "color (a string, [r, g, b] or an object)", "None", "None", "None", "No", "1", "None", "None"], + ["id", "object id (integer)", "at least 0, at most 9007199254740991", "None", "None", "No", "1", "None", "None"], + ["ratio", "number", "at least -1.5, less than 2", "None", "None", "No", "1", "None", "None"], + ]; + let names: Vec = + markdown.lines().filter(|l| l.starts_with("| ") && !l.starts_with("| Parameter")).map(|l| literal(&cells(l)[0])).collect(); + assert_eq!(names, expected.map(|r| r[0])); + for want in expected { + assert_eq!(row(&markdown, want[0]), want, "{}", want[0]); + } + } + + const HOSTILE: &str = "a | b [link](https://evil.example) `code` \\ *em* _u_ ![img](x) & ~~s~~ # h www.example.com me@example.com\r\nnext\tline"; + + static ESCAPES: ParamSchema = + ParamSchema::new(&[Param::new("a|b", Type::String).doc(HOSTILE).defaults_to("\"|\"")], HOSTILE).returns(HOSTILE); + + /// Markdown in a schema's text shows as plain text: a description stays in its own cell of a + /// nine-cell row, every punctuation character is escaped, and line breaks and tabs are spaces. + #[test] + fn markdown_in_schema_text_stays_literal_in_its_cell() { + let markdown = reference(&[spec("test.escapes", &["A|B", "[x](y)"], Some(&ESCAPES))]).unwrap(); + let shown = HOSTILE.replace(['\r', '\n', '\t'], " "); + let cells = row(&markdown, "a|b"); + assert_eq!(cells[4], "\"|\" (JSON)"); + assert_eq!(cells[8], shown); + assert_eq!(item(&markdown, "Summary"), shown); + assert_eq!(item(&markdown, "Returns"), shown); + assert_eq!(item(&markdown, "Menu"), "A|B › [x](y)"); + assert!(markdown.contains("www\\.example"), "{markdown}"); + let table: Vec<&str> = markdown.lines().filter(|l| l.starts_with('|')).collect(); + assert_eq!(table.len(), 3, "the header, the delimiter row and one row: {table:?}"); + assert!(!markdown.contains('\r') && !markdown.contains('\t')); + } + + /// A registry with no schema gives the header and a line that says so; a command without a + /// schema adds nothing. + #[test] + fn a_registry_without_schemas_gives_a_document_that_says_so() { + let empty = format!("{HEADER}\nNo commands have parameter schemas.\n"); + assert_eq!(reference(&[]).unwrap(), empty); + assert_eq!(reference(&[spec("test.none", &["View"], None)]).unwrap(), empty); + let one = reference(&[spec("test.none", &[], None), spec("test.full", &[], Some(&FULL)), spec("test.other", &[], None)]).unwrap(); + assert_eq!(one, reference(&[spec("test.full", &[], Some(&FULL))]).unwrap()); + } + + /// A reference of exactly [`MAX_BYTES`] is returned; one byte more is an error naming the + /// command, with no part of the reference. + #[test] + fn a_reference_past_the_limit_is_an_error() { + let with = |summary: String| -> Result { + let schema: &'static ParamSchema = Box::leak(Box::new(ParamSchema::new(&[], Box::leak(summary.into_boxed_str())))); + reference(&[spec("test.big", &[], Some(schema))]) + }; + let base = with("x".into()).unwrap().len(); + let fits = with("x".repeat(MAX_BYTES - base + 1)).unwrap(); + assert_eq!(fits.len(), MAX_BYTES); + let e = with("x".repeat(MAX_BYTES - base + 2)).unwrap_err().to_string(); + assert!(e.contains(ID) && e.contains("larger than 1048576 bytes"), "{e}"); + assert!(e.len() < 200, "{e}"); + } + + /// An unknown key is refused with a parameter error naming it, and the command needs no + /// document. + #[test] + fn an_unknown_parameter_is_refused_naming_it() { + let mut s = Session::new(); + let e = s.execute(ID, &json!({"prefix": "view."})).unwrap_err().to_string(); + assert!(e.contains(ID) && e.contains("`prefix`"), "{e}"); + assert!(s.execute(ID, &json!([])).is_err()); + assert!(s.active().is_none()); + assert!(s.execute(ID, &json!({})).is_ok()); + } + + #[test] + fn escape_marks_each_punctuation_character_and_flattens_control_characters() { + assert_eq!(escape("a|b"), "a\\|b"); + assert_eq!(escape("WWW.x and w.x"), "WWW\\.x and w.x"); + assert_eq!(escape("é\u{7}\u{85}z"), "é z"); + assert_eq!(escape("1.5 > 1"), "1.5 \\> 1"); + } +} diff --git a/crates/engine/src/schemas.rs b/crates/engine/src/schemas.rs index a24613a07..61ec8f0fd 100644 --- a/crates/engine/src/schemas.rs +++ b/crates/engine/src/schemas.rs @@ -26,10 +26,10 @@ use crate::cmd::{CommandSpec, bad, command_specs}; use crate::{EngineError, Result}; /// The API level of the commands' parameters. -pub const API_VERSION: u32 = 1; +pub const API_VERSION: u32 = 2; /// [`schema_hash`] at each API level, oldest first; the last row is [`API_VERSION`]'s. -pub const SCHEMA_HASHES: &[(u32, u64)] = &[(1, 0x9cdb_1087_7074_855c)]; +pub const SCHEMA_HASHES: &[(u32, u64)] = &[(1, 0x9cdb_1087_7074_855c), (2, 0x0e31_5d96_7bc9_2653)]; /// The hash of every built-in command's schema ([`vectorcraft_apischema::hash`]). pub fn schema_hash() -> u64 { @@ -296,8 +296,12 @@ mod tests { ("view.capsLock", json!([])), ("view.saved.new", json!(5)), ("view.transparencyGrid", json!("on")), + ("engine.apiReference", json!({})), + ("engine.apiReference", json!(null)), + ("engine.apiReference", json!({"prefix": "view."})), + ("engine.apiReference", json!([])), ]; - assert_eq!(schemas().len(), 6); + assert_eq!(schemas().len(), 7); for (id, schema) in schemas() { let written = schema.to_json(); check_schema(&written, id).unwrap_or_else(|e| panic!("{e}\n{written:#}")); diff --git a/crates/mcp/src/resources.rs b/crates/mcp/src/resources.rs index 50792de6d..9f2f3b99e 100644 --- a/crates/mcp/src/resources.rs +++ b/crates/mcp/src/resources.rs @@ -18,6 +18,20 @@ pub const DOC_JSON_URI: &str = "vectorcraft://document/json"; /// The live command catalog, including enabled state. pub const COMMANDS_URI: &str = "vectorcraft://commands"; +/// The command API reference in Markdown (`engine.apiReference`). +pub const REFERENCE_URI: &str = "vectorcraft://commands/reference"; + +/// The media type of the JSON resources. +pub const JSON: &str = "application/json"; +/// The media type of the command API reference. +pub const MARKDOWN: &str = "text/markdown; charset=utf-8"; + +/// What `resources/read` returns for a URI: the text and its media type. +#[derive(Debug, PartialEq)] +pub struct Contents { + pub mime: &'static str, + pub text: String, +} /// Why a resource could not be read. The distinction matters: the server turns it into `-32002` /// (no such resource) or `-32602` (bad URI), and guessing from the message text would not hold. @@ -83,9 +97,24 @@ pub fn list() -> Value { {"uri": DOC_URI, "name": "document", "title": "Active document (summary)", "description": "Layer tree, artboards, selection and history of the active document (document.inspect)", "mimeType": "application/json"}, {"uri": DOC_JSON_URI, "name": "document-json", "title": "Active document (full model)", "description": "The complete document model as JSON (document.json)", "mimeType": "application/json"}, {"uri": COMMANDS_URI, "name": "commands", "title": "Command catalog", "description": "Command ids, labels, parameter descriptions and current enabled state", "mimeType": "application/json"}, + {"uri": REFERENCE_URI, "name": "command-reference", "title": "Command API reference", "description": "Markdown reference of the commands that have a parameter schema: types, ranges, units, defaults, API levels, what each writes and returns (engine.apiReference)", "mimeType": MARKDOWN}, ]}) } +/// `resources/read`: the text behind a resource URI, the command API reference as Markdown and +/// every other resource as indented JSON ([`read`]). +pub fn read_contents(b: &mut dyn Backend, uri: &str) -> Result { + if uri == REFERENCE_URI { + let mut reply = exec(b, "engine.apiReference").map_err(ReadError::Backend)?; + return match reply.get_mut("markdown").map(Value::take) { + Some(Value::String(text)) => Ok(Contents { mime: MARKDOWN, text }), + _ => Err(ReadError::Backend("engine.apiReference returned no `markdown` text".into())), + }; + } + let v = read(b, uri)?; + Ok(Contents { mime: JSON, text: serde_json::to_string_pretty(&v).unwrap_or_default() }) +} + /// `resources/templates/list`. pub fn templates() -> Value { let list: Vec = TEMPLATES @@ -246,7 +275,7 @@ mod tests { #[test] fn lists_are_well_formed() { let l = list(); - assert_eq!(l["resources"].as_array().map(Vec::len), Some(3)); + assert_eq!(l["resources"].as_array().map(Vec::len), Some(4)); let t = templates(); let items = t["resourceTemplates"].as_array().expect("templates"); assert_eq!(items.len(), TEMPLATES.len()); @@ -257,6 +286,51 @@ mod tests { } } + /// A backend that gives every call the same reply. + struct Fixed(Result); + + impl Backend for Fixed { + fn call(&mut self, _: &str, _: Value) -> Result { + self.0.clone() + } + fn has_ui(&self) -> bool { + false + } + fn describe(&self) -> String { + "fixed".into() + } + } + + /// The command reference is listed and read as Markdown, with the text `engine.apiReference` + /// returns, and with no document open; the other fixed resources still read as JSON. + #[test] + fn the_command_reference_reads_as_markdown() { + let listed = list(); + let entry = listed["resources"].as_array().and_then(|r| r.iter().find(|r| r["uri"] == REFERENCE_URI)).expect("listed"); + assert_eq!(entry["mimeType"], MARKDOWN); + assert_eq!(entry["name"], "command-reference"); + let mut b = Box::new(crate::Headless::new()); + let read = read_contents(b.as_mut(), REFERENCE_URI).unwrap(); + assert_eq!(read.mime, MARKDOWN); + let direct = b.call("engine.execute", json!({"command": "engine.apiReference", "params": {}})).unwrap(); + assert_eq!(read.text, direct["markdown"].as_str().unwrap()); + assert!(read.text.starts_with("# VectorCraft command API reference\n"), "{}", read.text); + assert_eq!(read_contents(b.as_mut(), REFERENCE_URI).unwrap(), read); + let commands = read_contents(b.as_mut(), COMMANDS_URI).unwrap(); + assert_eq!(commands.mime, JSON); + assert!(serde_json::from_str::(&commands.text).unwrap().is_array()); + assert!(matches!(read_contents(b.as_mut(), "vectorcraft://commands/nope"), Err(ReadError::NotFound(_)))); + } + + /// A reply without the Markdown text, and an app that cannot be reached, are backend errors. + #[test] + fn a_bad_or_failed_reference_reply_is_a_backend_error() { + for reply in [Ok(json!({})), Ok(json!({"markdown": 5})), Ok(json!("text")), Err("the app is not running".to_string())] { + let mut b = Fixed(reply.clone()); + assert!(matches!(read_contents(&mut b, REFERENCE_URI), Err(ReadError::Backend(_))), "{reply:?}"); + } + } + #[test] fn missing_and_unknown_uris_are_errors() { let mut b = Box::new(crate::Headless::with_document()); diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index c0139bccc..b1cfd3087 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -439,12 +439,12 @@ impl Server { } "resources/read" => { let uri = params.get("uri").and_then(Value::as_str).ok_or((INVALID_PARAMS, "missing `uri`".to_string()))?; - let v = resources::read(self.backend.as_mut(), uri).map_err(|e| match e { + let read = resources::read_contents(self.backend.as_mut(), uri).map_err(|e| match e { resources::ReadError::NotFound(uri) => (RESOURCE_NOT_FOUND, format!("resource not found: {uri}")), resources::ReadError::Invalid(why) => (INVALID_PARAMS, why), resources::ReadError::Backend(why) => (INTERNAL_ERROR, why), })?; - Ok(json!({"contents": [{"uri": uri, "mimeType": "application/json", "text": serde_json::to_string_pretty(&v).unwrap_or_default()}]})) + Ok(json!({"contents": [{"uri": uri, "mimeType": read.mime, "text": read.text}]})) } "completion/complete" => Ok(prompts::complete(self.backend.as_mut(), params)), "logging/setLevel" => { diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index e986deda0..c07aa9efc 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -304,7 +304,7 @@ fn headless_end_to_end() { // Resources. let v = rpc(&mut s, 10, "resources/list", json!({})); - assert_eq!(v["result"]["resources"].as_array().unwrap().len(), 3); + assert_eq!(v["result"]["resources"].as_array().unwrap().len(), 4); let v = rpc(&mut s, 11, "resources/read", json!({"uri": "vectorcraft://document"})); let text = v["result"]["contents"][0]["text"].as_str().unwrap(); assert!(serde_json::from_str::(text).unwrap()["layers"].is_array()); diff --git a/crates/mcp/tests/protocol_props.rs b/crates/mcp/tests/protocol_props.rs index 2eb3db647..f9dc065ff 100644 --- a/crates/mcp/tests/protocol_props.rs +++ b/crates/mcp/tests/protocol_props.rs @@ -290,8 +290,16 @@ fn resources_are_json() { let list = rpc(&mut s, 2, "resources/list", json!({})); for r in list["result"]["resources"].as_array().unwrap() { let v = rpc(&mut s, 3, "resources/read", json!({"uri": r["uri"]})); - let text = v["result"]["contents"][0]["text"].as_str().unwrap(); - serde_json::from_str::(text).unwrap(); + let content = &v["result"]["contents"][0]; + assert_eq!(content["mimeType"], r["mimeType"], "{}", r["uri"]); + assert_eq!(content["uri"], r["uri"]); + let text = content["text"].as_str().unwrap(); + if r["mimeType"] == "application/json" { + serde_json::from_str::(text).unwrap(); + } else { + assert_eq!(r["mimeType"], "text/markdown; charset=utf-8", "{}", r["uri"]); + assert!(text.starts_with("# ") && text.ends_with('\n'), "{}", r["uri"]); + } } } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d58bda3aa..1f3d5dc90 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -567,6 +567,7 @@ font-technology Variable Променливи Coming soon — tracked in the parity plan Очаквайте скоро — следи се в плана за съвместимост Command Click to Select Objects Behind Щракване с Command за избор на обекти отзад Command Dialog Диалогов прозорец за команда + Command API Reference Справочник за API на командите Community Общност Compatibility: Съвместимост: Compatible Gradient and Gradient Mesh Printing Съвместим печат на градиент и градиентна мрежа diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 4d57ec80b..b432ce9dd 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -965,6 +965,7 @@ lighting Light Svjetlo Color Picker… Birač boja… Coming soon — tracked in the parity plan Uskoro — prati se u planu usklađenosti Command Dialog Dijalog naredbe + Command API Reference Referenca API-ja naredbi Copy Selected Style Kopiraj odabrani stil Corners… Uglovi… Define Perspective Grid… Definiši mrežu perspektive… diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 5ed856ac9..b00834c4e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -794,6 +794,7 @@ antiAlias Sharp Nítid Command Ordre Command Click to Select Objects Behind Clic amb Ordre per seleccionar els objectes del darrere Command Dialog Diàleg d’ordres + Command API Reference Referència de l’API d’ordres Comment not closed Comentari no tancat Commenting, filling in form fields and signing Comentar, emplenar camps de formulari i signar Commits Commits @@ -5432,6 +5433,7 @@ digits Malayalam Malaiàlam @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out S’ha carregat “{name}”: s’ha omès {n} pas|S’ha carregat “{name}”: s’han omès {n} passos @msg not an action set file no és un fitxer de conjunt d’accions @msg the action set file is larger than 8 MiB el fitxer de conjunt d’accions supera els 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: la referència superaria els {MAX_BYTES} bytes @msg the action set file holds more than 1,000 actions el fitxer de conjunt d’accions conté més de 1.000 accions @msg the action set file needs a later version of VectorCraft el fitxer de conjunt d’accions necessita una versió més recent del VectorCraft @msg an action in the action set file has more than 10,000 steps una acció del fitxer de conjunt d’accions té més de 10.000 passos diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5792e9ca1..bfa4fc62e 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -985,6 +985,7 @@ lighting Light Světlo Color Picker… Výběr barvy… Coming soon — tracked in the parity plan Připravujeme — sledováno v plánu parity Command Dialog Dialogové okno příkazu + Command API Reference Reference API příkazů Copy Selected Style Kopírovat vybraný styl Corners… Rohy… Define Perspective Grid… Definovat perspektivní mřížku… @@ -4722,6 +4723,7 @@ imageTrace Outline Obrys @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Načteno „{name}“: vynechán {n} krok|Načteno „{name}“: vynechány {n} kroky|Načteno „{name}“: vynecháno {n} kroků @msg not an action set file není to soubor sady akcí @msg the action set file is larger than 8 MiB soubor sady akcí je větší než 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: reference by byla větší než {MAX_BYTES} bajtů @msg the action set file holds more than 1,000 actions soubor sady akcí obsahuje více než 1 000 akcí @msg the action set file needs a later version of VectorCraft soubor sady akcí vyžaduje novější verzi aplikace VectorCraft @msg an action in the action set file has more than 10,000 steps akce v souboru sady akcí má více než 10 000 kroků diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index bf2efab42..ceb4dc632 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -906,6 +906,7 @@ lighting Light Lys Color Picker… Farvevælger… Coming soon — tracked in the parity plan Kommer snart — følges i paritetsplanen Command Dialog Kommandodialog + Command API Reference API-reference for kommandoer Copy Selected Style Kopiér markeret typografi Corners… Hjørner… Define Perspective Grid… Definér perspektivgitter… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 37a1b3b44..a8a2e3e05 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -560,6 +560,7 @@ font-technology Variable Variabel Coming soon — tracked in the parity plan Demnächst verfügbar – im Paritätsplan vermerkt Command Click to Select Objects Behind Cmd-Klick wählt dahinterliegende Objekte aus Command Dialog Befehlsdialog + Command API Reference API-Referenz der Befehle Community Community Compatibility: Kompatibilität: Compatible Gradient and Gradient Mesh Printing Kompatibles Drucken von Verläufen und Verlaufsgittern @@ -4329,6 +4330,7 @@ theme Appearance Erscheinungsbild @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out „{name}“ geladen: {n} Schritt wurde ausgelassen|„{name}“ geladen: {n} Schritte wurden ausgelassen @msg not an action set file keine Aktionssatzdatei @msg the action set file is larger than 8 MiB die Aktionssatzdatei ist größer als 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: die Referenz wäre größer als {MAX_BYTES} Byte @msg the action set file holds more than 1,000 actions die Aktionssatzdatei enthält mehr als 1.000 Aktionen @msg the action set file needs a later version of VectorCraft die Aktionssatzdatei erfordert eine neuere Version von VectorCraft @msg an action in the action set file has more than 10,000 steps eine Aktion in der Aktionssatzdatei hat mehr als 10.000 Schritte diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 25baaaa17..30e5675bb 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -542,6 +542,7 @@ font-technology Variable Μεταβλητές Coming soon — tracked in the parity plan Προσεχώς — περιλαμβάνεται στο σχέδιο αντιστοιχίας λειτουργιών Command Click to Select Objects Behind Command-κλικ για επιλογή αντικειμένων από πίσω Command Dialog Παράθυρο εντολών + Command API Reference Αναφορά API εντολών Community Κοινότητα Compatibility: Συμβατότητα: Compatible Gradient and Gradient Mesh Printing Συμβατή εκτύπωση ντεγκραντέ και πλέγματος ντεγκραντέ @@ -4367,6 +4368,7 @@ digits Malayalam Μαλαγιάλαμ @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Φορτώθηκε το «{name}»: παραλείφθηκε {n} βήμα|Φορτώθηκε το «{name}»: παραλείφθηκαν {n} βήματα @msg not an action set file δεν είναι αρχείο συνόλου ενεργειών @msg the action set file is larger than 8 MiB το αρχείο συνόλου ενεργειών είναι μεγαλύτερο από 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: η αναφορά θα ήταν μεγαλύτερη από {MAX_BYTES} byte @msg the action set file holds more than 1,000 actions το αρχείο συνόλου ενεργειών περιέχει περισσότερες από 1.000 ενέργειες @msg the action set file needs a later version of VectorCraft το αρχείο συνόλου ενεργειών απαιτεί νεότερη έκδοση του VectorCraft @msg an action in the action set file has more than 10,000 steps μια ενέργεια στο αρχείο συνόλου ενεργειών έχει περισσότερα από 10.000 βήματα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index dca929ff2..4c12636f9 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -582,6 +582,7 @@ font-technology Variable Variables Coming soon — tracked in the parity plan Próximamente: incluido en el plan de paridad Command Click to Select Objects Behind Cmd+clic para seleccionar objetos de detrás Command Dialog Cuadro de diálogo de comando + Command API Reference Referencia de la API de comandos Community Comunidad Compatibility: Compatibilidad: Compatible Gradient and Gradient Mesh Printing Impresión compatible de degradados y mallas de degradado @@ -4413,6 +4414,7 @@ axis Both Ambos @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Se cargó “{name}”: se omitió {n} paso|Se cargó “{name}”: se omitieron {n} pasos @msg not an action set file no es un archivo de conjunto de acciones @msg the action set file is larger than 8 MiB el archivo de conjunto de acciones supera los 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: la referencia superaría los {MAX_BYTES} bytes @msg the action set file holds more than 1,000 actions el archivo de conjunto de acciones contiene más de 1000 acciones @msg the action set file needs a later version of VectorCraft el archivo de conjunto de acciones necesita una versión más reciente de VectorCraft @msg an action in the action set file has more than 10,000 steps una acción del archivo de conjunto de acciones tiene más de 10 000 pasos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 1a1058dd9..e9bb13d60 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -964,6 +964,7 @@ lighting Light Valo Color Picker… Värivalitsin… Coming soon — tracked in the parity plan Tulossa pian — seurataan yhdenmukaisuussuunnitelmassa Command Dialog Komentovalintaikkuna + Command API Reference Komentojen API-viite Copy Selected Style Kopioi valittu tyyli Corners… Kulmat… Define Perspective Grid… Määritä perspektiiviruudukko… @@ -4630,6 +4631,7 @@ imageTrace Outline Ääriviiva @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Ladattiin ”{name}”: jätettiin pois {n} vaihe|Ladattiin ”{name}”: jätettiin pois {n} vaihetta @msg not an action set file ei ole toimintosarjatiedosto @msg the action set file is larger than 8 MiB toimintosarjatiedosto on suurempi kuin 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: viite olisi suurempi kuin {MAX_BYTES} tavua @msg the action set file holds more than 1,000 actions toimintosarjatiedostossa on yli 1 000 toimintoa @msg the action set file needs a later version of VectorCraft toimintosarjatiedosto vaatii uudemman VectorCraft-version @msg an action in the action set file has more than 10,000 steps toimintosarjatiedoston toiminnossa on yli 10 000 vaihetta diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3b28b43f2..ddbcf4d19 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -583,6 +583,7 @@ font-technology Variable Variables Coming soon — tracked in the parity plan Bientôt disponible — prévu dans le plan de parité Command Click to Select Objects Behind Cmd+clic pour sélectionner les objets situés derrière Command Dialog Boîte de dialogue des commandes + Command API Reference Référence de l’API des commandes Community Communauté Compatibility: Compatibilité : Compatible Gradient and Gradient Mesh Printing Impression compatible des dégradés et des filets de dégradé @@ -4402,6 +4403,7 @@ axis Both Les deux @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out « {name} » chargé : {n} étape a été ignorée|« {name} » chargé : {n} étapes ont été ignorées @msg not an action set file ce n’est pas un fichier de jeu de scripts @msg the action set file is larger than 8 MiB le fichier de jeu de scripts dépasse 8 Mio +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}` : la référence dépasserait {MAX_BYTES} octets @msg the action set file holds more than 1,000 actions le fichier de jeu de scripts contient plus de 1 000 scripts @msg the action set file needs a later version of VectorCraft le fichier de jeu de scripts nécessite une version plus récente de VectorCraft @msg an action in the action set file has more than 10,000 steps un script du fichier de jeu de scripts compte plus de 10 000 étapes diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 4e9cd6f41..5971e539f 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -896,6 +896,7 @@ antiAlias Sharp शार्प Command कमांड Command Click to Select Objects Behind कमांड पीछे ऑब्जेक्ट्स का सिलेक्ट करने के लिए क्लिक करें Command Dialog कमान डायलॉग + Command API Reference कमांड API संदर्भ Comment not closed टिप्पणी बंद नहीं Commenting, filling in form fields and signing टिप्पणी करना, फॉर्म फ़ील्ड में भरना और हस्ताक्षर करना Commits कमिट्स @@ -7385,6 +7386,7 @@ antiAlias Sharp शार्प @msg the action has no steps कार्रवाई में कोई कदम नहीं है @msg the action set file holds more than 1,000 actions एक्शन सेट फ़ाइल 1,000 से अधिक कार्यों को रखती है @msg the action set file is larger than 8 MiB एक्शन सेट फ़ाइल 8 MiB से बड़ा है +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: संदर्भ {MAX_BYTES} बाइट से बड़ा होगा @msg the action set file needs a later version of VectorCraft एक्शन सेट फ़ाइल को वेक्टर क्रेफ्ट के बाद के संस्करण की आवश्यकता होती है @msg the action set ids are not a list कार्रवाई सेट ids एक सूची नहीं हैं @msg the action sets changed; nothing was deleted कार्रवाई सेट बदल गया; कुछ भी नहीं हटा दिया गया था diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8ed5bc017..578339347 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -760,6 +760,7 @@ font-technology Variable Változó Command Parancs Command Click to Select Objects Behind Cmd+kattintás a mögöttes objektumok kijelöléséhez Command Dialog Parancs párbeszédablaka + Command API Reference Parancsok API-referenciája Commenting, filling in form fields and signing Megjegyzések, űrlapmezők kitöltése és aláírás Commits Commitok Community Közösség @@ -5095,6 +5096,7 @@ imageTrace Outline Körvonal @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out „{name}” betöltve; kihagyott lépések: {n} @msg not an action set file nem műveletkészlet-fájl @msg the action set file is larger than 8 MiB a műveletkészlet-fájl nagyobb 8 MiB-nál +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: a referencia nagyobb lenne {MAX_BYTES} bájtnál @msg the action set file holds more than 1,000 actions a műveletkészlet-fájl több mint 1000 műveletet tartalmaz @msg the action set file needs a later version of VectorCraft a műveletkészlet-fájlhoz a VectorCraft újabb verziója kell @msg an action in the action set file has more than 10,000 steps a műveletkészlet-fájl egyik műveletében több mint 10 000 lépés van diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 459e32a60..05d08d4bd 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -906,6 +906,7 @@ lighting Light Cahaya Color Picker… Pemilih warna… Coming soon — tracked in the parity plan Segera hadir — dilacak dalam rencana paritas Command Dialog Dialog perintah + Command API Reference Referensi API perintah Copy Selected Style Salin gaya terpilih Corners… Sudut… Define Perspective Grid… Definisikan kisi perspektif… diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d08128427..f225c4d5a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -582,6 +582,7 @@ font-technology Variable Variabili Coming soon — tracked in the parity plan Prossimamente — previsto nel piano di parità Command Click to Select Objects Behind Cmd+clic per selezionare gli oggetti retrostanti Command Dialog Finestra dei comandi + Command API Reference Riferimento API dei comandi Community Comunità Compatibility: Compatibilità: Compatible Gradient and Gradient Mesh Printing Stampa compatibile di sfumature e trame sfumatura @@ -4455,6 +4456,7 @@ axis Both Entrambi @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Caricato “{name}”: {n} passaggio è stato omesso|Caricato “{name}”: {n} passaggi sono stati omessi @msg not an action set file non è un file di set di azioni @msg the action set file is larger than 8 MiB il file del set di azioni supera 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: il riferimento supererebbe {MAX_BYTES} byte @msg the action set file holds more than 1,000 actions il file del set di azioni contiene più di 1.000 azioni @msg the action set file needs a later version of VectorCraft il file del set di azioni richiede una versione più recente di VectorCraft @msg an action in the action set file has more than 10,000 steps un’azione nel file del set di azioni ha più di 10.000 passaggi diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 11d1b5ced..3d176d839 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1253,6 +1253,7 @@ antiAlias Sharp シャープ Coming soon — tracked in the parity plan 近日対応予定 Command Click to Select Objects Behind Command+クリックで背面のオブジェクトを選択 Command Dialog コマンドダイアログ + Command API Reference コマンド API リファレンス Community コミュニティ Compatibility: 互換性: Compatible Gradient and Gradient Mesh Printing 互換性のあるグラデーションとグラデーションメッシュのプリント @@ -4801,6 +4802,7 @@ swatch Bright Violet ビビッドバイオレット @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out 「{name}」を読み込みました。{n} 個のステップは省きました @msg not an action set file アクションセットファイルではありません @msg the action set file is larger than 8 MiB アクションセットファイルが 8 MiB を超えています +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: リファレンスが {MAX_BYTES} バイトを超えます @msg the action set file holds more than 1,000 actions アクションセットファイルのアクションが 1,000 個を超えています @msg the action set file needs a later version of VectorCraft このアクションセットファイルには新しいバージョンの VectorCraft が必要です @msg an action in the action set file has more than 10,000 steps アクションセットファイルに 10,000 ステップを超えるアクションがあります diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 63a17e969..2595036c2 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -858,6 +858,7 @@ lighting Light სინათლე Color Picker… ფერის ამრჩევი… Coming soon — tracked in the parity plan მალე — აღრიცხულია თანხვედრის გეგმაში Command Dialog ბრძანების დიალოგი + Command API Reference ბრძანებების API ცნობარი Copy Selected Style მონიშნული სტილის კოპირება Corners… კუთხეები… Define Perspective Grid… პერსპექტივის ბადის განსაზღვრა… @@ -4180,6 +4181,7 @@ font-technology Variable ცვლადი @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out ჩაიტვირთა „{name}“; გამოტოვებული ნაბიჯები: {n} @msg not an action set file ეს არ არის მოქმედებების ნაკრების ფაილი @msg the action set file is larger than 8 MiB მოქმედებების ნაკრების ფაილი 8 MiB-ზე დიდია +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: ცნობარი {MAX_BYTES} ბაიტზე დიდი იქნებოდა @msg the action set file holds more than 1,000 actions მოქმედებების ნაკრების ფაილი 1000-ზე მეტ მოქმედებას შეიცავს @msg the action set file needs a later version of VectorCraft მოქმედებების ნაკრების ფაილს VectorCraft-ის უფრო ახალი ვერსია სჭირდება @msg an action in the action set file has more than 10,000 steps მოქმედებების ნაკრების ფაილში ერთ-ერთ მოქმედებას 10 000-ზე მეტი ნაბიჯი აქვს diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 1463307af..0cc31464f 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1243,6 +1243,7 @@ antiAlias Sharp 날카롭게 Coming soon — tracked in the parity plan 곧 제공 예정 — 기능 동등성 계획에서 관리 중 Command Click to Select Objects Behind Command-클릭으로 뒤쪽 개체 선택 Command Dialog 명령 대화상자 + Command API Reference 명령 API 참조 Community 커뮤니티 Compatibility: 호환성: Compatible Gradient and Gradient Mesh Printing 호환 그레이디언트 및 그레이디언트 메시 인쇄 @@ -4728,6 +4729,7 @@ theme Appearance 외관 @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out “{name}”을(를) 불러왔습니다. 단계 {n}개를 제외했습니다 @msg not an action set file 액션 세트 파일이 아닙니다 @msg the action set file is larger than 8 MiB 액션 세트 파일이 8 MiB보다 큽니다 +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: 참조가 {MAX_BYTES}바이트보다 커집니다 @msg the action set file holds more than 1,000 actions 액션 세트 파일의 액션이 1,000개를 넘습니다 @msg the action set file needs a later version of VectorCraft 이 액션 세트 파일을 열려면 최신 버전의 VectorCraft가 필요합니다 @msg an action in the action set file has more than 10,000 steps 액션 세트 파일에 단계가 10,000개를 넘는 액션이 있습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index e512752ea..d38e11ef7 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -920,6 +920,7 @@ lighting Light Licht Color Picker… Kleurkiezer… Coming soon — tracked in the parity plan Binnenkort beschikbaar — opgenomen in het pariteitsplan Command Dialog Opdrachtdialoogvenster + Command API Reference API-naslag voor opdrachten Copy Selected Style Geselecteerde stijl kopiëren Corners… Hoeken… Define Perspective Grid… Perspectiefraster definiëren… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 5469364e6..f8c4a23f7 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -921,6 +921,7 @@ lighting Light Światło Color Picker… Próbnik kolorów… Coming soon — tracked in the parity plan Już wkrótce — ujęte w planie zgodności Command Dialog Okno polecenia + Command API Reference Dokumentacja API poleceń Copy Selected Style Kopiuj zaznaczony styl Corners… Narożniki… Define Perspective Grid… Zdefiniuj siatkę perspektywy… @@ -4545,6 +4546,7 @@ imageTrace Outline Kontur @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Wczytano „{name}”: pominięto {n} krok|Wczytano „{name}”: pominięto {n} kroki|Wczytano „{name}”: pominięto {n} kroków @msg not an action set file to nie jest plik zestawu operacji @msg the action set file is larger than 8 MiB plik zestawu operacji jest większy niż 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: dokumentacja byłaby większa niż {MAX_BYTES} bajtów @msg the action set file holds more than 1,000 actions plik zestawu operacji zawiera ponad 1000 operacji @msg the action set file needs a later version of VectorCraft plik zestawu operacji wymaga nowszej wersji programu VectorCraft @msg an action in the action set file has more than 10,000 steps operacja w pliku zestawu operacji ma ponad 10 000 kroków diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b126286a0..30a61dae3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -532,6 +532,7 @@ antiAlias Sharp Nítido Coming soon — tracked in the parity plan Em breve — rastreado no plano de paridade Command Click to Select Objects Behind Clique com Comando para Selecionar Objetos Atrás Command Dialog Diálogo de Comandos + Command API Reference Referência da API de Comandos Community Comunidade Compatibility: Compatibilidade: Compatible Gradient and Gradient Mesh Printing Impressão Compatível de Gradiente e Malha de Gradiente @@ -3985,6 +3986,7 @@ axis Both Ambos @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out “{name}” carregado: {n} etapa foi omitida|“{name}” carregado: {n} etapas foram omitidas @msg not an action set file não é um arquivo de conjunto de ações @msg the action set file is larger than 8 MiB o arquivo de conjunto de ações tem mais de 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: a referência teria mais de {MAX_BYTES} bytes @msg the action set file holds more than 1,000 actions o arquivo de conjunto de ações contém mais de 1.000 ações @msg the action set file needs a later version of VectorCraft o arquivo de conjunto de ações requer uma versão mais recente do VectorCraft @msg an action in the action set file has more than 10,000 steps uma ação do arquivo de conjunto de ações tem mais de 10.000 etapas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index d54e7d199..dd954cea0 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -580,6 +580,7 @@ font-technology Variable Вариативные Coming soon — tracked in the parity plan Скоро — отслеживается в плане развития Command Click to Select Objects Behind Cmd+щелчок для выбора объектов позади Command Dialog Диалоговое окно команды + Command API Reference Справочник по API команд Community Сообщество Compatibility: Совместимость: Compatible Gradient and Gradient Mesh Printing Совместимая печать градиентов и градиентных сеток @@ -4449,6 +4450,7 @@ axis Both Оба @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Загружено «{name}»: пропущен {n} шаг|Загружено «{name}»: пропущено {n} шага|Загружено «{name}»: пропущено {n} шагов @msg not an action set file это не файл набора действий @msg the action set file is larger than 8 MiB файл набора действий больше 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: справочник был бы больше {MAX_BYTES} байт @msg the action set file holds more than 1,000 actions файл набора действий содержит более 1000 действий @msg the action set file needs a later version of VectorCraft для файла набора действий нужна более новая версия VectorCraft @msg an action in the action set file has more than 10,000 steps в файле набора действий есть действие более чем из 10 000 шагов diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 2fe873c72..d83100b8c 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -569,6 +569,7 @@ font-technology Variable Variabla Coming soon — tracked in the parity plan Kommer snart — följs upp i paritetsplanen Command Click to Select Objects Behind Cmd-klicka för att markera objekt bakom Command Dialog Kommandodialog + Command API Reference API-referens för kommandon Community Gemenskap Compatibility: Kompatibilitet: Compatible Gradient and Gradient Mesh Printing Kompatibel utskrift av gradienter och gradientnät @@ -4317,6 +4318,7 @@ axis Both Båda @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Läste in ”{name}”: {n} steg utelämnades|Läste in ”{name}”: {n} steg utelämnades @msg not an action set file inte en fil med åtgärdsuppsättning @msg the action set file is larger than 8 MiB filen med åtgärdsuppsättningen är större än 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: referensen skulle bli större än {MAX_BYTES} byte @msg the action set file holds more than 1,000 actions filen med åtgärdsuppsättningen innehåller fler än 1 000 åtgärder @msg the action set file needs a later version of VectorCraft filen med åtgärdsuppsättningen kräver en nyare version av VectorCraft @msg an action in the action set file has more than 10,000 steps en åtgärd i filen med åtgärdsuppsättningen har fler än 10 000 steg diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e3f73973c..ba54e4058 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -563,6 +563,7 @@ font-technology Variable Değişken Coming soon — tracked in the parity plan Çok yakında — eşlik planında takip ediliyor Command Click to Select Objects Behind Komut Arkasındaki Nesneleri Seçmek İçin Tıklayın Command Dialog Komut İletişim Kutusu + Command API Reference Komut API Başvurusu Community Topluluk Compatibility: Uyumluluk: Compatible Gradient and Gradient Mesh Printing Uyumlu Degrade ve Degrade Mesh Baskı @@ -4177,6 +4178,7 @@ axis Both Her İkisi @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out “{name}” yüklendi: {n} adım atlandı @msg not an action set file bir eylem seti dosyası değil @msg the action set file is larger than 8 MiB eylem seti dosyası 8 MiB'den büyük +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: başvuru {MAX_BYTES} bayttan büyük olur @msg the action set file holds more than 1,000 actions eylem seti dosyası 1.000'den fazla eylem içeriyor @msg the action set file needs a later version of VectorCraft eylem seti dosyası VectorCraft'ın daha yeni bir sürümünü gerektiriyor @msg an action in the action set file has more than 10,000 steps eylem seti dosyasındaki bir eylemde 10.000'den fazla adım var diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a91939074..056bf92ec 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -576,6 +576,7 @@ font-technology Variable Варіативні Coming soon — tracked in the parity plan Незабаром відстежується в плані розвитку Command Click to Select Objects Behind Cmd+клацання для вибору об’єктів позаду Command Dialog Діалогове вікно команди + Command API Reference Довідник API команд Community Спільнота Compatibility: Сумісність: Compatible Gradient and Gradient Mesh Printing Сумісний друк градієнтів та градієнтних сіток @@ -4439,6 +4440,7 @@ axis Both Обидві @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Завантажено «{name}»: пропущено {n} крок|Завантажено «{name}»: пропущено {n} кроки|Завантажено «{name}»: пропущено {n} кроків @msg not an action set file це не файл набору дій @msg the action set file is larger than 8 MiB файл набору дій більший за 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: довідник був би більшим за {MAX_BYTES} байтів @msg the action set file holds more than 1,000 actions файл набору дій містить понад 1000 дій @msg the action set file needs a later version of VectorCraft для файлу набору дій потрібна новіша версія VectorCraft @msg an action in the action set file has more than 10,000 steps у файлі набору дій є дія з понад 10 000 кроків diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 8f0780005..624a2dbff 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -671,6 +671,7 @@ font-technology Variable Biến thiên Coming soon — tracked in the parity plan Sắp ra mắt — đang theo dõi trong kế hoạch đối chiếu tính năng Command Click to Select Objects Behind Command-bấm để chọn đối tượng phía sau Command Dialog Hộp thoại lệnh + Command API Reference Tài liệu tham khảo API lệnh Commits Commit Community Cộng đồng Company Công ty @@ -4748,6 +4749,7 @@ theme Appearance Giao diện @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out Đã tải “{name}”: bỏ qua {n} bước @msg not an action set file không phải tệp bộ hành động @msg the action set file is larger than 8 MiB tệp bộ hành động lớn hơn 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`: tài liệu tham khảo sẽ lớn hơn {MAX_BYTES} byte @msg the action set file holds more than 1,000 actions tệp bộ hành động chứa hơn 1.000 hành động @msg the action set file needs a later version of VectorCraft tệp bộ hành động cần phiên bản VectorCraft mới hơn @msg an action in the action set file has more than 10,000 steps một hành động trong tệp bộ hành động có hơn 10.000 bước diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 65a59cabe..501819665 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -528,6 +528,7 @@ antiAlias Sharp 锐利 Coming soon — tracked in the parity plan 即将推出 — 在对标计划中跟踪 Command Click to Select Objects Behind 按住 Command 单击选择后方对象 Command Dialog 命令对话框 + Command API Reference 命令 API 参考 Community 社区 Compatibility: 兼容性: Compatible Gradient and Gradient Mesh Printing 兼容渐变和渐变网格打印 @@ -4508,6 +4509,7 @@ imageTrace Outline 轮廓 @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out 已载入“{name}”:已略过 {n} 个步骤 @msg not an action set file 不是动作组文件 @msg the action set file is larger than 8 MiB 动作组文件大于 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`:参考将大于 {MAX_BYTES} 字节 @msg the action set file holds more than 1,000 actions 动作组文件包含超过 1,000 个动作 @msg the action set file needs a later version of VectorCraft 动作组文件需要更高版本的 VectorCraft @msg an action in the action set file has more than 10,000 steps 动作组文件中有动作超过 10,000 个步骤 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b16ad5eec..eb983db79 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -544,6 +544,7 @@ font-technology Variable 可變 Coming soon — tracked in the parity plan 即將推出 — 已列入功能對等計畫 Command Click to Select Objects Behind Cmd 加按一下可選取後方物件 Command Dialog 指令對話框 + Command API Reference 指令 API 參考 Community 社群 Compatibility: 相容性: Compatible Gradient and Gradient Mesh Printing 相容的漸層與漸層網格列印 @@ -2352,6 +2353,7 @@ file-filter Templates 範本 @msgplural Loaded “{name}”: {n} step was left out|Loaded “{name}”: {n} steps were left out 已載入「{name}」:已略過 {n} 個步驟 @msg not an action set file 不是動作組檔案 @msg the action set file is larger than 8 MiB 動作組檔案大於 8 MiB +@msg `{ID}`: the reference would be larger than {MAX_BYTES} bytes `{ID}`:參考將大於 {MAX_BYTES} 位元組 @msg the action set file holds more than 1,000 actions 動作組檔案包含超過 1,000 個動作 @msg the action set file needs a later version of VectorCraft 動作組檔案需要較新版本的 VectorCraft @msg an action in the action set file has more than 10,000 steps 動作組檔案中有動作超過 10,000 個步驟 diff --git a/docs/mcp.md b/docs/mcp.md index 1bc9abf74..4bf2e6c2b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -210,8 +210,10 @@ echo '{"jsonrpc":"2.0","id":1,"method":"prompts/get","params":{"name":"poster"," ### Resource templates -`vectorcraft://document` (summary), `vectorcraft://document/json` (the whole model), and -`vectorcraft://commands` (the live command catalog with enabled state) are the fixed resources. The templates read one thing at a time, which matters on a real document: the full model +`vectorcraft://document` (summary), `vectorcraft://document/json` (the whole model), +`vectorcraft://commands` (the live command catalog with enabled state) and `vectorcraft://commands/reference` (the +command API reference in Markdown, `text/markdown; charset=utf-8`; see [Parameter schemas](#parameter-schemas)) are +the fixed resources. The templates read one thing at a time, which matters on a real document: the full model is thousands of lines an agent pays for again on every change. | `uriTemplate` | Reads | @@ -748,6 +750,7 @@ Fixed resources and the four templates (same as [Resource templates](#resource-t | `vectorcraft://document` | `document.inspect` summary (JSON) | | `vectorcraft://document/json` | The complete document model (JSON) | | `vectorcraft://commands` | Live command catalog with enabled state | +| `vectorcraft://commands/reference` | The command API reference (`engine.apiReference`), as Markdown (`text/markdown; charset=utf-8`) | | `vectorcraft://object/{id}` | One layer or object (`document.node {id, summary: true}`) | | `vectorcraft://command/{id}` | One command's metadata and enablement | | `vectorcraft://effect/{id}` | One live effect with parameters and defaults | @@ -1385,7 +1388,7 @@ vectorcraft-cli run --cmd file.new --params '{"width":600,"height":200}' --cmd ### Parameter schemas Some commands have a machine-readable parameter schema: at present `view.capsLock`, `view.saved.new`, -`view.saved.edit`, `view.saved.list`, `view.transparencyGrid` and `view.snapToGlyph.options`. The engine's +`view.saved.edit`, `view.saved.list`, `view.transparencyGrid`, `view.snapToGlyph.options` and `engine.apiReference`. The engine's `engine.commands` list gives such a command a `schema` field: an object schema in the shape of JSON Schema 2020-12 (https://json-schema.org/draft/2020-12). Its root has `$schema` (the 2020-12 dialect), `description` when the command has a summary, and `deprecated: true` when the command is deprecated. The schema uses `type`, `properties`, `required` and @@ -1427,9 +1430,25 @@ path to a value inside a list or an object, and the document and its undo histor An omitted value with a `default` takes it (`rotation` 0 in `view.saved.new`); the journal keeps the params as they were given. Saved steps played from a journal or an action keep a key the schema does not name, unless its value nests -more than 16 levels deep, and the values of the parameters it names are checked as in a new call. The schemas of built-in commands belong to API level 1 -(`vectorcraft_engine::schemas::API_VERSION`); a change to any of them raises the level. Commands that plug-ins add have -no schema here. +more than 16 levels deep, and the values of the parameters it names are checked as in a new call. The schemas of built-in commands belong to API level 2 +(`vectorcraft_engine::schemas::API_VERSION`); a change to any of them raises the level. Level 2 added +`engine.apiReference`. Commands that plug-ins add have no schema here. + +`engine.apiReference {}` → `{markdown}` returns the command API reference, written from these schemas each time it +runs; no file of it is kept. It needs no open document and changes nothing. The reference has a `##` section for each +built-in command that has a schema, in registry order (the order `engine.commands` lists them), and leaves out every +command without one, plug-in commands included; `engine.commands` still lists those with their params text. A section +lists the command's label, menu path, API level, deprecation level, what it writes (`x-writes`), cost, summary and +return, then a table of its parameters in the schema's order with the columns Parameter, Type, Range, Unit, Default, +Required, Since, Deprecated and Description. A member of an object parameter is a row of its own after it, named with +dots and `[]` for a list (`edits[].name`). A range says whether each end is included ("more than 0", "at most 64"); a +default is either the JSON value the engine fills in, marked "(JSON)", or the note of a default the command works out; +the Description says when null reads as omitted and when the engine does not refuse a value of another type (a lenient +parameter). Text from a schema is escaped as Markdown text: each of `` \ | [ ] < > ` * _ ! & # ~ : @ `` and the `.` +after `www` get a backslash, and line breaks, tabs and other control characters become spaces, so a description cannot +end a table cell or make a link, an image or HTML. A reference larger than 1 MiB (1,048,576 bytes) is an error naming +`engine.apiReference`, and no part of it is returned. MCP serves the same text as the resource +`vectorcraft://commands/reference`. ## Checking scripts written for the reference app diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e331a2d31..826108b5f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -674,6 +674,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Architecture:** 19+ crates with enforced layering (`cargo xtask layers`). Every action is a command (~400 engine + ~50 UI). Undo is unlimited via structural sharing. `command.batch` runs several commands as one transaction. - **Parameter schemas:** a command can carry a machine-readable parameter schema (the new `vectorcraft-apischema` crate): each parameter's type, range, unit, whether it is required, its default, whether null counts as left out, and the API levels it was added and deprecated at. `engine.commands` lists the schema in the shape of a JSON Schema 2020-12 object schema, a test keeps the command's `params` text equal to the text the schema writes, and a new call has the params checked against it before the command runs (batch steps before the first step), with an error naming the parameter and no change to the document; saved steps keep a key the schema does not name, and their values are checked. The View commands `view.capsLock`, `view.saved.new`, `view.saved.edit`, `view.saved.list`, `view.transparencyGrid` and `view.snapToGlyph.options` have schemas. The schemas belong to API level 1 (`vectorcraft_engine::schemas::API_VERSION`), and a test fails when a schema changes without a new level. Tests: `crates/apischema/src/tests.rs` (with `conformance.rs`, a separate check of the JSON form against the JSON Schema 2020-12 rules), `schemas::tests` and `cmd::views::tests::each_view_command_refuses_a_bad_value_naming_the_parameter` in `vectorcraft-engine`. - **Input schemas in the command listings:** `engine.commands` and the MCP tools `command_list` and `list_commands` give each command with a parameter schema an `inputSchema` next to `schema`: the same schema with the root `type` `"object"`, the form an MCP tool's input schema takes ([MCP schema reference](https://modelcontextprotocol.io/specification/2026-07-28/schema)). A command without a schema lists neither, and `brief` leaves out both schemas with the `params` text. Tests: `schemas::tests::engine_commands_list_an_input_schema_with_an_object_root` and `schemas::tests::an_input_schema_keeps_deprecation` in `vectorcraft-engine`, and `tests::conventions::command_listings_carry_input_schemas` in `vectorcraft-mcp`, which check every listed input schema against the JSON Schema 2020-12 rules. +- **Command API reference:** `engine.apiReference` returns a Markdown reference written from the parameter schemas when it is asked for, and MCP serves the same text as the resource `vectorcraft://commands/reference` (`text/markdown; charset=utf-8`). Each command that has a schema gets a section in registry order with its label, menu path, API levels, what it writes, summary and return, and a table of its parameters (type, range with its ends marked included or not, unit, default, required, levels and description; members of objects as dotted rows). Commands without a schema, plug-in commands included, are left out. Schema text is escaped so it stays plain text in its table cell, the same build always returns the same bytes, and a reference past 1 MiB is an error rather than a cut-short text. The command's own schema raised the API level to 2. This is a VectorCraft documentation feature; no score changes. Tests: `cmd::apiref::tests` and `schemas::tests` in `vectorcraft-engine`, `resources::tests` in `vectorcraft-mcp`. - **Automation:** - Variables: Update Data Set writes what the art holds into the active data set for the variables that data set holds, and keeps the data set's value for a text, image or graph variable whose objects are locked or inside a locked layer or group; `dataset.update {all: true}` also adds the other variables that the art reports a value for. - Variables: `dataset.select`, `dataset.next` and `dataset.prev` rewrite type and apply the other values on hidden objects and in hidden layers and groups, which stay hidden unless a visibility value shows them, so a layer shown later holds the selected data set's text; objects that are locked or in a locked layer or group are skipped, except by a visibility value.