From ebcb85a569bdea0a9146ba7da47cf9d5c7875d3a Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 13 Jun 2026 01:05:46 +0800 Subject: [PATCH] fix(mcp): emit numeric gap/padding in system-mode builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit theme=system audit (D2 prerequisite): the schema accepts $spacing-* expression refs in gap/padding, but both Rust layout paths zero them out — jian-core container_to_style and op-pen-loader gap_value fall through Expression to 0 — so a ref collapses the layout instead of resolving. Exactly three emission sites existed: modal_shell's card padding ($spacing-5) and gap ($spacing-3), and toast's pill gap ($spacing-2). Emit the palette-equal numbers (24/12/8) in every mode, same pattern as the numeric font-size toast fix (011f4104). A new catalog-wide invariant test builds all kinds with theme=system and rejects any expression gap/padding, so future ports can't reintroduce the class. Color $refs are unaffected — scene_vars resolves fill and stroke refs at paint. --- .../src/element_feedback_alias_builders.rs | 7 ++- crates/op-mcp/src/element_manifest.rs | 47 +++++++++++++++++++ crates/op-mcp/src/element_ported_shells.rs | 22 ++++----- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/op-mcp/src/element_feedback_alias_builders.rs b/crates/op-mcp/src/element_feedback_alias_builders.rs index 2a1d117c2..57289062c 100644 --- a/crates/op-mcp/src/element_feedback_alias_builders.rs +++ b/crates/op-mcp/src/element_feedback_alias_builders.rs @@ -139,7 +139,10 @@ fn build_toast(args: &BTreeMap, theme_aware: bool) -> Result (json!("#F1F5F9"), json!("#0F172A"), json!(14), json!(8)), @@ -147,7 +150,7 @@ fn build_toast(args: &BTreeMap, theme_aware: bool) -> Result (json!("#111827"), json!("#FFFFFF"), json!(14), json!(8)), } diff --git a/crates/op-mcp/src/element_manifest.rs b/crates/op-mcp/src/element_manifest.rs index d93292df2..437ad255e 100644 --- a/crates/op-mcp/src/element_manifest.rs +++ b/crates/op-mcp/src/element_manifest.rs @@ -745,4 +745,51 @@ mod tests { failures.join("\n") ); } + + /// 收集子树里所有布局层不解析的 expression gap/padding。Rust 布局链 + /// 把 Expression 归零(jian-core `container_to_style` 与 op-pen-loader + /// `gap_value` 的 `_ => 0` 臂),所以任何 `$spacing-*` ref 都等于 + /// gap/padding 直接消失。 + fn collect_expression_layout_refs(node: &PenNode, kind: &str, hits: &mut Vec) { + use jian_ops_schema::node::base::NumberOrExpression; + use jian_ops_schema::node::container::Padding; + let (container, children) = match node { + PenNode::Frame(f) => (Some(&f.container), f.children.as_deref()), + PenNode::Group(g) => (Some(&g.container), g.children.as_deref()), + PenNode::Rectangle(r) => (Some(&r.container), None), + _ => (None, None), + }; + if let Some(c) = container { + if let Some(NumberOrExpression::Expression(expr)) = c.gap.as_ref() { + hits.push(format!("{kind}: gap = {expr:?}")); + } + if let Some(Padding::Expression(expr)) = c.padding.as_ref() { + hits.push(format!("{kind}: padding = {expr:?}")); + } + } + for child in children.unwrap_or(&[]) { + collect_expression_layout_refs(child, kind, hits); + } + } + + /// system 模式布局不变量:任何 kind 在 theme=system 下都不得发 + /// `$spacing-*` 等 expression gap/padding(2026-06-13 审计:曾有 + /// modal_shell 的 padding/gap 与 toast 的 gap 三处,system 模式下 + /// 卡片/胶囊直接坍缩)。颜色 `$color-*` ref 不在此列——scene_vars + /// 的 VariableTable 在 paint 期解析 fill/stroke ref。 + #[test] + fn system_mode_never_emits_expression_gap_or_padding() { + let mut hits = Vec::new(); + for kind in known_element_kinds() { + let built = build_element(&kind, &args(&[("theme", "system")])) + .unwrap_or_else(|err| panic!("{kind} must build in system mode: {err:?}")); + collect_expression_layout_refs(&built.node, &kind, &mut hits); + } + assert!( + hits.is_empty(), + "{} expression gap/padding emission(s) — the layout chain zeroes these:\n{}", + hits.len(), + hits.join("\n") + ); + } } diff --git a/crates/op-mcp/src/element_ported_shells.rs b/crates/op-mcp/src/element_ported_shells.rs index f104e69be..7b3761ec4 100644 --- a/crates/op-mcp/src/element_ported_shells.rs +++ b/crates/op-mcp/src/element_ported_shells.rs @@ -107,9 +107,12 @@ fn shell_theme_colors(theme_aware: bool, args: &BTreeMap) -> She /// numbers in every mode. In `system` mode the TS would emit `$type-*` refs /// for these; the Rust `fontSize`/`fontWeight`/`lineHeight` schema slots are /// strict numbers, and the refs resolve to these same defaults at render -/// time, so the rendered result is identical. Card padding DOES accept an -/// expression ref, so in system mode (no explicit override) we faithfully -/// emit `$spacing-5`; gap likewise emits `$spacing-3`. +/// time, so the rendered result is identical. Padding/gap must ALSO stay +/// numeric in system mode: the schema accepts `$spacing-*` expression refs +/// but the Rust layout chain zeroes them (jian-core `container_to_style` +/// and op-pen-loader `gap_value` both fall through Expression to 0), which +/// collapses the card. 24/12 equal the semantic palette's spacing-5/3 +/// values, so the rendered result matches a resolved ref. fn build_modal_shell( args: &BTreeMap, theme_aware: bool, @@ -129,19 +132,14 @@ fn build_modal_shell( }; let c = shell_theme_colors(theme_aware, args); - // Padding: explicit override (max(12, floor)) wins in every mode; absent + - // system → $spacing-5 ref; absent + light/dark → 24. + // Padding: explicit override (max(12, floor)) wins; absent → 24 in + // every mode (numeric — see the fidelity caveat above). let card_padding: Value = match args.get("card_padding").and_then(|v| v.parse::().ok()) { Some(raw) => json!(raw.floor().max(12.0)), - None if theme == "system" => json!("$spacing-5"), None => json!(24), }; - // gap: light/dark → 12, system → $spacing-3. - let card_gap: Value = if theme == "system" { - json!("$spacing-3") - } else { - json!(12) - }; + // gap: 12 in every mode (numeric — see the fidelity caveat above). + let card_gap: Value = json!(12); // Title: v0/light has no fill; v1 dark/system gets textPrimary. let mut title_node = json!({