--- name: metadata-schema-generator description: "Generate per-component JSON metadata files (.ai/metadata/) from source: props, behaviour, composition, a11y contract, prohibited prop combinations. Triggers: component metadata schema, machine-readable component JSON, manifest for MCP/codegen. Prose for Figma: use ai-component-description." allowed-tools: Read, Write, Grep, Glob, Bash(cat:*), Bash(find:*), Bash(head:*), Bash(ls:*), Bash(npx react-docgen-typescript:*), Bash(npx custom-elements-manifest:*), Bash(npx vue-component-meta:*), Bash(npx ajv:*) references: - ../../knowledge-notes/ai-readiness.md - ../../knowledge-notes/component-bestiary-reference.md - ../../knowledge-notes/output-discipline.md --- # Metadata schema generator A skill for generating structured JSON metadata schemas for design system components. These schemas encode everything an AI agent, MCP server, code generator, or testing framework needs to work with a component programmatically — props, behavioural rules, composition constraints, accessibility contracts, and (where the team supplies it) business context — in a format that does not require natural language parsing. ## Before you begin: verify references Confirm that every path in this skill's frontmatter `references:` exists relative to this SKILL.md. If any is missing, stop: the install is incomplete, usually because a flattening installer (for example `npx skills install`) dropped the repo-root `knowledge-notes/` directory. Tell the user to reinstall by a method in `1-INSTALL.md` and run `verify-install.sh` from the install root. Proceed without the references only if the user explicitly says to, and then say in the output that it was produced without the pack's reference material. ## Context Component documentation serves humans. Component metadata serves machines. The distinction matters because the information needs are different, the format requirements are different, and the failure modes are different. A human reading a component's documentation can infer that "this button triggers the primary action" means it should be visually prominent and placed in the expected location. A code generation agent reading that same string cannot infer any of that — it needs explicit data: `{ "role": "primary_action", "visual_weight": "high", "placement": ["form_footer", "dialog_footer", "page_header"] }`. Most design systems have one layer of machine-readable data: TypeScript interfaces or PropTypes declarations that define prop names and types. This is necessary but insufficient. A TypeScript interface tells a tool that a Button accepts a `variant` prop of type `"primary" | "secondary" | "ghost"` — but it does not tell the tool when to use `primary` vs `secondary`, which combinations of props are prohibited, where the component can be placed in a layout, or what accessibility contract it must honour. Structured metadata fills this gap. It encodes the knowledge layer between "what the component accepts" (TypeScript) and "how the component should be used" (documentation) as machine-readable data that tooling consumes directly. The `ai-component-description` skill produces text descriptions optimised for LLM consumption. This skill produces structured data optimised for programmatic consumption. Together they serve the two modes of AI interaction: conversational (descriptions) and deterministic (metadata). ## Boundaries This skill is the pack's one extraction of component facts. It generates structured JSON metadata for tooling — not human-readable documentation (`usage-guidelines`, `pattern-documentation`) or LLM-facing prose (`ai-component-description`); those two read `.ai/metadata/` when it exists and render from it, so the props and accessibility contract are derived once. If the system has no component inventory yet, run `codebase-index` first. If no component source is in reach, stop and ask for it: metadata can't be extracted from a description. If the team has no consumer for the files yet (no `AGENTS.md` pointing at them, no MCP server, no lint integration), say so and suggest `agent-instructions` as the first consumer rather than generating files nothing reads. --- ## Configuration If `.ds-ops-config.yml` exists, follow the configuration-and-recurring knowledge note (`../../knowledge-notes/configuration-and-recurring.md`) for loading, integration fallbacks and recurring runs. This skill reads: - `system.framework` — determines prop extraction approach (React, Vue, Svelte, etc.) - `system.component_paths` — directs scanning to component source files - `integrations.*` — component data (see below) - `metadata.schema_version` — locks the output schema version for compatibility - `metadata.output_directory` — overrides default output location - `metadata.include_business_context` — toggles the business context layer (default: false; see Step 4) ## Auto-pull integrations **Figma MCP** (`integrations.figma.enabled: true`): - Read component properties, variant structures, and descriptions from `integrations.figma.file_key` - Cross-reference Figma component properties against code props to detect mismatches - Extract accessibility annotations if present **Storybook** (`integrations.storybook.enabled: true`): - Fetch prop tables and arg types from story metadata - Extract documented states and control definitions - Pull interaction test definitions if available **Source** (always attempted, with the tool that fits): - **React with TypeScript:** `react-docgen-typescript` gives name, type, required, default and description per prop; hand-read only what it can't resolve (complex generics), and say so - **Vue:** `vue-component-meta` - **Web Components:** the Custom Elements Manifest (`npx custom-elements-manifest analyze`, or an existing `custom-elements.json`) - **Anything else:** read the interfaces, PropTypes and JSDoc by hand, and note under Scope that extraction was manual --- ## Step 1: Select components and assess existing metadata Ask for or confirm (skip questions already answered by auto-pull): - Which components need metadata schemas? (Specific components, a category, or the full library) - Where do component source files live? - Is there existing structured metadata in any format (JSON, YAML, Custom Elements Manifest)? For each component, scan for existing metadata sources: - TypeScript interface or PropTypes declaration - JSDoc/TSDoc annotations - Storybook arg types and controls - Existing metadata files (`.metadata.json`, `.metadata.ts`) - Figma component description Produce a source assessment per component: | Component | TS interface | JSDoc | Storybook | Figma | Existing metadata | Gaps | |---|---|---|---|---|---|---| | Button | complete | partial | complete | yes | none | composition, behaviour | | Card | complete | none | partial | yes | none | all semantic layers | --- ## Step 2: Generate the base schema from source For each component, extract the prop layer automatically from TypeScript or equivalent: Illustrative Button (a real run uses the component's actual source): ```json { "$schema": "./schema.json", "component": "Button", "version": "1.0.0", "status": "stable", "props": [ { "name": "variant", "type": "enum", "values": ["primary", "secondary", "ghost"], "default": "primary", "required": false, "description": "Visual weight of the button", "semantic": { "primary": "Use for the single most important action on the page or in a section", "secondary": "Use for supporting actions alongside a primary action", "ghost": "Use for tertiary actions or actions within dense UI" } }, { "name": "size", "type": "enum", "values": ["sm", "md", "lg"], "default": "md", "required": false, "description": "Controls button height and text size" }, { "name": "disabled", "type": "boolean", "default": false, "required": false, "description": "Prevents interaction and applies disabled styling" }, { "name": "loading", "type": "boolean", "default": false, "required": false, "description": "Shows a spinner in place of the label and prevents interaction" }, { "name": "aria-label", "type": "string", "required": false, "description": "Accessible name when the button has no visible text" }, { "name": "children", "type": "ReactNode", "required": true, "description": "Button label content" } ] } ``` **Build on the standard, don't invent one.** For Web Components the metadata file *is* the Custom Elements Manifest with a `dsops` object added to each declaration for the semantic layers (CEM allows additional properties, and every CEM consumer keeps reading it). For React and Vue, the `props` array keeps docgen's field names (`name`, `type`, `required`, `defaultValue`, `description`) so anything that reads docgen output reads this too, and the semantic layers sit beside it. **Extraction rules:** - Every prop in the TypeScript interface must appear in the schema - Keep the raw type string in `type.raw`; add `type.kind` normalised to `string`, `number`, `boolean`, `enum`, `ReactNode`, `function`, `object`, `array`, so generics and non-literal unions aren't lost - Enum values must be listed explicitly, not as a type reference - Take `default` from the source's declared default. Omit `default` when source declares none - If a prop has JSDoc, use it as the description. If not, flag the prop for manual description. - Per-value `semantic` text comes from JSDoc or the team's docs. If neither has it, leave `semantic` out and flag the prop rather than writing plausible guidance --- ## Step 3: Add the semantic layer The semantic layer encodes meaning that TypeScript types cannot express. For each component, add the three blocks below. **Provenance rule.** Every `behaviour`, `composition` and `accessibility` block carries a `provenance` value: `source` (read from the component code), `docs` (from the team's documentation or the user), or `proposed` (anything not traced to either). A block that mixes sources takes the weakest. Never present a proposed rule as the system's actual behaviour or policy: tools and agents read this file as ground truth. See "Every figure and fact needs a source" in the output-discipline knowledge note. The examples below continue the illustrative Button. ### Behavioural rules ```json { "behaviour": { "states": ["default", "hover", "active", "focus", "disabled", "loading"], "transitions": { "default_to_loading": { "trigger": "Form submission or async action", "visual": "Replace label with spinner, disable interaction", "duration": "Until async operation completes or times out" } }, "constraints": [ "At most one primary variant Button per form", "Loading state must disable all sibling interactive elements", "Icon-only buttons must have an aria-label prop" ], "provenance": "docs" } } ``` The first two constraints stand in for team rules: record rules like these only when the team's docs or the user state them. ### Composition rules ```json { "composition": { "valid_parents": ["Form", "Card", "Dialog", "Toolbar", "PageHeader"], "valid_children": ["Icon", "text"], "prohibited_nesting": ["Button may not contain another Button"], "slot_contract": { "children": { "accepts": ["string", "Icon"], "max_elements": 2, "pattern": "Optional leading Icon + text label" } }, "common_combinations": [ { "pattern": "Primary + Secondary pair", "context": "Dialog footer, form actions" }, { "pattern": "Icon-only in Toolbar", "context": "Dense action bars" } ], "provenance": "proposed" } } ``` ### Accessibility contract ```json { "accessibility": { "role": "button", "aria_required": [], "aria_conditional": [ { "prop": "aria-label", "when": "children is Icon only (no visible text)" }, { "prop": "aria-expanded", "when": "button controls a collapsible region" }, { "prop": "aria-haspopup", "when": "button opens a menu or dialog" } ], "keyboard": { "Enter": "Activate button", "Space": "Activate button" }, "focus": { "receives_focus": true, "focus_visible": "Required — uses system focus ring token", "tab_order": "Follows DOM order unless explicitly managed" }, "screen_reader": { "announcement": "Button label + role", "state_changes": "Announces disabled state change" }, "provenance": "source" } } ``` --- ## Step 4: Add the business context layer The business context layer connects the component to product outcomes. It is off by default (`metadata.include_business_context: false`). Include it only when the user or the team's docs supply the metrics and rules, and give every entry a `source` (file path, URL, or `user`). Never infer a component's business function, metrics or traffic from its name. Illustrative shape: ```json { "business_context": { "function": { "value": "conversion", "source": "user" }, "metrics": [ { "name": "Click-through rate", "role": "primary", "source": "docs/analytics/cta-tracking.md" }, { "name": "Form completion rate", "role": "secondary", "source": "user" } ], "experimentation": { "safe_to_test": ["label text", "variant within brand palette"], "never_test": ["removing disabled state logic", "removing aria attributes"], "requires_review": ["changing size scale", "adding new variants"], "source": "user" }, "usage_analytics": { "custom_events": [ { "event": "cta_click", "data": ["variant", "page", "position"], "source": "src/analytics/events.ts" } ] } } } ``` --- ## Step 5: Add prohibited combinations Encode prop combinations that are technically valid but semantically wrong. Each entry carries a `provenance` value, as in Step 3: ```json { "prohibited_combinations": [ { "combination": { "variant": "ghost", "size": "lg" }, "reason": "Ghost buttons at large size create false visual hierarchy — they appear as primary actions despite being tertiary", "severity": "warning", "provenance": "proposed" }, { "combination": { "disabled": true, "loading": true }, "reason": "Redundant states — loading already prevents interaction. Use loading alone.", "severity": "error", "provenance": "docs" } ] } ``` **Severity levels:** - `error` — this combination is prohibited and tools should refuse to generate it - `warning` — this combination is discouraged and tools should flag it for review - `info` — this combination has nuances the developer should be aware of --- ## Step 6: Produce the complete schema Assemble all layers into the complete metadata schema per component. ### Output structure ``` .ai/metadata/ schema.json # JSON Schema definition for validation Button.metadata.json Card.metadata.json Modal.metadata.json ... manifest.json # Index of all metadata files ``` ### Base schema (`schema.json`) Write a JSON Schema alongside the metadata files so validation (Step 7) and CI have something to check against. A minimal version: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Component metadata", "type": "object", "required": ["component", "props"], "properties": { "component": { "type": "string" }, "version": { "type": "string" }, "status": { "enum": ["alpha", "beta", "stable", "deprecated"] }, "props": { "type": "array", "items": { "type": "object", "required": ["name", "type", "required"], "properties": { "name": { "type": "string" }, "type": { "enum": ["string", "number", "boolean", "enum", "ReactNode", "function", "object", "array"] }, "values": { "type": "array" }, "default": {}, "required": { "type": "boolean" }, "description": { "type": "string" }, "semantic": { "type": "object" } } } }, "behaviour": { "$ref": "#/$defs/layer" }, "composition": { "$ref": "#/$defs/layer" }, "accessibility": { "$ref": "#/$defs/layer" }, "business_context": { "type": "object" }, "prohibited_combinations": { "type": "array", "items": { "type": "object", "required": ["combination", "reason", "severity", "provenance"], "properties": { "severity": { "enum": ["error", "warning", "info"] }, "provenance": { "$ref": "#/$defs/provenance" } } } } }, "$defs": { "provenance": { "enum": ["source", "docs", "proposed"] }, "layer": { "type": "object", "required": ["provenance"], "properties": { "provenance": { "$ref": "#/$defs/provenance" } } } } } ``` ### Manifest format Illustrative values; coverage figures are counted from the files actually generated. ```json { "generated": "[date]", "system": "[design system name]", "component_count": 55, "coverage": { "props": "100%", "semantic": "85%", "accessibility": "90%", "business_context": "40%", "prohibited_combinations": "60%" }, "components": [ { "name": "Button", "file": "Button.metadata.json", "status": "stable" }, { "name": "Card", "file": "Card.metadata.json", "status": "stable" } ] } ``` --- ## Step 7: Validate the schemas Run validation checks on the generated schemas: **Structural validation:** - Every schema is valid JSON - Every schema conforms to `schema.json`: run `npx ajv validate -s .ai/metadata/schema.json -d ".ai/metadata/*.metadata.json"` (ajv-cli) and report its output. If ajv isn't available and can't be installed, say validation wasn't run; don't report the files as valid - Every `behaviour`, `composition` and `accessibility` block, and every prohibited combination, has a `provenance` value - Every prop in the TypeScript interface appears in the schema (no missing props) - Every enum value listed in the schema exists in the TypeScript type (no phantom values) **Semantic validation:** - Every prop has a description (not just a type) - Every enum prop either has per-value semantic descriptions from a source, or is listed under "Needs a human" (never filled with plausible guidance) - Composition rules reference components that exist in the system (no broken references) - Accessibility contracts are complete for all interactive components **Cross-reference validation:** - Component names in metadata match component names in code - Prop names and types match TypeScript interfaces - Where both sides declare composition, they agree (Card lists Button as a child and Button lists Card as a parent); a one-sided declaration is listed for review, not treated as an error - Status fields are consistent with the component's actual lifecycle status **Coverage reporting:** - What percentage of components have complete metadata schemas? - What percentage of props have semantic descriptions beyond the TypeScript type? - What percentage of interactive components have accessibility contracts? - What percentage of conversion-related components have business context? --- ## Step 8: Summarise in chat End with a short chat summary: - **Headline:** how many component files were written and how many are complete - **Files written:** paths under `.ai/metadata/` (or the configured `metadata.output_directory`), including `schema.json` and `manifest.json` - **Needs a human:** props flagged for manual description, and every block or combination marked `proposed` - **Scope:** the block from the output-discipline knowledge note, including components and sources not scanned --- ## Recommend to the user - Co-locate metadata files with component source files or in a dedicated `.ai/metadata/` directory - Automate prop extraction from TypeScript interfaces to keep the base layer in sync - Treat semantic, accessibility, and composition layers as authored knowledge: entries generated without a source stay `proposed` until someone on the team confirms them - Run validation as part of CI to catch metadata drift from source - Use the manifest as the entry point for all tooling that consumes component metadata - Regenerate after adding components, changing props, or updating accessibility contracts --- ## Quality checks - Every generated schema is valid JSON and was validated with ajv, or the summary says validation didn't run - Props came from docgen, component-meta or a Custom Elements Manifest where one applies, keep the raw type alongside the normalised kind, and the Scope block names the extraction method - Semantic descriptions add information beyond what the prop name and type convey - Composition rules form a consistent graph (no contradictions between parent and child declarations) - Accessibility contracts cover all interactive components, not just the most common ones - Prohibited combinations cite specific reasons, not generic advice - The manifest accurately reflects the actual set of generated schema files - Business context appears only where the user or docs supplied it, and every entry has a `source` - Nothing traced to neither source nor docs is presented without a `proposed` marker