fix(mcp): emit numeric gap/padding in system-mode builders
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.
This commit is contained in:
parent
05e1e4e2d8
commit
ebcb85a569
|
|
@ -139,7 +139,10 @@ fn build_toast(args: &BTreeMap<String, String>, theme_aware: bool) -> Result<Val
|
|||
// fontSize is a strict-number schema slot — emit 14 in every mode
|
||||
// (the TS `$type-body-size` ref resolves to the same default at
|
||||
// render time; see the fidelity caveat on `build_modal_shell`).
|
||||
// gap accepts an expression ref, so system mode keeps `$spacing-2`.
|
||||
// gap must ALSO stay numeric: the layout chain zeroes Expression
|
||||
// gap (jian-core `container_to_style` / op-pen-loader `gap_value`),
|
||||
// so a `$spacing-2` ref would collapse the pill. 8 equals the
|
||||
// semantic palette's spacing-2 value.
|
||||
let (pill_fill, fg, font_size, gap) = if theme_aware {
|
||||
match theme {
|
||||
"dark" => (json!("#F1F5F9"), json!("#0F172A"), json!(14), json!(8)),
|
||||
|
|
@ -147,7 +150,7 @@ fn build_toast(args: &BTreeMap<String, String>, theme_aware: bool) -> Result<Val
|
|||
json!("$color-text-primary"),
|
||||
json!("$color-surface"),
|
||||
json!(14),
|
||||
json!("$spacing-2"),
|
||||
json!(8),
|
||||
),
|
||||
_ => (json!("#111827"), json!("#FFFFFF"), json!(14), json!(8)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,9 +107,12 @@ fn shell_theme_colors(theme_aware: bool, args: &BTreeMap<String, String>) -> 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<String, String>,
|
||||
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::<f64>().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!({
|
||||
|
|
|
|||
Loading…
Reference in a new issue