agentleFS
robritacca-dotcomrobritacca-dotcom/rift-ds/.claude/skills/api-consistency/SKILL.md

api-consistency

Skill9 starsChanged 6 days agoMITView on GitHub

Review component prop interfaces across the design system, or scoped to one component or family, for naming inconsistencies, missing standard props, mixed colour vocabulary, contract violations, and descriptions that disagree with the contract. Use when asked to review component APIs, audit prop naming consistency, check a component's API, or check TypeScript interfaces for API inconsistencies.

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 3 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when asked to review component APIs, check prop naming consistency, or audit TypeScript interfaces — phrases like "review …

    line 15
  2. Distinctive:

    Default values (from destructuring defaults in the function signature)

    line 29
  3. Very specific:

    Should follow is*/has* convention OR plain adjective — not both (e.g.

    line 47
  4. Very specific:

    Must be on* (e.g. onClick, onChange, onDismiss)

    line 53
  5. Distinctive:

    Should use a consistent vocabulary across components

    line 61
  6. Very specific:

    All components rendering clickable/interactive elements should have className?: string

    line 74
  7. Very specific:

    All components with visual disabled states should have disabled?: boolean

    line 75
  8. Very specific:

    All form-like components should have id?: string

    line 76
  9. Very specific:

    If one component accepts iconLeft/iconRight, siblings in the same family should follow the same pattern

    line 81
  10. Very specific:

    Default values: if size defaults to "default" on a Gadget, it should not default to "medium" on a related Sprocket

    line 82

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

api-consistency

Review component prop interfaces across the design system for naming inconsistencies, missing standard props, and pattern violations.

When invoked

Use this skill when asked to review component APIs, check prop naming consistency, or audit TypeScript interfaces — phrases like "review component APIs", "prop consistency audit", "are our component props consistent", "check for API inconsistencies", "check this component's API".

It also runs scoped inside other flows: new-component runs it on a new component before registering, and ship and super-ship run it on the components a diff touched. When run scoped inside another flow, report only findings about the components in scope (the touched components and their family). Library-wide findings the owner has already reviewed and accepted are not news to that flow, and re-raising them there buries the finding the flow exists to catch.

Instructions

  1. Determine scope. Accept one of:
    • Scoped (the cheap mode other skills call): one component, or a family. A family is the component's shared folder (the registry entry's folder) or its category in src/components/registry.json, plus any component that composes it or that it composes. Every check below runs, but only over these components, and the family is the comparison set for the family checks.
    • A list of specific components (e.g. Button, CircularButton, ButtonGroup) → compare those
    • all → scan all components in src/components/, plus the chat widget's documented exports in src/chat/ (they ride the same generated API with importPath rift-ds/chat)
    • A category description (e.g. "all button-like components", "all form inputs") → infer the relevant components
  2. Read every component's TypeScript interface. Start from website/src/data/component-api.generated.ts for the prop inventory — it holds every own prop with type, default, requiredness and description, parsed exactly as the published .d.ts documents them. Then read the .tsx files in scope for what the generated file does not capture:
    • The structural contract (the ref, {...rest} placement, the own-props split, 'use client')
    • Default values (from destructuring defaults in the function signature)
  3. First, check conformance to the published contract. This is the highest-value part of the review and takes precedence over style preferences below. The contract is defined in the new-component skill and summarised in CLAUDE.md's Component Anatomy — read one of them rather than trusting this list, which exists to tell you what to look for, not to restate the rules:

    • The ref, the rest spread and the native props. node scripts/validate-component-contract.mjs build-enforces a ref (forwardRef with a displayName, or React 19's plain ref prop), props extending the native element's type, and the ref and {...rest} on one node with the root class after the spread; the props-contract story (src/stories/Contract.stories.tsx) proves at runtime that both reach the same element. Run the script rather than re-reading for these. What it cannot judge, and you still check: whether the node it found is the primary one (the panel for a portal component, the control for a form field), and whether own handlers the component sets on that node compose with the consumer's (onMouseEnter from rest must still fire) instead of overwriting them.
    • 'use client' present when the component is interactive, and absent when it is purely presentational. A needless directive silently costs consumers Server Component rendering — flag both directions.
    • Native event signatures keep the standard names. onChange must be a ChangeEventHandler, never (value) => void. The convenience callback is named for the value's shape: onValueChange (string/number), onCheckedChange (boolean), onValuesChange (array) — and both fire.
    • The controlled/uncontrolled pair convention. State a consumer can own comes as x / defaultX / onXChange, and works with x omitted. A controlled-only prop with no default* partner, or a selection named activeTab/onTabChange where the convention says value/onValueChange, is a finding, unless the component's design.md spec says it is fully controlled by design.
    • English text is a prop, announced or visible. A literal English aria-label (or visually hidden text) the component writes with no *Label prop to replace it is a finding: a consumer cannot localise it. The same goes for visible words. validate-accessible-names.mjs and validate-visible-text.mjs catch the literal attributes and JSX text; what they cannot see, and you check, is English inside string literals and arrays: a weekday or month list, a pluralised count built in a template (${n} results), a status default.
    • No Figma variant properties in the code API. A state enum mixing hover/active (CSS pseudo-classes the browser owns) with disabled (real semantics) has two sources of truth. Prefer variant over priority/kind, and disabled as a real boolean.
    • Labelled form controls compose inside Field rather than re-implementing label/helper/required/ARIA wiring.
    • Deprecations, not removals — an old prop should still work and carry @deprecated naming its replacement.

    Where a prop deliberately shadows a native attribute with different meaning (size vs the native character-width attribute, title vs the native tooltip), that is acceptable only if the collision is documented in the prop's JSDoc. Flag undocumented collisions.

  4. Then check these style-level inconsistencies:

    Boolean prop naming:

    • Should follow is*/has* convention OR plain adjective — not both (e.g. isDisabled on one component, disabled on another doing the same thing)
    • Flag: mixed usage within the same component family

    The machine-held half of this vocabulary is website/public/schema/component.schema.json: its allOf rules are the list of what it locks, every generated component schema is proven against it on each build, and CLAUDE.md's Schemas are generated twins bullet owns the rule that a new sanctioned tone changes that file and SANCTIONED_TONES together. A finding that changes a locked vocabulary is also an edit there. The decisions in HISTORY.md's 2026-11-01 schemas entry are accepted findings: do not re-raise them.

    Event handler naming:

    • Must be on* (e.g. onClick, onChange, onDismiss)
    • Flag: handleClick, clickHandler, onClickHandler, or similar

    Content prop naming:

    • label for display text, children for slot content
    • Flag: text, title, copy, content used interchangeably across components for the same purpose

    Size enum values:

    • Should use a consistent vocabulary across components
    • Flag: "sm"/"md"/"lg" on one component and "small"/"medium"/"large" on another, or "compact"/"default" on one and "small"/"medium" on another

    Colour vocabulary:

    • Emphasis and colour are separate axes. variant holds emphasis (primary/secondary/tertiary); tone holds colour (default/neutral/destructive). Flag a variant union that mixes the two without the colour values marked as legacy aliases beside a tone prop (CLAUDE.md's Component Anatomy states the rule; design.md's Components intro owns the details; scripts/validate-prop-vocabulary.mjs enforces the mechanical half). Its SANCTIONED_TONES, SANCTIONED_FORM_VARIANTS, SANCTIONED_SIZES and LEGACY_VALUES tables are the record of the deliberate exceptions (a tone in another vocabulary, a variant that picks a form rather than an emphasis, a size scale of its own, a labelled legacy alias), so read them before flagging any of these
    • One status set. A status-bearing component carries the five status roles on variant, the same five every other status component uses. Flag a sixth role, a missing one, or a synonym (success for positive, danger for error)
    • One word for danger: destructive. Flag danger, negative and other strays naming the same thing, unless the value is a labelled legacy alias in LEGACY_VALUES
    • Data entries take a boolean, components take a tone. An item inside a data array (a menu item, a command row) marks danger with destructive?: boolean; a component marks it with tone. Flag the two crossed

    Popup side vocabulary:

    • The components that open an anchored panel name its preferred side with position (Tooltip, Popover, HoverCard), placement (ModelPicker, ThemePicker, AiButton's summaryPlacement and requestPlacement) or align for the edge (DropdownMenu, Popover, SplitButton, ThemePicker). Those are settled and never renamed (the API only grows); flag a new component that introduces a fourth spelling, or uses one of these words for something else

    Missing standard props on interactive components:

    • All components rendering clickable/interactive elements should have className?: string
    • All components with visual disabled states should have disabled?: boolean
    • All form-like components should have id?: string
    • A form control's name must reach FormData, whatever element it renders: on the native input, or through a hidden input when the control is a div or button with an ARIA role (design.md, "A control reaches a form through name"). Flag a name that a form never reads, including one spread onto a button or a div. The controls design.md lists as taking no name are not missing one.

    Family consistency:

    • Components in the same family (e.g. Button / CircularButton / ButtonGroup) should share size enum values
    • If one component accepts iconLeft/iconRight, siblings in the same family should follow the same pattern
    • Default values: if size defaults to "default" on a Gadget, it should not default to "medium" on a related Sprocket
  5. Then check the contract agrees with its description. For each component in scope, read its entry in website/src/data/component-api.generated.ts (exactly what /api/mcp's get_component serves, so this is the contract strangers' agents see), then read the two prose descriptions of it against that entry:

    • its registry description (src/components/registry.json, or src/chat/registry.json for the widget), which also ships through the site chat's corpus, the consumer agent skill and the page metadata
    • its ### spec section in design.md

    Flag any prop or value a description names that the contract does not carry, or carries only as @deprecated (a description still saying "neutral and destructive variants" after those moved to tone is the classic case). A design.md spec is not a props list and is not expected to name every prop: the prop JSDoc is the one home of the full contract, and the generated component API, the per-component .md pages and the MCP endpoint all serve it, so restating it in design.md would be a second copy to drift. Flag a spec that omits a prop only when the prop changes what the component looks like or how it behaves (a variant, a size, a mode) and a reader of the spec would design wrongly without it. The fix goes in the source of the description, never in a generated file.

  6. Output a grouped findings report. The component names in this example are fictional by design — findings about real components go stale the moment someone fixes them, so this block only demonstrates the format:

    ## API Consistency Report
    
    ### Contract violations (highest impact)
    - Gadget — takes no ref; a consumer cannot reach its node
    - Sprocket — props do not extend ComponentPropsWithoutRef<'span'>; data-* unreachable
    - MetricPod — has 'use client' but no hooks or handlers; blocks Server Component rendering
    
    ### Boolean prop naming
    - Gadget: uses `disabled` (plain adjective)
    - Doodad: uses `isDisabled` (is* prefix)
    → Standardise to `disabled` across all interactive components
    
    ### Size enum values
    - Gadget: "compact" | "default" | "large"
    - Whatsit: "small" | "medium" | "large"
    → Standardise to the enum of the most-used component in the family
    
    ### Colour vocabulary
    - Whatsit: `variant` holds "primary" | "danger"; danger is a colour, and a stray word for destructive
    → Move it to `tone="destructive"`, keep `variant="danger"` as a deprecated alias
    
    ### Description disagrees with contract
    - Doodad — registry description names a "ghost variant"; the contract has no such value
    
    ### Missing className prop
    - Doodad — no className passthrough
    - Gadget — no className passthrough
    
    ### Summary
    X naming inconsistencies · Y missing props · Z structural mismatches
  7. Prioritise fixes by impact:
    • High: Renames that would require consuming code changes — flag these clearly so the owner can decide whether to batch into a breaking release
    • Medium: Missing props that are commonly needed by consumers
    • Low: Style preferences with no breaking impact

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.