MCP: a parameter nested too deeply is refused as bad parameters

A value that nests past a command schema's 16-level limit was reported
as an internal error ("please report this bug"). It is now a parameter
error that names the value's path, like the schema's other refusals; a
schema that is wrong in itself is still an internal error.

The schema check now also refuses a list of ids or a point inside 16
lists, since every value of those types is a list and the value check
refuses a list at that depth. A color there still takes a string.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-12 16:45:18 -08:00
parent 4b344e91ce
commit 06db8adae9
27 changed files with 90 additions and 4 deletions
+7 -2
View File
@@ -250,7 +250,8 @@ impl ParamSchema {
/// What is wrong with the schema itself, or `None`: a parameter name that is empty, repeated
/// or holds `.`, `[` or `]`; a required parameter with a default; a default that isn't JSON or
/// doesn't pass its parameter's type; an enum with no names or a repeated name; a range with
/// an end that isn't finite or that no value can be in; nesting past [`MAX_DEPTH`].
/// an end that isn't finite or that no value can be in; nesting past [`MAX_DEPTH`], or a
/// [`Type::Ids`] or [`Type::Point`], whose values are all lists, at the nesting limit.
pub fn definition_problem(&self) -> Option<String> {
object_problem("", self.params, 0)
}
@@ -319,7 +320,9 @@ pub enum Problem {
Missing,
/// The value is not of the parameter's type: `expected` says what it must be.
WrongType { expected: String, got: String },
/// The schema nests deeper than [`MAX_DEPTH`] here ([`ParamSchema::definition_problem`]).
/// The value nests deeper than [`MAX_DEPTH`] here: a list given for a [`Type::Color`] at the
/// nesting limit, or any value under a schema that nests too deeply itself
/// ([`ParamSchema::definition_problem`]).
TooDeep,
}
@@ -779,6 +782,8 @@ fn type_problem(at: &str, ty: &Type, depth: usize) -> Option<String> {
Type::Object(params) => object_problem(at, params, depth + 1),
Type::Array(_) if depth >= MAX_DEPTH => Some(format!("`{at}` nests deeper than {MAX_DEPTH} levels")),
Type::Array(item) => type_problem(&format!("{at}[]"), item, depth + 1),
// Every value of these is a list, and a list at the nesting limit is refused.
Type::Ids | Type::Point if depth >= MAX_DEPTH => Some(format!("`{at}` nests deeper than {MAX_DEPTH} levels")),
_ => None,
}
}
+30
View File
@@ -156,6 +156,36 @@ fn id_lists_points_and_color_lists_count_toward_the_nesting_limit() {
assert!(check(MAX_DEPTH, &Type::Color, &json!("#ff0000")).is_ok());
}
/// `ty` inside `levels` lists.
fn in_lists(levels: usize, ty: Type) -> Type {
(0..levels).fold(ty, |inner, _| Type::Array(Box::leak(Box::new(inner))))
}
/// `v` inside `levels` lists.
fn wrapped(levels: usize, v: Value) -> Value {
(0..levels).fold(v, |inner, _| Value::Array(vec![inner]))
}
/// The schema check and the value check agree on the fixed-shape lists at the nesting limit: an id
/// list or a point there could take no value, so the schema is wrong in itself, and a color there
/// takes a string, while a color list is refused as nested too deeply, at its path.
#[test]
fn fixed_shape_lists_at_the_nesting_limit_follow_the_schema_check() {
for ty in [Type::Ids, Type::Point, Type::Color] {
assert_eq!(one(in_lists(MAX_DEPTH - 1, ty)).definition_problem(), None, "{ty:?}");
}
for ty in [Type::Ids, Type::Point] {
let problem = one(in_lists(MAX_DEPTH, ty)).definition_problem();
assert!(problem.as_deref().is_some_and(|p| p.contains("nests deeper")), "{ty:?}: {problem:?}");
}
let color = one(in_lists(MAX_DEPTH, Type::Color));
assert_eq!(color.definition_problem(), None);
assert_eq!(color.validate(&json!({"x": wrapped(MAX_DEPTH, json!("#ff0000"))})).unwrap(), None);
let e = color.validate(&json!({"x": wrapped(MAX_DEPTH, json!([1, 0, 0]))})).unwrap_err();
assert_eq!(e.problem, Problem::TooDeep);
assert_eq!(e.path, format!("x{}", "[0]".repeat(MAX_DEPTH)));
}
#[test]
fn a_saved_retired_key_past_the_nesting_limit_is_refused() {
let params = holding("older", nested_lists(500_000));
+25 -1
View File
@@ -69,7 +69,7 @@ fn param_error(id: &str, e: &ParamError) -> EngineError {
Problem::Unknown { keys } => bad(id, format!("unknown parameter `{path}`; it takes: {keys}", keys = keys.join(", "))),
Problem::Missing => bad(id, format!("missing `{path}`")),
Problem::WrongType { expected, got } => bad(id, format!("`{path}` must be {expected}, not {got}")),
Problem::TooDeep => EngineError::Internal { cmd: id.into(), msg: e.to_string() },
Problem::TooDeep => bad(id, format!("`{path}` is nested too deeply")),
}
}
@@ -246,6 +246,30 @@ mod tests {
assert!(s.doc().unwrap().history.undo.is_empty());
}
/// A value nested past the limit of a sound schema is a parameter error that names it, not an
/// internal error: a color list at the nesting limit, where a color string still passes.
#[test]
fn a_value_nested_too_deeply_is_a_parameter_error() {
let levels = vectorcraft_apischema::MAX_DEPTH;
let ty = (0..levels).fold(Type::Color, |inner, _| Type::Array(Box::leak(Box::new(inner))));
let params: &'static [Param] = Box::leak(Box::new([Param::new("fills", ty)]));
let schema = ParamSchema::new(params, "");
assert_eq!(schema.definition_problem(), None);
let wrapped = |v: Value| (0..levels).fold(v, |inner, _| Value::Array(vec![inner]));
assert!(schema.validate(&json!({"fills": wrapped(json!("#ff0000"))})).is_ok());
let e = schema.validate(&json!({"fills": wrapped(json!([1, 0, 0]))})).unwrap_err();
let path = format!("fills{}", "[0]".repeat(levels));
match param_error("view.synthetic", &e) {
EngineError::BadParams { cmd, msg } => {
assert_eq!(cmd, "view.synthetic");
assert_eq!(msg, format!("`{path}` is nested too deeply"));
}
other => panic!("not a parameter error: {other}"),
}
let shown = param_error("view.synthetic", &e).to_string();
assert!(shown.starts_with("invalid parameters for `view.synthetic`") && !shown.contains("report this bug"), "{shown}");
}
/// Each command's JSON schema follows the 2020-12 rules, and a validator of those rules, which
/// shares no code with the writer, gives the same answer as the schema check. None of these
/// schemas has a lenient parameter.
+1
View File
@@ -6103,6 +6103,7 @@ digits Malayalam Malaiàlam
@msg unknown parameter `{path}`; it takes: {keys} paràmetre desconegut `{path}`; admet: {keys}
@msg missing `{path}` falta `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` ha de ser {expected}, no {got}
@msg `{path}` is nested too deeply `{path}` està imbricat massa profundament
@msg unknown preference `{key}` preferència desconeguda `{key}`
@msg Preferences imported. S’han importat les preferències.
@msg not a preferences file: {e} no és un fitxer de preferències: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5794,6 +5794,7 @@ mark-style Japanese Japonské
@msg unknown parameter `{path}`; it takes: {keys} neznámý parametr `{path}`; přijímá: {keys}
@msg missing `{path}` chybí `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` musí být {expected}, nikoli {got}
@msg `{path}` is nested too deeply `{path}` je vnořený příliš hluboko
Load EPS into Clipboard Načíst EPS do schránky
@msg an image of {w} × {h} pixels is too large to draw obraz o velikosti {w} × {h} obrazových bodů je příliš velký na vykreslení
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: není obraz PNG, JPEG, WebP, TIFF ani BMP
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5369,6 +5369,7 @@ lighting Light Licht
@msg unknown parameter `{path}`; it takes: {keys} unbekannter Parameter `{path}`; akzeptiert werden: {keys}
@msg missing `{path}` `{path}` fehlt
@msg `{path}` must be {expected}, not {got} `{path}` muss {expected} sein, nicht {got}
@msg `{path}` is nested too deeply `{path}` ist zu tief verschachtelt
Load EPS into Clipboard EPS in die Zwischenablage laden
@msg an image of {w} × {h} pixels is too large to draw ein Bild mit {w} × {h} Pixeln ist zu groß, um gezeichnet zu werden
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: kein PNG-, JPEG-, WebP-, TIFF- oder BMP-Bild
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5153,6 +5153,7 @@ digits Malayalam Μαλαγιάλαμ
@msg unknown parameter `{path}`; it takes: {keys} άγνωστη παράμετρος `{path}`· δέχεται: {keys}
@msg missing `{path}` λείπει το `{path}`
@msg `{path}` must be {expected}, not {got} Το `{path}` πρέπει να είναι {expected}, όχι {got}
@msg `{path}` is nested too deeply Το `{path}` έχει υπερβολικά βαθιά ένθεση
1 Layer 1 επίπεδο
1 bit 1 bit
16 Colors 16 χρώματα
Can't render this file because it is too large.
+1
View File
@@ -5735,6 +5735,7 @@ mark-style Japanese Japonesas
@msg unknown parameter `{path}`; it takes: {keys} parámetro desconocido `{path}`; acepta: {keys}
@msg missing `{path}` falta `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` debe ser {expected}, no {got}
@msg `{path}` is nested too deeply `{path}` está anidado a demasiada profundidad
Load EPS into Clipboard Cargar EPS en el portapapeles
@msg an image of {w} × {h} pixels is too large to draw una imagen de {w} × {h} píxeles es demasiado grande para dibujarse
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: no es una imagen PNG, JPEG, WebP, TIFF ni BMP
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5676,6 +5676,7 @@ mark-style Japanese Japanilainen
@msg unknown parameter `{path}`; it takes: {keys} tuntematon parametri `{path}`; parametrit: {keys}
@msg missing `{path}` `{path}` puuttuu
@msg `{path}` must be {expected}, not {got} `{path}` on oltava {expected}, ei {got}
@msg `{path}` is nested too deeply `{path}` on liian syvällä sisäkkäin
Load EPS into Clipboard Lataa EPS leikepöydälle
@msg an image of {w} × {h} pixels is too large to draw kuva, jonka koko on {w} × {h} pikseliä, on liian suuri piirrettäväksi
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: ei ole PNG-, JPEG-, WebP-, TIFF- tai BMP-kuva
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5364,6 +5364,7 @@ lighting Light Lumière
@msg unknown parameter `{path}`; it takes: {keys} paramètre inconnu `{path}` ; il accepte : {keys}
@msg missing `{path}` `{path}` manquant
@msg `{path}` must be {expected}, not {got} `{path}` doit être {expected}, pas {got}
@msg `{path}` is nested too deeply `{path}` est imbriqué trop profondément
1 Layer 1 calque
1 bit 1 bit
16 Colors 16 couleurs
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -8144,6 +8144,7 @@ antiAlias Sharp शार्प
@msg unknown parameter `{path}`; it takes: {keys} अज्ञात पैरामीटर `{path}`; यह लेता है: {keys}
@msg missing `{path}` लापता `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` {expected} होना चाहिए, {got} नहीं
@msg `{path}` is nested too deeply `{path}` बहुत गहराई तक नेस्ट किया गया है
@msg unknown PDF preset `{name}` (presets: {_1}) अज्ञात PDF प्रीसेट `{name}` (presets: {_1})
@msg unknown pieLegend `{v}` (none, standard or wedges) अज्ञात pieLegend `{v}` (one, मानक या wedges)
@msg unknown piePosition `{v}` (even, ratio or stacked) अज्ञात piePosition `{v}` (यहां तक कि अनुपात या स्टैक्ड)
Can't render this file because it is too large.
+1
View File
@@ -5777,6 +5777,7 @@ imageTrace Outline Körvonal
@msg unknown parameter `{path}`; it takes: {keys} ismeretlen paraméter: `{path}`; ezeket fogadja el: {keys}
@msg missing `{path}` hiányzik: `{path}`
@msg `{path}` must be {expected}, not {got} `{path}`: {expected} kell, nem ez: {got}
@msg `{path}` is nested too deeply `{path}` túl mélyen van egymásba ágyazva
Load EPS into Clipboard EPS betöltése a vágólapra
@msg an image of {w} × {h} pixels is too large to draw egy {w} × {h} pixeles kép túl nagy a kirajzoláshoz
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: nem PNG, JPEG, WebP, TIFF vagy BMP kép
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5411,6 +5411,7 @@ lighting Light Luce
@msg unknown parameter `{path}`; it takes: {keys} parametro sconosciuto `{path}`; accetta: {keys}
@msg missing `{path}` manca `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` deve essere {expected}, non {got}
@msg `{path}` is nested too deeply `{path}` è annidato troppo in profondità
Load EPS into Clipboard Carica EPS negli appunti
@msg an image of {w} × {h} pixels is too large to draw un'immagine di {w} × {h} pixel è troppo grande per essere disegnata
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: non è un'immagine PNG, JPEG, WebP, TIFF o BMP
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5453,6 +5453,7 @@ lighting Light ライト
@msg unknown parameter `{path}`; it takes: {keys} `{path}` は不明なパラメーターです。使えるパラメーター: {keys}
@msg missing `{path}` `{path}` が指定されていません
@msg `{path}` must be {expected}, not {got} `{path}` は {expected} にしてください ({got} は使えません)
@msg `{path}` is nested too deeply `{path}` の入れ子が深すぎます
Load EPS into Clipboard EPS をクリップボードに読み込む
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} ピクセルの画像は大きすぎて描画できません
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: PNG、JPEG、WebP、TIFF、BMP のいずれの画像でもありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5161,6 +5161,7 @@ font-technology Variable ცვლადი
@msg unknown parameter `{path}`; it takes: {keys} უცნობი პარამეტრი `{path}`; იღებს: {keys}
@msg missing `{path}` აკლია `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` უნდა იყოს {expected} და არა {got}
@msg `{path}` is nested too deeply `{path}` ზედმეტად ღრმადაა ჩადგმული
file-filter All readable files ყველა წაკითხვადი ფაილი
file-filter All placeable files ყველა განთავსებადი ფაილი
file-filter Swatch libraries ფერის ნიმუშების ბიბლიოთეკები
Can't render this file because it is too large.
+1
View File
@@ -5372,6 +5372,7 @@ lighting Light 광원
@msg unknown parameter `{path}`; it takes: {keys} 알 수 없는 매개변수 `{path}`: 받는 매개변수: {keys}
@msg missing `{path}` `{path}`이(가) 없습니다
@msg `{path}` must be {expected}, not {got} `{path}`은(는) {expected}이어야 합니다({got} 사용 불가)
@msg `{path}` is nested too deeply `{path}`이(가) 너무 깊게 중첩되어 있습니다
Load EPS into Clipboard EPS를 클립보드로 불러오기
@msg an image of {w} × {h} pixels is too large to draw {w} × {h}픽셀 이미지는 너무 커서 그릴 수 없습니다
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: PNG, JPEG, WebP, TIFF 또는 BMP 이미지가 아닙니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5633,6 +5633,7 @@ mark-style Japanese Japońskie
@msg unknown parameter `{path}`; it takes: {keys} nieznany parametr `{path}`; przyjmuje: {keys}
@msg missing `{path}` brak `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` musi być {expected}, a nie {got}
@msg `{path}` is nested too deeply `{path}` jest zagnieżdżone zbyt głęboko
Load EPS into Clipboard Wczytaj EPS do schowka
@msg an image of {w} × {h} pixels is too large to draw obraz o wymiarach {w} × {h} pikseli jest zbyt duży, aby go narysować
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: to nie jest obraz PNG, JPEG, WebP, TIFF ani BMP
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4667,6 +4667,7 @@ axis Both Ambos
@msg unknown parameter `{path}`; it takes: {keys} parâmetro desconhecido `{path}`; aceita: {keys}
@msg missing `{path}` falta `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` deve ser {expected}, não {got}
@msg `{path}` is nested too deeply `{path}` está aninhado com profundidade excessiva
Load EPS into Clipboard Carregar EPS na Área de Transferência
Drawer menu Menu da gaveta
Toolbar: Show Controls Barra de ferramentas: Mostrar controles
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -5397,6 +5397,7 @@ lighting Light Свет
@msg unknown parameter `{path}`; it takes: {keys} неизвестный параметр `{path}`; принимает: {keys}
@msg missing `{path}` отсутствует `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` должно быть {expected}, а не {got}
@msg `{path}` is nested too deeply `{path}` вложено слишком глубоко
Load EPS into Clipboard Загрузить EPS в буфер обмена
@msg an image of {w} × {h} pixels is too large to draw изображение размером {w} × {h} пикселей слишком велико для отрисовки
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: это не изображение PNG, JPEG, WebP, TIFF или BMP
Can't render this file because it is too large.
+1
View File
@@ -5265,6 +5265,7 @@ lighting Light Ljus
@msg unknown parameter `{path}`; it takes: {keys} okänd parameter `{path}`; den tar: {keys}
@msg missing `{path}` `{path}` saknas
@msg `{path}` must be {expected}, not {got} `{path}` måste vara {expected}, inte {got}
@msg `{path}` is nested too deeply `{path}` är för djupt kapslat
1 Layer 1 lager
1 bit 1 bit
16 Colors 16 färger
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -5060,6 +5060,7 @@ lighting Light Işık
@msg unknown parameter `{path}`; it takes: {keys} bilinmeyen parametre `{path}`; şunları alır: {keys}
@msg missing `{path}` `{path}` eksik
@msg `{path}` must be {expected}, not {got} `{path}` {expected} olmalıdır, {got} değil
@msg `{path}` is nested too deeply `{path}` çok derin iç içe geçmiş
1 Layer 1 Katman
1 bit 1 bit
16 Colors 16 Renk
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -5391,6 +5391,7 @@ lighting Light Світло
@msg unknown parameter `{path}`; it takes: {keys} невідомий параметр `{path}`; приймає: {keys}
@msg missing `{path}` відсутнє `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` має бути {expected}, а не {got}
@msg `{path}` is nested too deeply `{path}` вкладено надто глибоко
Load EPS into Clipboard Завантажити EPS у буфер обміну
@msg an image of {w} × {h} pixels is too large to draw зображення розміром {w} × {h} пікселів завелике для відображення
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}: це не зображення PNG, JPEG, WebP, TIFF чи BMP
Can't render this file because it is too large.
+1
View File
@@ -5305,6 +5305,7 @@ theme Appearance Giao diện
@msg unknown parameter `{path}`; it takes: {keys} tham số không xác định `{path}`; nhận: {keys}
@msg missing `{path}` thiếu `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` phải là {expected}, không phải {got}
@msg `{path}` is nested too deeply `{path}` lồng nhau quá sâu
@msg unknown preference `{key}` tùy chọn không xác định `{key}`
@msg Preferences imported. Đã nhập tùy chọn.
@msg not a preferences file: {e} không phải tệp tùy chọn: {e}
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -5667,6 +5667,7 @@ lighting Light 光源
@msg unknown parameter `{path}`; it takes: {keys} 未知参数 `{path}`;它接受:{keys}
@msg missing `{path}` 缺少 `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` 必须是 {expected},而不是 {got}
@msg `{path}` is nested too deeply `{path}` 嵌套过深
Load EPS into Clipboard 将 EPS 载入剪贴板
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的图像过大,无法绘制
@msg the JPEG XL file is cut short JPEG XL 文件不完整
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5499,6 +5499,7 @@ lighting Light 光源
@msg unknown parameter `{path}`; it takes: {keys} 未知的參數 `{path}`;它接受:{keys}
@msg missing `{path}` 未提供 `{path}`
@msg `{path}` must be {expected}, not {got} `{path}` 必須是 {expected},而不是 {got}
@msg `{path}` is nested too deeply `{path}` 巢狀層級過深
Load EPS into Clipboard 將 EPS 載入剪貼簿
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的影像過大,無法繪製
@msg {path}: not a PNG, JPEG, WebP, TIFF or BMP image {path}:不是 PNG、JPEG、WebP、TIFF 或 BMP 影像
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -1784,6 +1784,11 @@ path to a value inside a list or an object, and the document and its undo histor
→ invalid parameters for `view.saved.edit`: `edits[0].delete` must be true or false, not 1
```
A schema nests at most 16 lists and objects. A value that would go past that depth is refused with the same kind of
parameter error, ``invalid parameters for `<id>`: `<path>` is nested too deeply``, where `<path>` is the path to the
value. A schema that is wrong in itself, such as one that nests past 16 levels or puts a list of ids or a point
inside 16 lists, is an internal error in the command, and the command does not run.
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 3
+1 -1
View File
@@ -719,7 +719,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Linux file associations:** the desktop entry lists the MIME type of every format File › Open reads but PSD, PSB, `.ps` PostScript, Targa, PCX, Pixar, JPEG 2000, HEIC/HEIF, JPEG XL and HP-GL, `.ai`, `.ait` and EPS included, and the MIME package declares the types shared-mime-info lacks (VectorCraft documents and templates, `.ait`, Affinity), so file managers list VectorCraft under Open With for them, as Finder does on macOS. `the_linux_desktop_entry_opens_every_readable_format` in `apps/vectorcraft/src/tests.rs` checks both files against `OPEN_EXTS`. On Debian 13, Ubuntu 24.04 and Fedora 42, `desktop-file-validate` accepts the desktop entry, `update-mime-database` accepts the MIME package, and `gio` registers the app for every one of these types.
- **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`.
- **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 (a value nested past the schema's 16 levels is such a parameter error, and only a schema that is wrong in itself is an internal error); 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:**