-
Notifications
You must be signed in to change notification settings - Fork 28
Night Watch API Auditor
⚠️ 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.
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.
All props must follow the naming conventions from API Conventions.
Check for:
- Booleans missing
is/hasprefix (e.g.disabledinstead ofisDisabled,loadinginstead ofisLoading) - Callbacks not following
on{Verb}{Scope?}pattern (e.g.handleClickas a prop name) - Primary change callback named something other than
onChange(e.g.onValueChange,onSelectionChangefor the single primary value) - Visibility callbacks not using
onOpenChangepattern for layer components - Directional props using
left/rightinstead ofstart/end - Uncontrolled defaults not using
defaultprefix (e.g.initialValueinstead ofdefaultValue,initialIsOpeninstead ofdefaultIsOpen) - Boolean defaults not preserving
is/hasafterdefault(e.g.defaultOpeninstead ofdefaultIsOpen)
Exceptions:
- HTML attribute passthroughs (e.g.
onClick,onBlur) are fine as-is - Props prefixed with
htmlfor collision avoidance (e.g.htmlName,htmlFor) are fine
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
Components must follow the standard file structure and conventions.
Check for:
- Missing
displayNameon exported components - Missing file header with
@file,@input,@output,@position,SYNCblock - Components not extending
BaseProps(or an appropriate Omit of it) - Missing
xstyle,className,styleescape hatches - Not using
mergeProps()for style merging (e.g. separate spreads that overwrite) -
Missing
'use client'directive on files that use React client APIs. Runnode 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.
Components must follow slot, composition, and escape hatch conventions.
Check for:
- Slot props that hoist child state onto the parent (e.g. parent accepting
isMobileNavOpenwhen 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
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 -
statusprop not following{type, message?}shape -
sizevariants not using'sm' | 'md'(or'sm' | 'md' | 'lg'for non-inputs) - Missing
isLoading/isBusypattern for async-capable components - Using native
disabledfor busy state instead of visual-onlyaria-busy
Components must implement the required accessibility patterns.
Check for:
- Interactive components missing
labelprop - Missing
aria-required,aria-invalid,aria-describedbywiring on inputs -
isDisablednot mapping to nativedisabledattribute - Busy state using native
disabled(should usearia-busy+ visual treatment only) - Icon-only buttons missing
aria-label(should come fromlabelprop) -
addEventListenerusage instead of React event handlers - Focus not preserved during state transitions (elements removed from DOM instead of hidden)
Component exports must follow the standard patterns.
Check for:
- Components missing from package
src/index.tsre-exports - Missing type exports (props, variant, status types)
- Component not registered as a separate entry point in
tsup.config.ts
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.
Check memory/xds-night-watch-state.json for apiAuditor.lastRunAt. If it's from today (PST), skip and NO_REPLY.
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)
For each of the selected components:
- Read the source files — main component file, types, index.ts, hooks
- Check prop naming — scan the props interface for convention violations
-
Check type/context naming — verify all exported types use the unprefixed
<Component>*form - Check component structure — displayName, file header, BaseProps, mergeProps
- Check composition patterns — slot props, escape hatches, primitive reuse
- Check input consistency (if input component) — field props, status shape, size variants
- Check accessibility — label, ARIA attributes, focus management, event handling
- Check exports — index.ts, package index, tsup config
For each issue found:
- Log it to
memory/xds-night-watch/{date}.mdunder a## API Auditsection - 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
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 testandpnpm buildbefore 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
hardeningandapi - 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
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.
- 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)
{
"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"]
}
}interface DialogProps {
closable?: boolean; // ❌ missing `is` prefix
}interface DialogProps {
isClosable?: boolean; // ✅ `is` prefix
}interface SelectorProps {
onValueChange?: (value: string) => void; // ❌ primary callback should be `onChange`
}interface SelectorProps {
onChange?: (value: string) => void; // ✅ primary change callback
}interface TextInputProps {
leftIcon?: IconType; // ❌ should use `start`/`end`
}interface TextInputProps {
startIcon?: IconType; // ✅ RTL-friendly
}interface CollapsibleProps {
initialIsOpen?: boolean; // ❌ should be `defaultIsOpen`
}interface CollapsibleProps {
defaultIsOpen?: boolean; // ✅ follows React convention + Astryx boolean rules
}export function Card({ children }: CardProps) {
return <div>{children}</div>;
}
// ❌ no displayNameexport function Card({ children }: CardProps) {
return <div>{children}</div>;
}
Card.displayName = 'Card'; // ✅// ❌ Parent hoists child's state
<AppShell
mobileNav={content}
isMobileNavOpen={isOpen}
onMobileNavOpenChange={setOpen}
/>// ✅ Child owns its own state
<AppShell
mobileNav={<MobileNav isOpen={isOpen} onOpenChange={setIsOpen} />}
/>