From f5eff2c9a3089e20768aff91f80cc3ca8b4a6322 Mon Sep 17 00:00:00 2001 From: Fini Date: Mon, 27 Apr 2026 09:20:00 +0800 Subject: [PATCH] fix(ai-skills): restore element-tool arg-shape examples in elements-cookbook.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trimming the Minimal usage block out of elements.md (65c31832) lost arg-shape templates that the A/B harness depends on. Text-only LLMs in the treatment arm see only the markdown skill content — no MCP tools/list, no published inputSchema — so without the per-tool example payloads they have to guess argument names and break M1. Restore the full block as a sibling skill `elements-cookbook` (same hasMcpTools flag, slightly later priority so it loads alongside elements). Wire it through buildFullPrompt + the get_design_prompt(section='elements-cookbook') section map. Update the A/B harness to also strip the cookbook body when building the baseline prompt — leaving it in B would leak tool names + arg shapes back into the no-tools variant and re-bias the comparison. Both files now under the 800-line per-file ceiling. --- .../phases/generation/elements-cookbook.md | 393 ++++++++++++++++++ packages/pen-mcp/src/tools/design-prompt.ts | 7 +- scripts/ab-corpus/build-prompt.ts | 54 ++- 3 files changed, 436 insertions(+), 18 deletions(-) create mode 100644 packages/pen-ai-skills/skills/phases/generation/elements-cookbook.md diff --git a/packages/pen-ai-skills/skills/phases/generation/elements-cookbook.md b/packages/pen-ai-skills/skills/phases/generation/elements-cookbook.md new file mode 100644 index 000000000..3fb11132f --- /dev/null +++ b/packages/pen-ai-skills/skills/phases/generation/elements-cookbook.md @@ -0,0 +1,393 @@ +--- +name: elements-cookbook +description: Minimal-usage examples for every add_*_v0 element tool — arg-shape templates the A/B treatment prompt feeds to LLMs that don't have MCP tools/list (text-only callers). Pair with the elements skill for the decision tree + PREFER mappings; this file just shows what each tool's arguments look like end-to-end +phase: [generation] +trigger: + flags: [hasMcpTools] +priority: 15 +budget: 2400 +category: base +--- + + + +## Minimal usage + +``` +add_card_row_v0({ + items: [ + { title: "Hiit", subtitle: "30 min", icon: "flame" }, + { title: "Strength", subtitle: "45 min", icon: "dumbbell" }, + { title: "Yoga", subtitle: "25 min", icon: "leaf" }, + ], +}) + +add_metric_row_v0({ + items: [ + { label: "Steps", value: "8,432", icon: "activity" }, + { label: "Kcal", value: "512", icon: "flame" }, + { label: "Sleep", value: "7h 24m", icon: "moon" }, + ], +}) + +add_nav_chip_row_v0({ + items: [ + { label: "All", active: true }, // label-only chips OK + { label: "Videos", icon: "video" }, + { label: "Photos", icon: "image" }, + ], +}) + +add_bottom_nav_v0({ + items: [ + { title: "Home", icon: "home", active: true }, + { title: "Search", icon: "search" }, + { title: "Profile", icon: "user" }, + ], +}) + +add_activity_ring_v0({ + center_text: "8,432", + size: 80, + thickness: 8, +}) + +add_stat_grid_v0({ + items: [ + { value: "8,432", label: "Steps", icon: "activity" }, + { value: "512", label: "Kcal", icon: "flame" }, + { value: "7h", label: "Sleep", icon: "moon" }, + ], +}) + +add_section_header_v0({ + title: "Recent Workouts", + action: { label: "See all", icon: "arrow-right" }, +}) + +add_top_nav_bar_v0({ + title: "Settings", + leading_icon: "chevron-left", + trailing_icon: "more-vertical", +}) + +add_icon_button_v0({ + icon: "search", +}) + +add_divider_v0({}) // horizontal hairline (h=1 fill_container) +add_divider_v0({ orientation: "vertical" }) // vertical hairline + +add_badge_v0({ label: "NEW" }) + +add_avatar_v0({ initial: "JD", size: 56 }) // with initial +add_avatar_v0({ size: 40 }) // empty circle (fill via batch_design image later) + +add_text_button_v0({ label: "Get Started" }) +add_text_button_v0({ label: "Add item", leading_icon: "plus" }) + +add_heading_v0({ content: "Welcome back" }) // defaults to h2 (24/600/1.2) +add_heading_v0({ content: "Hero Headline", level: "display" }) // 48/700/1.0/-0.5 + +add_body_text_v0({ content: "Lorem ipsum dolor sit amet…" }) // Inter + 1.5 +add_body_text_v0({ content: "你好世界,这是一段中文正文。" }) // CJK: Inter + 1.6 + letterSpacing 0 +// body ALWAYS Inter; only HEADINGS dispatch to Noto Sans SC/JP/KR. + +add_icon_label_v0({ icon: "info", label: "Learn more" }) + +add_list_row_v0({ + title: "Notifications", + subtitle: "Push, email, and in-app", + leading_icon: "bell", + trailing_icon: "chevron-right", +}) + +add_search_bar_v0({ placeholder: "Search workouts..." }) + +add_form_field_v0({ label: "Email", placeholder: "you@example.com", leading_icon: "mail", required: true }) +add_form_field_v0({ label: "Password", leading_icon: "lock", trailing_icon: "eye", required: true }) +add_textarea_v0({ label: "Bio", placeholder: "Tell us about yourself", rows: 5 }) +add_textarea_v0({ label: "Feedback", rows: 4, required: true }) +add_skeleton_v0({}) // default 3 rows, last short +add_skeleton_v0({ rows: 5, row_height: 20, row_gap: 8 }) +add_select_v0({ label: "Country", value: "United States" }) +add_select_v0({ label: "Currency", placeholder: "Choose currency", required: true }) +add_chart_line_v0({ values: [2, 5, 3, 7, 4, 8, 6] }) +add_chart_pie_v0({ values: [40, 30, 20, 10], diameter: 200 }) +add_chart_pie_v0({ values: [1, 1, 1, 1], inner_radius_ratio: 0.5 }) // donut +add_image_placeholder_v0({ width: 320, height: 200, label: "Upload cover" }) +add_video_placeholder_v0({ width: 320, height: 180, label: "Coming soon" }) +add_comment_v0({ author: "Sarah", timestamp: "2h ago", body: "Looks great!", avatar_initial: "S" }) +add_modal_shell_v0({ title: "Confirm delete", subtitle: "This cannot be undone." }) +add_modal_shell_v1({ title: "Confirm delete", subtitle: "This cannot be undone.", theme: "dark" }) +add_modal_shell_v1({ title: "Confirm delete", theme: "system" }) // $color-* refs; requires seeded palette +add_status_badge_v0({ label: "Online", tone: "success" }) +add_status_badge_v0({ label: "Degraded", tone: "warning" }) +add_spinner_v0({ size: 40 }) +add_tooltip_v0({ text: "Click to delete" }) +add_metric_comparison_v0({ label: "Revenue", value: "$12,480", change: "8%", trend: "up" }) +add_notification_row_v0({ title: "New follower", body: "Alice started following you.", timestamp: "2m", unread: true, icon: "user-plus" }) + +add_switch_v0({}) // off (default) +add_switch_v0({ active: true }) // on — iOS green + +add_checkbox_v0({ label: "Accept terms" }) +add_checkbox_v0({ label: "Done", checked: true }) + +add_radio_v0({ label: "Small" }) +add_radio_v0({ label: "Medium", selected: true }) + +add_tabs_v0({ + items: [ + { label: "Overview", active: true }, + { label: "Details" }, + { label: "Reviews" }, + ], +}) + +add_segmented_control_v0({ + items: [ + { label: "Day" }, + { label: "Week", active: true }, + { label: "Month" }, + ], +}) + +add_empty_state_v0({ + title: "No items yet", + subtitle: "Add one to get started", + icon: "inbox", + cta_label: "Create new", +}) + +add_alert_v0({ message: "Your changes are saved.", icon: "check", dismissible: true }) + +add_toast_v0({ message: "Copied to clipboard", icon: "check" }) + +add_progress_bar_v0({ value: 60 }) // default bar_width=240 → fill=144 +add_progress_bar_v0({ value: 25, bar_width: 400 }) + +add_fab_v0({ icon: "plus" }) // default 56×56 +add_fab_v0({ icon: "edit", size: 40 }) + +add_breadcrumb_v0({ + items: [{ label: "Home" }, { label: "Settings" }, { label: "Billing" }], +}) + +add_stepper_v0({ total: 4, current: 1 }) // steps 1+2 done, 3+4 pending + +add_rating_stars_v0({ filled: 4 }) // 4/5 (default total) +add_carousel_dots_v0({ total: 5, current: 2 }) +add_link_v0({ label: "Learn more", trailing_icon: "arrow-right" }) +add_kbd_v0({ keys: ["Ctrl", "Shift", "P"] }) // default "+" separator +add_price_v0({ amount: "29", period: "/month" }) // currency defaults to "$" +add_quote_block_v0({ quote: "Stay hungry.", author: "Steve Jobs" }) +add_code_block_v0({ code: "const x = 1;", language: "typescript" }) +add_color_swatch_v0({ color: "#2563EB", label: "Primary" }) // hex OR $ref both accepted + +add_chart_bars_v0({ values: [4, 7, 3, 9, 5, 8, 6] }) // weekly steps skeleton +add_chart_bars_v0({ values: [10, 20], bar_width: 40, chart_height: 200 }) + +add_timeline_v0({ + items: [ + { title: "Order placed", subtitle: "10:42 AM", active: true }, + { title: "Preparing" }, + { title: "Out for delivery" }, + { title: "Delivered" }, + ], +}) + +add_calendar_grid_v0({}) // vanilla 30-day month, Sun-start +add_calendar_grid_v0({ days_in_month: 31, start_day_offset: 2, today: 15, selected_day: 22 }) + +add_pagination_v0({ total: 10, current: 5 }) // 1 … 4 [5] 6 … 10 +add_pagination_v0({ total: 3, current: 1, show_arrows: false }) // no prev/next + +add_faq_item_v0({ question: "Can I cancel anytime?" }) // collapsed +add_faq_item_v0({ question: "How do refunds work?", answer: "Email billing@…", expanded: true }) + +add_chip_input_v0({ label: "Tags", chips: ["design", "mobile", "a11y"], placeholder: "Add tag…" }) +add_chip_input_v0({ label: "Send to", chips: [], placeholder: "Enter emails" }) // empty + +add_empty_chart_v0({}) // default 320×200 bar-chart-2 icon +add_empty_chart_v0({ icon: "line-chart", title: "No trends yet", subtitle: "Come back after 7 days" }) + +add_action_menu_v0({ + items: [ + { label: "Edit", icon: "pencil" }, + { label: "Share", icon: "share" }, + { label: "Report", icon: "flag", divider_before: true }, + { label: "Delete", icon: "trash", destructive: true }, + ], +}) + +add_date_picker_v0({ label: "Due date" }) // placeholder state +add_date_picker_v0({ label: "Due date", value: "Jan 15, 2026", clearable: true }) // populated + +add_upload_dropzone_v0({}) // default 480×200 cloud icon +add_upload_dropzone_v0({ icon: "file-up", title: "Drop resume here", subtitle: "PDF or DOCX, max 5 MB" }) + +add_otp_input_v0({}) // 6 blank slots, first focused +add_otp_input_v0({ length: 6, digits: ["1","2","3"], focused_index: 3 }) // partial state, 4th slot focused +add_otp_input_v0({ length: 4, digits: ["1","2","3","4"] }) // 4-digit PIN, all filled + +add_attachment_row_v0({ filename: "report.pdf", size: "1.2 MB", icon: "file-text" }) +add_attachment_row_v0({ filename: "sealed.zip", icon: "file-archive", removable: false }) + +add_chat_bubble_v0({ message: "Hi! How can I help?", author: "Support", timestamp: "Just now" }) // left (from-others) +add_chat_bubble_v0({ message: "My order hasn't arrived.", side: "right", timestamp: "2m" }) // right (from-self) + +add_stat_card_v0({ label: "Monthly revenue", value: "$12.4k", icon: "trending-up", delta: "+8% vs last week", trend: "up" }) +add_stat_card_v0({ label: "Active users", value: "1,284", icon: "users" }) // no delta = static snapshot + +add_social_login_row_v0({ providers: [{ name: "Google" }, { name: "Apple" }, { name: "Microsoft" }] }) // stacked "Continue with X" buttons +add_social_login_row_v0({ providers: [{ name: "Google" }, { name: "GitHub" }, { name: "Slack" }], orientation: "horizontal" }) // compact icon-only row + +add_pricing_card_v0({ tier: "Starter", price: "0", period: "/month", features: ["3 projects", "Community support"], cta: "Get started" }) +add_pricing_card_v0({ tier: "Pro", price: "29", period: "/month", features: ["Unlimited projects", "Priority support", "Advanced analytics"], emphasis: "featured" }) // highlighted recommended tier +add_pricing_card_v0({ tier: "Enterprise", price: "Custom", features: ["Dedicated support", "SSO", "SLA"], cta: "Contact sales" }) + +add_toast_v1({ message: "Changes saved", icon: "check" }) // default light = v0 parity (dark pill) +add_toast_v1({ message: "Changes saved", icon: "check", theme: "dark" }) // inverted light pill for dark-surface screens +add_toast_v1({ message: "Changes saved", theme: "system" }) // $color-* refs — requires applySemanticPalette(doc) seeded + +add_range_slider_v0({ value: 60, label: "Volume", show_value: true, value_suffix: "%" }) +add_range_slider_v0({ value: 128, min: 0, max: 255, label: "Brightness", show_value: true }) + +add_empty_chart_v1({ icon: "line-chart" }) // default light = v0 parity +add_empty_chart_v1({ icon: "pie-chart", theme: "dark" }) // dashboard dark-mode "no data" slot +add_empty_chart_v1({ icon: "bar-chart-2", theme: "system" }) // $color-* refs — requires applySemanticPalette(doc) seeded + +add_phone_input_v0({ label: "Phone number", country_code: "+1", country_flag: "🇺🇸", required: true }) +add_phone_input_v0({ country_code: "+86", country_flag: "🇨🇳", value: "138 0000 0000" }) // populated state + +add_input_with_action_v0({ placeholder: "Enter your email", action_label: "Subscribe", leading_icon: "mail" }) // newsletter signup +add_input_with_action_v0({ placeholder: "Apply discount code", action_label: "Apply" }) // checkout discount +add_input_with_action_v0({ placeholder: "Type a message…", action_kind: "icon", action_icon: "send" }) // chat composer + +add_cookie_banner_v0({}) // default GDPR banner +add_cookie_banner_v0({ show_settings_link: true }) // with fine-grained consent link +add_cookie_banner_v0({ title: "Privacy choices", body: "We use cookies for analytics.", accept_label: "Allow", decline_label: "Decline" }) + +add_sidebar_nav_v0({ + title: "Acme", + items: [ + { label: "Dashboard", icon: "layout-dashboard", active: true }, + { label: "Customers", icon: "users" }, + { label: "Orders", icon: "shopping-cart" }, + { label: "Reports", icon: "bar-chart-3" }, + { label: "Settings", icon: "settings" }, + ], +}) +add_sidebar_nav_v0({ items: [{ label: "Home", icon: "home", active: true }, { label: "Profile", icon: "user" }] }) // titleless minimal + +add_avatar_group_v0({ + items: [ + { initial: "JD" }, + { initial: "SK" }, + { initial: "MN" }, + { initial: "AL" }, + { initial: "BR" }, + { initial: "CT" }, + { initial: "EF" }, + ], + max_visible: 4, +}) // renders 4 ringed circles + "+3" overflow tile +add_avatar_group_v0({ items: [{ initial: "A" }, { initial: "B" }, { initial: "C" }], size: 24 }) // compact 3-up + +add_data_table_row_v0({ + header: true, + columns: [ + { content: "Customer" }, + { content: "Email" }, + { content: "Status" }, + { content: "Total" }, + ], +}) +add_data_table_row_v0({ + columns: [ + { content: "Sarah Lee" }, + { content: "sarah@acme.com" }, + { content: "Active" }, + { content: "$1,240" }, + ], +}) +add_data_table_row_v0({ + selected: true, + columns: [ + { content: "Alex Park" }, + { content: "alex@acme.com" }, + { content: "Pending" }, + { content: "$680" }, + ], +}) // tinted hover/selected row + +add_tag_v0({ label: "Status: Active" }) // default tone, × visible +add_tag_v0({ label: "Plan: Pro", tone: "accent" }) // accent (blue) tone +add_tag_v0({ label: "Verified", tone: "success", removable: false }) // read-only success chip + +add_user_card_v0({ name: "Sarah Lee", role: "Senior Engineer", initial: "SL" }) + +add_profile_header_v0({ name: "Sarah Lee", handle: "@sarah", bio: "Designer at Acme. Cat-mom of two.", initial: "SL" }) + +add_drawer_shell_v0({ title: "Edit project", side: "right", width: 480 }) + +add_combobox_v0({ + label: "Country", + value: "Sing", + options: [ + { label: "Singapore", highlighted: true }, + { label: "Sweden" }, + { label: "Switzerland" }, + ], +}) + +add_toolbar_v0({ + items: [ + { icon: "bold", active: true }, + { icon: "italic" }, + { icon: "underline", divider_after: true }, + { icon: "list" }, + { icon: "list-ordered" }, + ], +}) + +add_callout_v0({ tone: "info", title: "Heads up", body: "This action affects every team member." }) +add_callout_v0({ tone: "warning", body: "Saving will overwrite the current draft." }) + +add_inline_action_v0({ message: "Comment deleted", action_label: "Undo", icon: "info" }) + +add_share_row_v0({ + targets: [ + { label: "Twitter", icon: "twitter" }, + { label: "Facebook", icon: "facebook" }, + { label: "Email", icon: "mail" }, + { label: "Copy", icon: "link" }, + ], +}) + +add_legend_item_v0({ label: "Revenue", color: "#2563EB", value: "$12,480" }) + +add_inbox_message_v0({ + from: "Stripe", + subject: "Your weekly summary", + preview: "Total volume rose 8.4% week-over-week.", + timestamp: "10:42 AM", + unread: true, +}) +``` diff --git a/packages/pen-mcp/src/tools/design-prompt.ts b/packages/pen-mcp/src/tools/design-prompt.ts index 96d31e1da..a5e3c2911 100644 --- a/packages/pen-mcp/src/tools/design-prompt.ts +++ b/packages/pen-mcp/src/tools/design-prompt.ts @@ -19,6 +19,7 @@ const SECTION_NAME_MAP: Record = { cjk: 'cjk-typography', examples: 'examples', elements: 'elements', + 'elements-cookbook': 'elements-cookbook', }; /** Look up a skill by legacy section key or skill name. */ @@ -237,6 +238,7 @@ type PromptSection = | 'guidelines' | 'planning' | 'elements' + | 'elements-cookbook' | 'design-md' | 'copywriting' | 'overflow' @@ -267,6 +269,7 @@ const SECTION_MAP: Record string> = { guidelines: () => DESIGN_GUIDELINES, planning: () => PLANNING_GUIDE, elements: () => getSkillContent('elements'), + 'elements-cookbook': () => getSkillContent('elements-cookbook'), 'design-md': () => 'No design.md loaded in the current document.', copywriting: () => getSkillContent('copywriting'), overflow: () => getSkillContent('overflow'), @@ -361,5 +364,7 @@ ${AUTO_REPLACE_RULES} ${POST_PROCESSING} -${getSkillContent('elements')}`; +${getSkillContent('elements')} + +${getSkillContent('elements-cookbook')}`; } diff --git a/scripts/ab-corpus/build-prompt.ts b/scripts/ab-corpus/build-prompt.ts index 9ea4821d3..9198b23de 100644 --- a/scripts/ab-corpus/build-prompt.ts +++ b/scripts/ab-corpus/build-prompt.ts @@ -1,22 +1,25 @@ /** * Build the system prompt for each A/B variant. * - * Baseline (B): current OpenPencil design prompt MINUS the elements - * section. The model sees schema/layout/text rules + examples and - * can only emit `batch_design` DSL. + * Baseline (B): current OpenPencil design prompt MINUS both element- + * tool skills (decision tree + cookbook). The model sees schema / + * layout / text rules + examples and can only emit `batch_design` + * DSL. * - * Treatment (T): same baseline PLUS the elements section PLUS the + * Treatment (T): same baseline PLUS both element-tool skills PLUS the * `` output-format instruction. The model can either emit * an element-tool call (preferred, schema-narrow) or fall back to * `batch_design` DSL. * - * Implementation: `buildDesignPrompt()` includes the elements section - * by default (we wired that in last session so external MCP clients - * default to the full tool doc). We derive B by string-subtracting the - * elements skill body; T is base + trailing instructions. String - * subtraction is brittle if elements.md moves, but it's O(minutes) to - * fix if it does — much less churn than refactoring the prompt - * builder just for this eval. + * Implementation: `buildDesignPrompt()` includes elements + elements- + * cookbook by default. We derive B by string-subtracting each skill + * body; T is base + trailing instructions. The cookbook lives in a + * separate file so neither piece exceeds the 800-line per-file ceiling + * — text-only LLMs (no MCP `tools/list`) need both halves, the + * decision tree to pick the tool and the cookbook to know its arg + * shape. String subtraction is brittle if elements.md / elements- + * cookbook.md move, but it's O(minutes) to fix if it does — much less + * churn than refactoring the prompt builder just for this eval. */ import { getSkillByName } from '@zseven-w/pen-ai-skills'; @@ -69,20 +72,37 @@ export function buildSystemPrompt(variant: 'B' | 'T'): BuiltPrompt { if (variant === 'T') { return { system: full + '\n\n' + T_TOOL_CALL_INSTRUCTIONS, variant }; } - // Baseline: strip elements content. Exact-match removal keeps the - // rest of the prompt byte-identical so any A/B delta isn't - // explained by accidental prompt-shape drift between variants. + // Baseline: strip BOTH elements skills (decision-tree + cookbook). + // The cookbook split landed when elements.md crossed the 800-line + // ceiling — both halves carry element-tool guidance that B must not + // see, otherwise the baseline leaks tool names + arg shapes and the + // A/B delta no longer measures "tools vs no-tools". Exact-match + // removal of each skill body keeps the rest of the prompt byte- + // identical so any delta isn't explained by accidental shape drift. const elementsSkill = getSkillByName('elements'); + const cookbookSkill = getSkillByName('elements-cookbook'); if (!elementsSkill) { throw new Error( 'elements skill not found in registry — cannot build baseline prompt without it (would leak elements content into B if left in place)', ); } - const withoutElements = full.replace(elementsSkill.content, '').trim(); - if (withoutElements === full.trim()) { + if (!cookbookSkill) { + throw new Error( + 'elements-cookbook skill not found in registry — cannot build baseline prompt without stripping it (would leak the arg-shape examples into B)', + ); + } + let stripped = full.replace(elementsSkill.content, ''); + if (stripped === full) { throw new Error( 'elements content was not present in the full prompt — buildDesignPrompt() may have been refactored; update build-prompt.ts to match', ); } - return { system: withoutElements + '\n\n' + B_TOOL_CALL_INSTRUCTIONS, variant }; + const beforeCookbook = stripped; + stripped = stripped.replace(cookbookSkill.content, ''); + if (stripped === beforeCookbook) { + throw new Error( + 'elements-cookbook content was not present in the full prompt — buildDesignPrompt() may have been refactored; update build-prompt.ts to match', + ); + } + return { system: stripped.trim() + '\n\n' + B_TOOL_CALL_INSTRUCTIONS, variant }; }