Skip to content

Night Watch API Auditor

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

Night Watch — API Auditor (Deprecated)

⚠️ This role has been merged into Night Watch Component Auditor. The Component Auditor combines theming, API, accessibility, and export checks into a single nightly pass per component. This page is kept for historical reference only.

Assigned to: Cindy's Navi (cixzhang)

Goal: Ensure all components follow the API conventions documented in API Conventions — consistent naming, prop patterns, composition rules, and accessibility contracts.


Why This Role Exists

API inconsistencies accumulate silently. A callback named onValueChange instead of onChange, a missing is prefix on a boolean, a leftIcon instead of startIcon, a slot that hoists child props onto its parent — these don't break builds but they break the developer contract. When consumers learn one component's API, they expect the rest to follow the same patterns. This role catches the drift.


Scope

1. Prop Naming

All props must follow the naming conventions from API Conventions.

Check for:

  • Booleans missing is/has prefix (e.g. disabled instead of isDisabled, loading instead of isLoading)
  • Callbacks not following on{Verb}{Scope?} pattern (e.g. handleClick as a prop name)
  • Primary change callback named something other than onChange (e.g. onValueChange, onSelectionChange for the single primary value)
  • Visibility callbacks not using onOpenChange pattern for layer components
  • Directional props using left/right instead of start/end
  • Uncontrolled defaults not using default prefix (e.g. initialValue instead of defaultValue, initialIsOpen instead of defaultIsOpen)
  • Boolean defaults not preserving is/has after default (e.g. defaultOpen instead of defaultIsOpen)

Exceptions:

  • HTML attribute passthroughs (e.g. onClick, onBlur) are fine as-is
  • Props prefixed with html for collision avoidance (e.g. htmlName, htmlFor) are fine

2. Type & Context Naming

All exported types and contexts use unprefixed names.

Check for:

  • Props types not named <Component>Props
  • Variant types not named <Component>Variant
  • Status types not named <Component>Status
  • Contexts not named <Component>Context
  • Context hooks not named use<Component><Thing>
  • Public hooks not following the use<Component> form

3. Component Structure

Components must follow the standard file structure and conventions.

Check for:

  • Missing displayName on exported components
  • Missing file header with @file, @input, @output, @position, SYNC block
  • Components not extending BaseProps (or an appropriate Omit of it)
  • Missing xstyle, className, style escape hatches
  • Not using mergeProps() for style merging (e.g. separate spreads that overwrite)
  • Missing 'use client' directive on files that use React client APIs. Run node scripts/check-use-client.mjs — it scans all non-test source files for React client API imports and fails if any are missing the directive. If it reports failures, add 'use client'; as the first line of each flagged file. Without it, tsup may bundle the file into a shared chunk that loses the directive, breaking Next.js builds.

4. Composition Patterns

Components must follow slot, composition, and escape hatch conventions.

Check for:

  • Slot props that hoist child state onto the parent (e.g. parent accepting isMobileNavOpen when the child component owns that state)
  • Raw HTML elements where Astryx primitives exist (same as Theme Auditor scope — but from the API contract angle: the prop interface shouldn't expose raw-element-only APIs)
  • Escape hatch props without demonstrated use cases (e.g. trigger="hover" on Popover when HoverCard exists)
  • Boolean-or-config props that should be individual props (or vice versa) per the conventions

5. Input Component Consistency

All input components must implement the standard field props.

Check for:

  • Missing required field props: label, value, onChange/onChangeAction
  • Missing standard optional props where applicable: isLabelHidden, description, isOptional, isRequired, isDisabled
  • status prop not following {type, message?} shape
  • size variants not using 'sm' | 'md' (or 'sm' | 'md' | 'lg' for non-inputs)
  • Missing isLoading / isBusy pattern for async-capable components
  • Using native disabled for busy state instead of visual-only aria-busy

6. Accessibility Contract

Components must implement the required accessibility patterns.

Check for:

  • Interactive components missing label prop
  • Missing aria-required, aria-invalid, aria-describedby wiring on inputs
  • isDisabled not mapping to native disabled attribute
  • Busy state using native disabled (should use aria-busy + visual treatment only)
  • Icon-only buttons missing aria-label (should come from label prop)
  • addEventListener usage instead of React event handlers
  • Focus not preserved during state transitions (elements removed from DOM instead of hidden)

7. Export Hygiene

Component exports must follow the standard patterns.

Check for:

  • Components missing from package src/index.ts re-exports
  • Missing type exports (props, variant, status types)
  • Component not registered as a separate entry point in tsup.config.ts

Nightly Checklist

This role runs once per night and produces one PR covering all findings. Hourly runs accumulate audit results into a staging log; only the final run of the night creates a PR.

Step 1: Check if Already Run Today

Check memory/xds-night-watch-state.json for apiAuditor.lastRunAt. If it's from today (PST), skip and NO_REPLY.

Step 2: Determine Which Components to Audit

Check memory/xds-night-watch-state.json for apiAuditor.auditQueue.

  • If the queue is empty, scan packages/core/src/ for all component directories and build a fresh queue
  • Pick the next 5 components from the queue (or fewer if less than 5 remain)

Step 3: Audit the Components

For each of the selected components:

  1. Read the source files — main component file, types, index.ts, hooks
  2. Check prop naming — scan the props interface for convention violations
  3. Check type/context naming — verify all exported types use the unprefixed <Component>* form
  4. Check component structure — displayName, file header, BaseProps, mergeProps
  5. Check composition patterns — slot props, escape hatches, primitive reuse
  6. Check input consistency (if input component) — field props, status shape, size variants
  7. Check accessibility — label, ARIA attributes, focus management, event handling
  8. Check exports — index.ts, package index, tsup config

Step 4: Report Findings

For each issue found:

  • Log it to memory/xds-night-watch/{date}.md under a ## API Audit section
  • Categorize as: naming-violation, missing-type-prefix, structure-issue, composition-violation, input-inconsistency, a11y-gap, export-gap
  • Include the file path, line number, the current code, and what the fix should be

Step 5: Fix Findings — One PR Per Night

Batch all findings from this run into a single PR:

  • Branch naming: navi/api-audit/YYYY-MM-DD
  • Fix naming violations, add missing prefixes, add missing displayName/headers, fix composition issues
  • Run pnpm test and pnpm build before creating the PR. Fix any failures. Do not send a PR with broken tests or build.
  • Publish PRs ready for review (not draft) — label with hardening and api
  • When a finding needs human judgment (e.g. whether a prop should be renamed given existing consumers), include it in the PR description under a Needs Review section rather than fixing it unilaterally
  • If no issues are found across all audited components, do not create a PR — just update state

Step 6: Update State

Update memory/xds-night-watch-state.json:

{
  "apiAuditor": {
    "lastAuditedComponent": "Button",
    "auditQueue": ["Card", "Dialog", ...],
    "prsFiled": ["#800"],
    "lastRunAt": "2026-03-21T04:00:00Z",
    "completedComponents": ["Button", "TextInput"]
  }
}

When the queue is empty (all components audited), clear completedComponents and rebuild the queue from scratch. This ensures components are rechecked continuously — fixes from previous cycles may have regressed, and new code may have introduced new issues.


Does NOT Do

  • Subjective API design decisions — only convention enforcement
  • Theme auditing (CSS variables, xdsThemeProps — that's Theme Auditor)
  • Review PRs (that's Reviewer)
  • Fix CI (that's QA)
  • Triage issues (that's PM)

State Schema

{
  "apiAuditor": {
    "lastAuditedComponent": "string — last component fully audited",
    "auditQueue": ["string[] — remaining components to audit"],
    "prsFiled": ["string[] — PR numbers filed this cycle"],
    "lastRunAt": "ISO timestamp",
    "completedComponents": ["string[] — all components audited this cycle"]
  }
}

Examples

Boolean prop missing prefix (bad)

interface DialogProps {
  closable?: boolean;  // ❌ missing `is` prefix
}

Boolean prop with prefix (good)

interface DialogProps {
  isClosable?: boolean;  // ✅ `is` prefix
}

Primary callback misnamed (bad)

interface SelectorProps {
  onValueChange?: (value: string) => void;  // ❌ primary callback should be `onChange`
}

Primary callback named correctly (good)

interface SelectorProps {
  onChange?: (value: string) => void;  // ✅ primary change callback
}

Directional prop using left/right (bad)

interface TextInputProps {
  leftIcon?: IconType;  // ❌ should use `start`/`end`
}

Directional prop using start/end (good)

interface TextInputProps {
  startIcon?: IconType;  // ✅ RTL-friendly
}

Uncontrolled default missing prefix (bad)

interface CollapsibleProps {
  initialIsOpen?: boolean;  // ❌ should be `defaultIsOpen`
}

Uncontrolled default with correct prefix (good)

interface CollapsibleProps {
  defaultIsOpen?: boolean;  // ✅ follows React convention + Astryx boolean rules
}

Missing displayName (bad)

export function Card({ children }: CardProps) {
  return <div>{children}</div>;
}
// ❌ no displayName

With displayName (good)

export function Card({ children }: CardProps) {
  return <div>{children}</div>;
}
Card.displayName = 'Card';  // ✅

Slot hoisting child state (bad)

// ❌ Parent hoists child's state
<AppShell
  mobileNav={content}
  isMobileNavOpen={isOpen}
  onMobileNavOpenChange={setOpen}
/>

Slot as passthrough (good)

// ✅ Child owns its own state
<AppShell
  mobileNav={<MobileNav isOpen={isOpen} onOpenChange={setIsOpen} />}
/>

Clone this wiki locally