feat(ai): add 'elements' section to get_design_prompt + new skill
Step 4 of N-tool element design. Teach external MCP clients (Claude
Code / Codex / Gemini CLI / Cursor) when to reach for a narrow
element tool vs fall through to batch_design.
New skill: packages/pen-ai-skills/skills/phases/generation/elements.md
- Decision tree mapping item shape → tool (card_row / metric_row /
nav_chip_row / bottom_nav / activity_ring)
- PREFER vs STILL-USE-batch_design conditions with concrete spec
phrases ("horizontal scrolling cards", "KPI cards", "bottom nav")
- Minimal usage examples for all 5 tools
- Composition pattern: build section via batch_design → insert row
with parent_id → post-hoc style via batch_design U-op
- Invariants enumeration (wrapper / id assignment / role set) so AI
knows what NOT to rebuild
- Failure-mode guidance: if tool throws, inspect message and switch
strategy rather than retry
- Priority 14 / budget 1500 tokens
design-prompt.ts: register 'elements' in SECTION_NAME_MAP,
PromptSection type, SECTION_MAP dispatch.
design-routes.ts: add 'elements' to get_design_prompt.section enum
+ updated description.
_generated/skill-registry.ts regenerates at vite build time (43 → 44
skills; gitignored, not committed).
Tests:
- 10 new unit tests (design-prompt-elements.test.ts) cover
registration, content invariants, decision-tree presence, fallback
teaching, composition pattern, invariants naming, unknown-section
fallback behavior
- d0-parity-spike snapshot updated (get_design_prompt definition
intentionally changed)
MCP live e2e smoke via StdioClientTransport confirms ListTools enum
shows 11 values including 'elements', CallTool returns 4122 char
content, all 5 tools named, all structural assertions pass.
98/98 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
This commit is contained in:
parent
b62e1e9bd9
commit
43a6b7b86f
105
packages/pen-ai-skills/skills/phases/generation/elements.md
Normal file
105
packages/pen-ai-skills/skills/phases/generation/elements.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
name: elements
|
||||
description: N-tool element family reference — when and how to call add_card_row_v0 / add_metric_row_v0 / add_nav_chip_row_v0 / add_bottom_nav_v0 / add_activity_ring_v0 instead of hand-building via batch_design
|
||||
phase: [generation]
|
||||
trigger: null
|
||||
priority: 14
|
||||
budget: 1500
|
||||
category: base
|
||||
---
|
||||
|
||||
ELEMENT TOOLS (schema-constrained alternatives to batch_design):
|
||||
|
||||
These narrow MCP tools emit well-known structures that batch_design frequently gets wrong on non-Claude models (overflow, wrong role, anti-pattern layout). Each is shape-locked — you pick the tool by matching intent, then supply only content. Visual styling (color, font) stays orthogonal: override via a follow-up batch_design U-op if needed.
|
||||
|
||||
## Decision tree (pick first match)
|
||||
|
||||
1. Row of items with title + subtitle + optional icon → `add_card_row_v0`
|
||||
2. Row of items with small label + big numeric value → `add_metric_row_v0`
|
||||
3. Row of filter chips / category tabs (label + optional icon, active state) → `add_nav_chip_row_v0`
|
||||
4. Bottom tab bar (inline flow, 3-5 nav items) → `add_bottom_nav_v0`
|
||||
5. Apple-style progress ring with centered text → `add_activity_ring_v0`
|
||||
6. None match → fall through to `batch_design`
|
||||
|
||||
## When to use vs batch_design
|
||||
|
||||
PREFER an element tool when the spec says any of:
|
||||
|
||||
- "horizontal scrolling cards", "swipeable row", "chip row", "pills"
|
||||
- "metric tiles", "KPI cards", "dashboard stats", "Steps / Kcal / Sleep" row
|
||||
- "category filter chips", "quick-access shortcuts"
|
||||
- "bottom nav", "tab bar", "tabbar", "底部导航"
|
||||
- "activity ring", "progress ring", "circular progress", "Apple health ring"
|
||||
|
||||
STILL use batch_design when:
|
||||
|
||||
- The row's items are structurally heterogeneous (can't be uniformly described by a single items[] shape)
|
||||
- You need to build a larger composite (e.g. a whole section containing a scroll row + other content — build the section via batch_design, then insert the row via element tool with parent_id)
|
||||
- Post-hoc styling: once the element tool has laid the structure, use batch_design U-ops to apply fills, typography, or theme variables
|
||||
|
||||
## 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,
|
||||
})
|
||||
```
|
||||
|
||||
## Composition pattern
|
||||
|
||||
For a dashboard that needs a metric row inside a page:
|
||||
|
||||
1. Build the page structure via `batch_design` (root frame + section container) — note the section's id
|
||||
2. Call `add_metric_row_v0({ parent_id: "<section-id>", items: [...] })` to insert the row under that section
|
||||
3. Optional: a second `batch_design` U-op to style (fill, theme variables)
|
||||
|
||||
## Invariants you don't need to think about
|
||||
|
||||
The tool guarantees — you cannot break them from the input side:
|
||||
|
||||
- Wrapper structure (`scroll-row-wrapper` + `scroll-row` + fixed-width children) for row tools — overflow-safe
|
||||
- `bottom-tab-bar` is inline (no empty spacer sibling needed, do NOT add one)
|
||||
- Activity ring is frame+cornerRadius=size/2+stroke+centered text — NEVER emit ellipse+sibling text for rings
|
||||
- Every emitted node has a unique id (you can reference it later)
|
||||
- Roles are set (`card` / `metric-tile` / `nav-chip` / `nav-chip-active` / `bottom-tab-bar` / `nav-item` / `nav-item-active` / `activity-ring`)
|
||||
|
||||
## Failure mode
|
||||
|
||||
If the tool throws, **do NOT retry with the same arguments** — the tool has already verified the failure is real (pre-check rejected the parent_id, or post-check detected a silent DSL no-op). Re-throwing from your side wastes tokens. Inspect the error message and either:
|
||||
|
||||
- Fix `parent_id` (ensure the referenced node exists and has no `"` or `\` in its id)
|
||||
- Switch to `batch_design` with the structure taught in `overflow.md` / `layout.md`
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
exports[`D0 parity spike — additive & gated tool registration > pre-D0 production design tool DEFINITIONS are unchanged (snapshot) > pre-d0-design-tool-definitions 1`] = `
|
||||
[
|
||||
{
|
||||
"description": "Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), planning (layered workflow guide). Omit section for the full prompt.",
|
||||
"description": "Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), planning (layered workflow guide), elements (N-tool element reference — add_card_row_v0 / add_metric_row_v0 / add_nav_chip_row_v0 / add_bottom_nav_v0 / add_activity_ring_v0 usage + decision tree). Omit section for the full prompt.",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"section": {
|
||||
"description": "Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow.",
|
||||
"description": "Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference.",
|
||||
"enum": [
|
||||
"all",
|
||||
"schema",
|
||||
|
|
@ -19,6 +19,7 @@ exports[`D0 parity spike — additive & gated tool registration > pre-D0 product
|
|||
"examples",
|
||||
"guidelines",
|
||||
"planning",
|
||||
"elements",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Tests for the `elements` section of get_design_prompt.
|
||||
*
|
||||
* The elements section is the AI-facing index of the N-tool element
|
||||
* family (add_card_row_v0 / add_metric_row_v0 / add_nav_chip_row_v0 /
|
||||
* add_bottom_nav_v0 / add_activity_ring_v0). It teaches the LLM WHEN
|
||||
* to reach for a narrow tool vs fall through to batch_design.
|
||||
*
|
||||
* Spec: openpencil-docs/superpowers/specs/2026-04-19-element-tools-v0.md §5 step 4
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildDesignPrompt, listPromptSections } from '../tools/design-prompt';
|
||||
import { DESIGN_TOOL_DEFINITIONS } from '../routes/design-routes';
|
||||
|
||||
describe('get_design_prompt — elements section', () => {
|
||||
it('"elements" appears in listPromptSections()', () => {
|
||||
expect(listPromptSections()).toContain('elements');
|
||||
});
|
||||
|
||||
it('"elements" appears in get_design_prompt.inputSchema.section.enum', () => {
|
||||
const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'get_design_prompt');
|
||||
expect(def).toBeDefined();
|
||||
const sectionProp = (def?.inputSchema.properties as Record<string, unknown> | undefined)
|
||||
?.section as { enum?: string[] } | undefined;
|
||||
expect(sectionProp?.enum).toContain('elements');
|
||||
});
|
||||
|
||||
it('buildDesignPrompt("elements") returns non-empty content', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
expect(content).toMatch(/ELEMENT TOOLS/i);
|
||||
});
|
||||
|
||||
it('elements section names all 5 production element tools', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
expect(content).toContain('add_card_row_v0');
|
||||
expect(content).toContain('add_metric_row_v0');
|
||||
expect(content).toContain('add_nav_chip_row_v0');
|
||||
expect(content).toContain('add_bottom_nav_v0');
|
||||
expect(content).toContain('add_activity_ring_v0');
|
||||
});
|
||||
|
||||
it('elements section teaches a decision tree (pick first match)', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
expect(content).toMatch(/decision tree/i);
|
||||
// Decision tree should reference the "match items shape → tool" pattern
|
||||
expect(content).toMatch(/title.*subtitle|label.*value|chip|tab bar|ring/);
|
||||
});
|
||||
|
||||
it('elements section teaches when to fall back to batch_design', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
expect(content).toMatch(/batch_design/);
|
||||
// Must name at least one fallback condition so the AI knows escape is OK
|
||||
expect(content).toMatch(/heterogeneous|composite|post-hoc|styling/i);
|
||||
});
|
||||
|
||||
it('elements section teaches the composition pattern (parent_id for row-in-section)', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
expect(content).toMatch(/parent_id/i);
|
||||
});
|
||||
|
||||
it('elements section names the invariants tools guarantee', () => {
|
||||
const content = buildDesignPrompt('elements');
|
||||
// The AI should know it doesn't need to reproduce wrapper structure
|
||||
expect(content).toMatch(/wrapper|clipContent|overflow|invariant/i);
|
||||
expect(content).toMatch(/unique id/i);
|
||||
});
|
||||
|
||||
it('elements section NOT returned when section argument omitted, but IS part of full prompt', () => {
|
||||
// Sanity: default / full prompt should not crash
|
||||
const full = buildDesignPrompt();
|
||||
expect(full.length).toBeGreaterThan(0);
|
||||
// Passing 'elements' explicitly returns just that section (smaller than full)
|
||||
const elementsOnly = buildDesignPrompt('elements');
|
||||
expect(elementsOnly.length).toBeLessThan(full.length);
|
||||
});
|
||||
|
||||
it('unknown section string falls back to full prompt (documented behavior)', () => {
|
||||
const unknown = buildDesignPrompt('does-not-exist');
|
||||
const full = buildDesignPrompt();
|
||||
expect(unknown).toBe(full);
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,9 @@ export const DESIGN_TOOL_DEFINITIONS = [
|
|||
description:
|
||||
'Get design knowledge prompt. Use "section" to retrieve a focused subset instead of the full prompt. ' +
|
||||
'Sections: schema (PenNode types), layout (flexbox rules), roles (semantic roles), text (typography/CJK/copywriting), ' +
|
||||
'style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), planning (layered workflow guide). ' +
|
||||
'style (visual style policy), icons (icon names), examples (design examples), guidelines (design tips), ' +
|
||||
'planning (layered workflow guide), elements (N-tool element reference — add_card_row_v0 / add_metric_row_v0 / ' +
|
||||
'add_nav_chip_row_v0 / add_bottom_nav_v0 / add_activity_ring_v0 usage + decision tree). ' +
|
||||
'Omit section for the full prompt.',
|
||||
inputSchema: {
|
||||
type: 'object' as const,
|
||||
|
|
@ -35,9 +37,10 @@ export const DESIGN_TOOL_DEFINITIONS = [
|
|||
'examples',
|
||||
'guidelines',
|
||||
'planning',
|
||||
'elements',
|
||||
],
|
||||
description:
|
||||
'Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow.',
|
||||
'Which section of design knowledge to retrieve. Default: all. Use "planning" for layered generation workflow; "elements" for N-tool element tool reference.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const SECTION_NAME_MAP: Record<string, string> = {
|
|||
copywriting: 'copywriting',
|
||||
cjk: 'cjk-typography',
|
||||
examples: 'examples',
|
||||
elements: 'elements',
|
||||
};
|
||||
|
||||
/** Look up a skill by legacy section key or skill name. */
|
||||
|
|
@ -235,6 +236,7 @@ type PromptSection =
|
|||
| 'examples'
|
||||
| 'guidelines'
|
||||
| 'planning'
|
||||
| 'elements'
|
||||
| 'design-md'
|
||||
| 'copywriting'
|
||||
| 'overflow'
|
||||
|
|
@ -264,6 +266,7 @@ const SECTION_MAP: Record<PromptSection, () => string> = {
|
|||
examples: () => getSkillContent('examples'),
|
||||
guidelines: () => DESIGN_GUIDELINES,
|
||||
planning: () => PLANNING_GUIDE,
|
||||
elements: () => getSkillContent('elements'),
|
||||
'design-md': () => _designMdContent ?? 'No design.md loaded in the current document.',
|
||||
copywriting: () => getSkillContent('copywriting'),
|
||||
overflow: () => getSkillContent('overflow'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue