Skip to content

Night Watch Doc Reviewer

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

Night Watch: Doc Reviewer Role

Assigned to: Joey's Navi (josephfarina) Goal: Keep component documentation compatible with Storybook autodocs, validate block/page template metadata, keep translation overlays (docsZh/docsDense) complete and faithful for both components and hooks, and catch content-level issues the type checker can't see. Frequency: Once per night.


Why This Role Exists

Astryx docs serve three audiences simultaneously:

  1. Humans browsing Storybook or reading .doc.mjs files for API reference
  2. LLMs consuming CLI output (xds component --brief, --compact, --lang zh, --lang dense, and xds hook) for code generation
  3. Storybook autodocs parsing JSDoc @example blocks for live previews

The .doc.mjs files are type-checked by tsc --checkJs in CI — structural issues (wrong fields, missing required properties, bad types) are caught automatically. This role focuses on what the type checker can't catch: Storybook docblock formatting, prop documentation drift against source code, template metadata accuracy, and showcase/block completeness.


Background: The Doc Format

Component docs live in typed .doc.mjs files. Each component directory has a {Name}.doc.mjs that exports a docs constant typed as ComponentDoc (defined in packages/core/src/docs-types.ts).

The CLI imports these directly with import() — no markdown parsing. Type checking runs via tsc --checkJs in CI (pnpm --filter @xds/core typecheck:docs), so structural validity is already enforced before merge.

Template System (PR #1393+)

Templates are split into two categories:

  • Page templates (templates/pages/) — Full-page scaffolding (dashboard, login, settings, etc.). Each has a template.doc.mjs with type: 'page'.
  • Block templates (templates/blocks/) — Smaller UI patterns and component examples. Each has a .doc.mjs with type: 'block', aspectRatio, and componentsUsed.

Component examples that used to live in .doc.mjs examples arrays have been migrated to standalone block template files under templates/blocks/components/<Component>/. The examples field on ComponentDoc and ComponentEntry is now optional.

The gallery preview ("hero") is no longer a showcase field on the component doc — that field was removed. The canonical hero is now a block template flagged isShowcase: true in its .doc.mjs (BlockTemplateDoc.isShowcase). See check 6.


Nightly Checklist

This role runs once per night. Check state to see if it has already run today — if so, skip.

Phase 1: Audit

Run all checks against origin/main. Collect findings into categories.

1. Storybook Compatibility — Docblock @example Blocks

Scan every Astryx*.tsx file (excluding *Context*, *test*, *story*) for JSDoc @example blocks.

Rules:

  • No ```tsx language tag. Storybook's autodocs parser chokes on the tsx tag — use bare ``` instead.
  • No blank lines inside code blocks. Storybook's markdown renderer can interpret a blank line as ending the code fence, causing the rest of the example to render as raw text/HTML. If you need to separate examples, use separate @example blocks or JS comments without blank line gaps. Important: "inside code blocks" means between the opening ``` and closing ```. Do NOT add extra blank lines before @example — if a blank * line already separates the description from @example, leave it as-is.
  • No JS comments (//) inside code blocks. Comments inside @example code blocks can confuse Storybook's markdown parser. If context is needed, put it in the JSDoc description above the @example tag, or use the example's surrounding description.
  • No bare > on its own line inside code blocks. If the code fence breaks (due to blank lines or parser issues), a bare > becomes a markdown blockquote. Restructure multi-line JSX to avoid a lone > — e.g., put > on the same line as the last prop or use a self-closing pattern.
  • Preserve indentation inside code blocks. JSX props and children must keep their indentation relative to the parent element. The * JSDoc prefix is followed by the code content with its natural indentation. Never flatten * prop="value" to * prop="value" — the spaces after * are the code's indentation and must be preserved. Only the content type (comments, blank lines) should be changed, never the whitespace structure.
  • Single concise example. Each @example should have exactly one code block showing the most common usage. Multiple code blocks confuse Storybook and bloat LLM context.
  • Every exported component needs an @example. If a component's JSDoc has no @example, Storybook autodocs shows nothing.

Good example format:

/**
 * Avatar component for displaying user profile pictures.
 *
 * @example
 * ```
 * <Avatar src="/user.jpg" name="John Doe" />
 * <Avatar name="Jane Smith" size="large" />
 * ```
 */

Bad example format (will break Storybook):

/**
 * @example
 * ```
 * // Basic usage           ← JS comment breaks parser
 * <Component foo="bar">
 *                          ← blank line ends code fence
 *   <Child />
 * </Component>
 *                          ← blank line
 * // Advanced usage        ← now renders as raw text
 * <Component baz="qux"
 * >                        ← bare > becomes blockquote
 *   <Child />
 * </Component>
 * ```
 */

Audit commands:

# Find ```tsx in docblocks
grep -rn '```tsx' packages/core/src/*/Astryx*.tsx

# Find components missing @example
# (check each Astryx*.tsx for @example in its main JSDoc)

Use the audit script to find blank lines, comments, and bare > inside code blocks:

# Scan all components for @example issues
python3 -c "
import re, glob, os
for f in sorted(glob.glob('packages/core/src/*/Astryx*.tsx')):
    if 'test' in f or 'story' in f or 'Context' in f: continue
    comp = os.path.basename(f).replace('.tsx', '')
    with open(f) as fh: content = fh.read()
    for m in re.finditer(r'/\*\*(.*?)\*/', content, re.DOTALL):
        block = m.group(1)
        if '@example' not in block: continue
        ex = block[block.index('@example'):]
        in_code = False
        issues = []
        for i, line in enumerate(ex.split('\n')):
            s = line.lstrip(' *')
            if s.startswith('\`\`\`'): in_code = not in_code; continue
            if in_code:
                if s.strip() == '': issues.append(f'  blank line at {i}')
                if '//' in s: issues.append(f'  JS comment at {i}: {s.strip()}')
                if s.strip() == '>': issues.append(f'  bare > at {i}')
        if issues:
            print(f'{comp}:')
            for issue in issues: print(issue)
"

2. Prop Documentation Drift

The type checker validates .doc.mjs structure but can't verify that the documented props match what the component actually accepts. For each component, compare:

  1. Props in the TypeScript interface (Astryx*Props)
  2. Props in the .doc.mjs props array

Flag any user-facing props that exist in code but not in docs. Skip props inherited from HTML attributes (e.g., onClick, onFocus) unless they have Astryx-specific behavior.

Note: The examples field is now optional on ComponentDoc and ComponentEntry. Do NOT flag missing examples as a prop drift issue — component examples now live as block templates.

Priority: Focus on props that affect behavior. Props inherited from HTML attributes (e.g., onClick, onFocus) don't need explicit documentation unless they have Astryx-specific behavior.

3. Block Template Coverage (replaces "Example Quality")

Component examples have moved from .doc.mjs examples arrays to standalone block template files in templates/blocks/components/<Component>/. For each component, verify:

  • At least one block template exists — check for a directory under templates/blocks/components/<Component>/ with at least one .tsx file
  • Block templates have matching .doc.mjs files — every .tsx in templates/blocks/ should have a matching .doc.mjs, and vice versa. Flag orphaned files.
  • Block .doc.mjs files have type: 'block' with aspectRatio (number > 0) and componentsUsed (string array)
  • Page .doc.mjs files have type: 'page' — every template.doc.mjs in templates/pages/ must have type: 'page'

Audit script:

# Check block template file pairing
python3 -c "
import glob, os

blocks_dir = 'packages/cli/templates/blocks'
pages_dir = 'packages/cli/templates/pages'

# Check block file pairing
tsx_files = set(os.path.splitext(f)[0] for f in glob.glob(f'{blocks_dir}/**/*.tsx', recursive=True))
doc_files = set(os.path.splitext(f)[0].replace('.doc', '') for f in glob.glob(f'{blocks_dir}/**/*.doc.mjs', recursive=True))

orphan_tsx = tsx_files - doc_files
orphan_doc = doc_files - tsx_files

if orphan_tsx:
    print(f'ORPHANED .tsx files (no matching .doc.mjs): {len(orphan_tsx)}')
    for f in sorted(orphan_tsx): print(f'  {f}.tsx')
if orphan_doc:
    print(f'ORPHANED .doc.mjs files (no matching .tsx): {len(orphan_doc)}')
    for f in sorted(orphan_doc): print(f'  {f}.doc.mjs')
if not orphan_tsx and not orphan_doc:
    print('All block template files are properly paired.')
"

# Check components with no block templates
python3 -c "
import glob, os

components = set()
for f in glob.glob('packages/core/src/*/Astryx*.tsx'):
    if 'test' in f or 'story' in f or 'Context' in f: continue
    comp = os.path.basename(os.path.dirname(f))
    components.add(comp)

blocks_dir = 'packages/cli/templates/blocks/components'
covered = set(os.path.basename(d) for d in glob.glob(f'{blocks_dir}/*/') if glob.glob(f'{d}/*.tsx'))

missing = components - covered
if missing:
    print(f'Components with NO block templates ({len(missing)}):')
    for c in sorted(missing): print(f'  {c}')
else:
    print('All components have at least one block template.')
"

4. Common Props Consistency

Several props appear across many components and must have consistent, accurate JSDoc comments in the TypeScript source (.tsx files) and entries in the .doc.mjs props arrays. These are NOT internal props — they're part of the public API and LLMs rely on them heavily.

For each component that accepts these props, verify:

xstyle — Must have this exact JSDoc pattern:

/**
 * StyleX styles for layout customization (margins, positioning, sizing).
 * Must be a `stylex.create()` value — not an inline style object.
 *
 * @example
 * ```
 * const styles = stylex.create({ wrapper: { marginTop: 8 } });
 * <Component xstyle={styles.wrapper} />
 * ```
 */
xstyle?: StyleXStyles;

And this .doc.mjs entry:

{
  name: 'xstyle',
  type: 'StyleXStyles',
  description: 'StyleX styles for layout customization (margins, positioning, sizing). Must be a stylex.create() value — not an inline style object like style={{}}.',
}

Why this matters: LLMs frequently pass inline style objects to xstyle (e.g., xstyle={{ marginTop: 8 }}). This compiles but breaks StyleX's static extraction. The JSDoc and doc entry must make clear that only stylex.create() values are valid.

id — If the component accepts an id prop (via HTML attributes or explicit declaration):

{
  name: 'id',
  type: 'string',
  description: 'HTML id attribute. Useful for anchor links, label associations, and test selectors.',
}

data-testid — If the component accepts data-testid:

{
  name: 'data-testid',
  type: 'string',
  description: 'Test selector for automated testing frameworks.',
}

className — If the component accepts className:

{
  name: 'className',
  type: 'string',
  description: 'CSS class name for the root element. Prefer xstyle for styling — className is provided for integration with non-StyleX systems.',
}

style — If the component accepts style:

{
  name: 'style',
  type: 'CSSProperties',
  description: 'Inline styles for the root element. Prefer xstyle for styling — inline styles bypass StyleX optimization.',
}

Audit steps:

  1. For each component with xstyle in its props interface, check the JSDoc comment. If it's a generic description like "StyleX styles to apply to the container" — flag it for the stylex.create() guidance.
  2. For .doc.mjs files, check if common props are present when the component accepts them. Add missing entries.
  3. Don't add common prop entries for components that don't accept them (e.g., don't add xstyle to a component that doesn't have it in its interface).

5. Theming Section Accuracy

Every component that calls xdsThemeProps() in its source should have a matching theming section in its .doc.mjs. The theming section documents the CSS class names and visual props that defineTheme can target via @scope selectors.

Format:

theming: {
  targets: [
    {className: 'astryx-button', visualProps: ['size', 'variant']},
  ],
},
  • className — the stable CSS class name from xdsThemeProps('button', ...)astryx-button
  • visualProps — the prop names passed as the second argument to xdsThemeProps() (the variant classes)

Audit steps:

  1. Extract all xdsThemeProps() calls from source (excluding test files)
  2. Extract all theming.targets from .doc.mjs files
  3. Flag any mismatches:
    • Component has xdsThemeProps() but no theming section in docs → missing
    • theming.targets lists a className that doesn't exist in source → stale
    • visualProps in docs don't match the props passed to xdsThemeProps()drift

Audit script:

python3 -c "
import re, glob, os

# Collect xdsThemeProps calls from source
source_map = {}  # className -> set of visualProps
for f in sorted(glob.glob('packages/core/src/**/*.tsx', recursive=True)):
    if '.test.' in f: continue
    with open(f) as fh:
        for m in re.finditer(r\"xdsThemeProps\('([^']+)'(?:,\s*\{([^}]*)\})?\)\", fh.read()):
            cn = m.group(1)
            if cn not in source_map: source_map[cn] = set()
            if m.group(2):
                for p in re.findall(r'(\w+)', m.group(2)):
                    source_map[cn].add(p)

# Collect theming targets from docs
doc_map = {}  # className -> set of visualProps
for f in sorted(glob.glob('packages/core/src/*/*.doc.mjs')):
    with open(f) as fh: content = fh.read()
    for m in re.finditer(r\"className:\s*'astryx-([^']+)',\s*visualProps:\s*\[([^\]]*)\]\", content):
        cn = m.group(1)
        doc_map[cn] = set(re.findall(r\"'(\w+)'\", m.group(2)))
    for m in re.finditer(r\"className:\s*'astryx-([^']+)'\", content):
        cn = m.group(1)
        if cn not in doc_map: doc_map[cn] = set()

# Compare
for cn in sorted(set(source_map) & set(doc_map)):
    if source_map[cn] != doc_map[cn]:
        print(f'DRIFT astryx-{cn}: source={sorted(source_map[cn])} docs={sorted(doc_map[cn])}')
for cn in sorted(set(source_map) - set(doc_map)):
    print(f'MISSING astryx-{cn}: has xdsThemeProps() but no theming doc')
for cn in sorted(set(doc_map) - set(source_map)):
    print(f'STALE astryx-{cn}: in docs but no xdsThemeProps() in source')
if not (set(source_map) - set(doc_map)) and not (set(doc_map) - set(source_map)):
    if all(source_map.get(cn, set()) == doc_map.get(cn, set()) for cn in source_map):
        print('All theming docs are in sync with source!')
"

Fix: Add or update the theming section. Derive targets directly from xdsThemeProps() calls — don't guess.

5b. CSS Property Documentation

Components that expose public CSS custom properties for theme overrides must document them in their .doc.mjs theming.cssProperties array. This field was added in PR #850 — see CSSPropertyDoc in docs-types.ts.

Format:

theming: {
  targets: [{className: 'astryx-card'}],
  cssProperties: [{
    name: '--astryx-card-padding',
    description: 'Controls Card container padding. Set in theme component overrides.',
    default: 'var(--spacing-4)',
  }],
},

Audit steps:

  1. Search for --astryx- CSS custom properties set or read in component source files (excluding test files):
    grep -rn '\-\-astryx-' packages/core/src/ --include="*.tsx" --include="*.ts" | grep -v test | grep -v node_modules
  2. For each --astryx-* property found, check if the component's .doc.mjs has a matching cssProperties entry
  3. Flag any mismatches:
    • Component uses --astryx-* property but no cssProperties in docs → missing
    • cssProperties lists a property not found in source → stale
    • Description or default is inaccurate → drift

6. Hero Showcase Completeness

Updated (block-template model): The old showcase field on component .doc.mjs was removed — it no longer exists in ComponentDoc. The canonical preview is now a block template flagged isShowcase: true in its .doc.mjs (BlockTemplateDoc.isShowcase, see docs-types.ts). Do NOT check for a showcase field on component docs — that produces ~107 false positives. Check that each component's block-template directory has exactly one hero block instead.

Every component should have exactly one canonical hero block: a .doc.mjs under packages/cli/templates/blocks/components/<Component>/ with isShowcase: true.

Audit script:

python3 -c "
import glob, os, re

base = 'packages/cli/templates/blocks/components'
for d in sorted(glob.glob(f'{base}/*/')):
    comp = os.path.basename(d.rstrip('/'))
    hero = 0
    for doc in glob.glob(f'{d}*.doc.mjs'):
        with open(doc) as fh: c = fh.read()
        if re.search(r'isShowcase:\s*true', c): hero += 1
    if hero == 0:
        print(f'MISSING hero block (isShowcase: true): {comp}')
    elif hero > 1:
        print(f'MULTIPLE hero blocks ({hero}): {comp} — only one block should be isShowcase: true')
"

A clean run prints nothing. A component with no isShowcase: true block has no gallery hero; more than one is ambiguous. Fix by setting isShowcase: true on the single most representative block .doc.mjs for that component (and removing it from any others).

7. Block Template componentsUsed Accuracy

For each block .tsx file, parse the imports for @xds/core/* and compare against the componentsUsed array in the matching .doc.mjs. Flag mismatches:

  • Block imports Button from @xds/core/Button but doesn't list 'Button' in componentsUsedmissing
  • componentsUsed lists 'Button' but the block doesn't import from @xds/core/Buttonstale

Audit script:

python3 -c "
import glob, re, os

blocks_dir = 'packages/cli/templates/blocks'
issues = []

for doc_file in sorted(glob.glob(f'{blocks_dir}/**/*.doc.mjs', recursive=True)):
    tsx_file = doc_file.replace('.doc.mjs', '.tsx')
    if not os.path.exists(tsx_file): continue

    with open(doc_file) as f: doc_content = f.read()
    with open(tsx_file) as f: tsx_content = f.read()

    # Extract componentsUsed from doc
    m = re.search(r'componentsUsed:\s*\[([^\]]*)\]', doc_content)
    if not m: continue
    documented = set(re.findall(r\"'(\w+)'\", m.group(1)))

    # Extract @xds/core imports from tsx
    imported = set()
    for im in re.finditer(r\"from\s+['\\\"]@xds/core/(\w+)['\\\"]\" , tsx_content):
        imported.add(im.group(1))

    missing = imported - documented
    stale = documented - imported

    name = os.path.basename(tsx_file).replace('.tsx', '')
    if missing:
        issues.append(f'{name}: imports {sorted(missing)} but not in componentsUsed')
    if stale:
        issues.append(f'{name}: componentsUsed lists {sorted(stale)} but not imported')

if issues:
    print(f'componentsUsed drift ({len(issues)} blocks):')
    for i in issues: print(f'  {i}')
else:
    print('All block componentsUsed arrays match imports.')
"

8. Block Template Type Safety

Run the block template typecheck and flag any new errors:

npx tsc --project packages/cli/tsconfig.template-docs.json --noEmit 2>&1 | head -50

Also scan for @ts-expect-error or @ts-ignore comments creeping back into block .doc.mjs files:

grep -rn '@ts-expect-error\|@ts-ignore' packages/cli/templates/blocks/ --include="*.doc.mjs"

8b. Playground Defaults and slotElements

PR #2005 added two fields to the component doc system for the interactive playground:

playground.defaults (on BaseDoc) — initial prop values for the playground preview. Values can be primitives or ElementDescriptor objects (serializable React element specs).

slotElements (on PropDoc) — declares what Astryx components a ReactNode prop typically accepts. The playground renders a toggle/selector control based on this.

When to add slotElements:

Prop pattern Action Example
icon, startIcon, endIcon, etc. Add [{__element: 'Icon', props: {icon: 'check', size: 'sm'}}] Button.icon
endContent with ReactElement<IconProps> | ReactElement<BadgeProps> Add both options Button.endContent
actions Add [{__element: 'Button', props: {label: 'Action', variant: 'secondary'}}] EmptyState.actions
status on Avatar Add [{__element: 'StatusDot', props: {variant: 'online'}}] Avatar.status
Named composition slots (topNav, sideNav, banner) Add the expected component AppShell.topNav
label, title, description, name (text-only) Skip — text input is correct Badge.label
Render functions (item: T) => ReactNode Skip — not an element slot Selector.children
children on generic containers Skip — too many possible values Card.children
Compound component sub-entries (TableRow, usePopover) Skip — documentation, not interactive Table.TableRow

When to add playground.defaults:

  • Component needs children to render visibly (Card, Dialog, Section)
  • Component has required props that benefit from better defaults (label: 'Click me' vs label: 'label')
  • Component renders broken without specific prop combinations

Format:

playground: {
  defaults: {
    label: 'Click me',
    variant: 'primary',
    children: {
      __element: 'VStack', props: {gap: 2}, children: [
        {__element: 'Heading', props: {level: 3}, children: 'Title'},
        {__element: 'Text', props: {type: 'body'}, children: 'Content'},
      ],
    },
  },
},

Audit script:

python3 -c "
import re, glob, os

src = 'packages/core/src'
for f in sorted(glob.glob(f'{src}/*/*.doc.mjs')):
    with open(f) as fh: content = fh.read()
    comp = os.path.basename(f).replace('.doc.mjs', '')
    
    # Find ReactNode props without slotElements
    missing = []
    for m in re.finditer(r\"\\{\\s*\\n?\\s*name:\\s*['\"](\w+)['\"][^}]*?type:\\s*['\"]([^'\"]+)['\"]\", content):
        pname, ptype = m.group(1), m.group(2)
        if 'ReactNode' not in ptype and 'ReactElement' not in ptype: continue
        # Check if slotElements exists nearby
        block_end = content.find('}', m.end())
        block = content[m.start():block_end+1] if block_end > 0 else ''
        if 'slotElements' in block: continue
        # Skip text props, render functions, compound entries, children
        if pname in ('label','title','description','name'): continue
        if '=>' in ptype: continue
        if pname.startswith('Astryx') or pname.startswith('use'): continue
        if pname[0:1].isupper(): continue
        if pname == 'children': continue
        if pname in ('element','layerNode'): continue
        missing.append(pname)
    
    if missing:
        print(f'{comp}: missing slotElements on {missing}')
"

Reference:

  • Type definitions: packages/core/src/docs-types.ts (ElementDescriptor, PlaygroundConfig, PropDoc.slotElements)
  • Issue tracking remaining work: #2008
  • Existing examples: Button.doc.mjs, Card.doc.mjs, Badge.doc.mjs, EmptyState.doc.mjs

9. Page Template Type Field

Every template.doc.mjs in templates/pages/ must have type: 'page'. Every .doc.mjs in templates/blocks/ must have type: 'block'.

python3 -c "
import glob, re

# Pages must have type: 'page'
for f in sorted(glob.glob('packages/cli/templates/pages/*/template.doc.mjs')):
    with open(f) as fh: content = fh.read()
    if \"type: 'page'\" not in content and 'type: \"page\"' not in content:
        print(f'MISSING type: page in {f}')

# Blocks must have type: 'block'
for f in sorted(glob.glob('packages/cli/templates/blocks/**/*.doc.mjs', recursive=True)):
    with open(f) as fh: content = fh.read()
    if \"type: 'block'\" not in content and 'type: \"block\"' not in content:
        print(f'MISSING type: block in {f}')
"

10. Aspect Ratio Sanity

Flag blocks with aspectRatio: 0, negative values, or obviously wrong values:

python3 -c "
import glob, re

for f in sorted(glob.glob('packages/cli/templates/blocks/**/*.doc.mjs', recursive=True)):
    with open(f) as fh: content = fh.read()
    m = re.search(r'aspectRatio:\s*([\d./-]+)', content)
    if not m:
        name = f.split('/')[-1].replace('.doc.mjs', '')
        print(f'MISSING aspectRatio: {name}')
        continue
    try:
        val = eval(m.group(1))
        name = f.split('/')[-1].replace('.doc.mjs', '')
        if val <= 0:
            print(f'BAD aspectRatio ({val}): {name}')
        elif val > 10:
            print(f'SUSPICIOUS aspectRatio ({val}): {name} — very wide, possibly miscategorized')
    except:
        pass
"

11. Examples Field Removal Watchdog

The examples array was removed from all component .doc.mjs files in PR #1393. If someone adds one back, flag it and suggest creating a block template instead.

python3 -c "
import glob, re

for f in sorted(glob.glob('packages/core/src/*/*.doc.mjs')):
    with open(f) as fh: content = fh.read()
    # Look for examples: [ with actual content (not just examples: [])
    if re.search(r'examples:\s*\[(?!\s*\])', content):
        comp = f.split('/')[-1].replace('.doc.mjs', '')
        print(f'FOUND examples array in {comp} — should be a block template instead')
"

12. Import Path Consistency

Watch for wrong relative import paths in .doc.mjs files. Page templates and blocks have different depths:

  • Page templates: ../../../../core/src/docs-types
  • Block templates: ../../../../../core/src/docs-types

Flag any that use incorrect relative paths:

python3 -c "
import glob, re

# Check page template imports
for f in sorted(glob.glob('packages/cli/templates/pages/*/template.doc.mjs')):
    with open(f) as fh: content = fh.read()
    if 'docs-types' in content:
        if '../../../../core/src/docs-types' not in content:
            print(f'WRONG import path in page template: {f}')

# Check block template imports
for f in sorted(glob.glob('packages/cli/templates/blocks/**/*.doc.mjs', recursive=True)):
    with open(f) as fh: content = fh.read()
    if 'docs-types' in content:
        if '../../../../../core/src/docs-types' not in content:
            print(f'WRONG import path in block template: {f}')
"

13. Translation Coverage & Fidelity (docsZh / docsDense)

Astryx ships translated/compressed doc overlays alongside the English docs export in the same .doc.mjs file:

  • docsZh — Chinese Simplified translation. Served by xds component <Name> --lang zh.
  • docsDense — token-compressed English. Served by xds component <Name> --lang dense.

Both are typed as TranslationDoc (components) or HookTranslationDoc (hooks) in docs-types.ts. The CLI loader (packages/cli/src/lib/component-loader.mjs, mergeTranslation) merges the overlay onto the English docs at render time. When an overlay is missing or incomplete, the CLI silently falls back to English — the user sees uncompressed/untranslated output with no error. This check exists to catch that silent fidelity loss the type checker can't see.

Scope note: This applies to both components and hooks, and to theme docs (packages/core/src/theme/*.doc.mjs, reached via xds component <ThemeName>). Hooks live at packages/core/src/hooks/*.doc.mjs and co-located packages/core/src/<Comp>/use*.doc.mjs.

What the loader actually merges (anything else in an overlay is dead — flag it):

Doc kind English fields Overlay (TranslationDoc / HookTranslationDoc) fields merged
Component usage.description, usage.bestPractices, props[], components[] description, usage.description, usage.bestPractices, propDescriptions (keyed by prop name), components[] (by name)
Hook usage.description, usage.bestPractices, params[], returns[] description, usage.description, usage.bestPractices, paramDescriptions (keyed by param name, incl. dotted options.foo), returnDescriptions (keyed by return field name)

Hard rules the overlay must satisfy (these mirror the dense compression protocol in .claude/skills/dense-compression-protocol.md — read it before fixing):

  • Coverage: every component, hook, and theme doc with a docs export MUST have a docsDense export. (docsZh coverage is tracked but lower priority — backfill where present and drifted, flag where wholly missing.)
  • Count parity 1:1: overlay usage.bestPractices must match English length, order, AND guidance: true/false flag per position. Same for features, notes, accessibility if present. Never pack two English bullets into one.
  • Prop/param/return coverage: propDescriptions (component) / paramDescriptions + returnDescriptions (hook) must have an entry for every English prop/param/return that has a description, except universal props (children, ref, key, style, className, xstyle).
  • Sub-components: component overlay components[] must include every docs.components[].name (including hook sub-entries like useImperativeDialog).
  • No invalid schema: the only valid overlay keys are those in the table above. Short-key schemas (n, d, kw, p, ex) are dead — the loader never reads them, so the data silently doesn't render. Convert any short-key overlay to the canonical TranslationDoc shape (ddescription, ppropDescriptions, drop n/kw/ex).
  • No duplicate (required) markers: the renderer auto-appends **(required)** from the English required: true field. Overlay text must NOT also contain a literal (required) — that double-renders.
  • Capitalization convention (match merged precedent): prop/param/return description fragments are lowercase-leading (e.g. "card width", "whether trap active"); bestPractices are capital-leading imperative sentences.
  • Signal words + identifiers preserved: keep if/when/unless/only/must/never/always, full Astryx* cross-reference names + the relationship word (e.g. "use Foo instead"), and technical identifiers (event names, ARIA terms, types).

Audit script (a portable harness lives in the workspace at xds-worktrees/.tools/dense-audit.mjs — if present, prefer node <that> packages/core/src --hooks; otherwise inline):

node -e '
import {readdirSync, statSync} from "node:fs";
import {join, relative} from "node:path";
import {pathToFileURL} from "node:url";
const ROOT="packages/core/src", UNIVERSAL=new Set(["children","ref","key","style","className","xstyle"]);
const len=x=>Array.isArray(x)?x.length:0;
const walk=(d,o=[])=>{for(const e of readdirSync(d)){const p=join(d,e),s=statSync(p);s.isDirectory()?walk(p,o):e.endsWith(".doc.mjs")&&o.push(p);}return o;};
const I={missing:[],bullet:[],comp:[],prop:[],param:[],ret:[],shortkey:[]};
for(const f of walk(ROOT).sort()){
  let m;try{m=await import(pathToFileURL(f).href);}catch{continue;}
  const d=m.docs,t=m.docsDense,rel=relative(ROOT,f);if(!d)continue;
  const isHook=Array.isArray(d.params)||Array.isArray(d.returns);
  if(!t){I.missing.push(rel+(isHook?" [hook]":""));continue;}
  for(const k of ["n","d","kw","p","ex"])if(k in t){I.shortkey.push(rel+" key:"+k);break;}
  if(len(d.usage?.bestPractices)!==len(t.usage?.bestPractices))I.bullet.push(`${rel}: ${len(d.usage?.bestPractices)} vs ${len(t.usage?.bestPractices)}`);
  const dc=(d.components||[]).map(c=>c.name),tc=new Set((t.components||[]).map(c=>c.name));
  const dropped=dc.filter(n=>!tc.has(n));if(dropped.length)I.comp.push(`${rel}: dropped ${dropped.join(", ")}`);
  const pg=(Array.isArray(d.props)?d.props:[]).filter(p=>p?.name&&!UNIVERSAL.has(p.name)&&p.description&&!(p.name in (t.propDescriptions||{}))).map(p=>p.name);
  if(pg.length)I.prop.push(`${rel}: ${pg.join(",")}`);
  if(isHook){
    const pm=(d.params||[]).filter(p=>p?.name&&p.description&&!(p.name in (t.paramDescriptions||{}))).map(p=>p.name);
    if(pm.length)I.param.push(`${rel}: ${pm.join(",")}`);
    const rm=(d.returns||[]).filter(r=>r?.name&&r.description&&!(r.name in (t.returnDescriptions||{}))).map(r=>r.name);
    if(rm.length)I.ret.push(`${rel}: ${rm.join(",")}`);
  }
}
for(const[k,v]of Object.entries(I)){console.log(`## ${k}: ${v.length}`);v.forEach(x=>console.log("   "+x));}
'

Also spot-check rendering after any fix — confirm the dense/zh output actually changed and the duplicate-(required) bug is absent:

node packages/cli/bin/xds.mjs component Card --lang dense | head -20
node packages/cli/bin/xds.mjs hook useFocusTrap --lang dense | sed -n '/Parameters/,/Returns/p'   # expect single **(required)**

14. Hooks Doc Quality

The checks above (prop drift, common props, showcase) were originally written for components and glob Astryx*.tsx / component .doc.mjs. Extend them to hooks (packages/core/src/hooks/*.doc.mjs and co-located use*.doc.mjs):

  • Every hook .doc.mjs params[] and returns[] should match the hook's TypeScript signature (param/return drift — the hook analog of prop drift).
  • Hook docs use HookDoc (with params/returns), not ComponentDoc — do NOT flag missing props/showcase/theming on hooks (those fields don't apply).
  • Every hook should have a docsDense (see check 13).

17. AI-Slop Prose Tells

Doc prose should read like a careful engineer wrote it, not like an LLM generated it. Scan every prose string (description, text, title, guidance descriptions, best-practices entries) and README body for the AI-slop tells below, and fix them. This is the full rubric; em dashes are only one row of it.

This is a prose check. It applies to natural-language strings only, never to code (inside backticks, code fences, // or /* */ comments), name:/displayName: Component — Variant convention labels, prop/token identifiers, or CJK punctuation.

Sub Tell Fix
A1 Em dash in prose Recast with the right punctuation: colon (list/definition intro), semicolon (two independent clauses), comma (appositive/contrast aside), parentheses (nested aside with internal commas), or period (full sentence break).
A2 En dash in prose (non-numeric) Same as A1. Leave numeric ranges (20–31px, h1–h6, 2–7).
A3 Curly double quotes “ ” Straight ". Escape correctly for the surrounding JS string.
A4 Curly single quotes / apostrophes ‘ ’ Straight '. In a single-quoted JS string, escape the apostrophe (audience\'s) or the file won't parse.
A5 Ellipsis char ASCII ... (or the word "to" for a range).
B Vocabulary filler / buzzwords Cut or replace: seamless(ly), leverage→use, utilize→use, robust, delve, elevate, unlock, empower, streamline, harness, boast, cutting-edge / state-of-the-art / best-in-class / world-class, game-changer, supercharge / turbocharge, powerful, intuitive, effortless(ly), plethora / myriad, tapestry / realm of / landscape of.
C Significance padding Delete the throat-clearing: "it's worth noting", "it's important to note/remember", leading "Importantly,/Notably,/Crucially,/Interestingly,", "plays a crucial/key/vital/pivotal role", "is a testament to", "in today's fast-paced/digital/modern world", "when it comes to", "at the end of the day", "needless to say".
D Structural / rhetorical tells Rewrite plainly: "not only … but also", "isn't just X — it's Y" / "more than just", sweeping "From X to Y, …" openers, "whether you're X or Y", "it's not about X, it's about Y" antithesis.
E Hollow intensifiers / hedging Cut the empty word: very, really, truly, incredibly, extremely, highly, quite, simply, easily, effortlessly; hedging clusters "can help to", "may potentially", "might possibly"; over-enthusiasm ("Great!", "Amazing!", stray exclamation marks).
F Redundancy / wordiness Tighten: "in order to"→to, "due to the fact that"→because, "a variety of / a number of" (be specific), "various different", pleonasms ("absolutely essential", "end result", "final outcome", "advance planning").

Meaning is sacred. Only remove filler and normalize typography; never delete information, never reword a technical claim, never fabricate. If cutting a word changes the meaning, leave it. Regex is for detection only; every fix is a judgment call made by reading the sentence.

Triage before fixing. These are NOT slop:

  • API/domain identifiers that happen to match a buzzword: isShowcase, showcase blocks/registry, the 'elevated' prop value/token, "test harness", "flexible" when it describes flexbox behavior, "first-class" when it means genuine first-class support, "scroll lock". Match the whole word in prose, then confirm it isn't a real term.
  • Numeric/identifier ranges with dashes (h1–h6, 6–8, 0–2).
  • Chinese double em-dash —— (correct CJK punctuation). For a single inside CJK prose, use or .
# Detection helper (DETECTION ONLY; then read each hit and judge):
# Typographic tells outside code:
grep -rnP '[\x{2014}\x{2013}\x{2018}\x{2019}\x{201c}\x{201d}\x{2026}]' packages apps internal \
  --include='*.doc.mjs' --include='*.md' | grep -v node_modules
# Vocab/filler (case-insensitive, word-boundary), e.g.:
grep -rniE '\b(seamless(ly)?|leverage|utilize|robust|delve|effortless(ly)?|plethora|myriad|cutting[- ]edge|game[- ]chang(er|ing))\b' \
  packages apps internal --include='*.doc.mjs' --include='*.md' | grep -v node_modules
# Significance padding / hedging:
grep -rniE "it'?s worth noting|it'?s important to (note|remember)|plays? a (crucial|key|vital|pivotal) role|in today'?s .* world|can help to|may potentially" \
  packages apps internal --include='*.doc.mjs' --include='*.md' | grep -v node_modules

After fixing a .doc.mjs, always parse-check it (node --input-type=module -e "import('file://$PWD/<file>').then(()=>{}).catch(e=>{console.error(e.message);process.exit(1)})"). The most common breakage is an unescaped apostrophe created when straightening a curly quote inside a single-quoted string.


Phase 2: Fix

Group findings and create PRs. Each PR should be focused on one category of fix.

PR Categories

Category Branch Name Scope
Storybook compat navi/docs/storybook-compat Fix ```tsx```, remove blank lines/comments/bare > from code blocks, consolidate multiple examples, add missing @example
Common props navi/docs/common-props Add/standardize xstyle, id, className, data-testid, style prop docs
Prop drift navi/docs/prop-docs-{component} Add undocumented props to .doc.mjs props arrays
Theming sync navi/docs/theming-sync Add/fix theming sections to match xdsThemeProps() calls in source
Block template metadata navi/docs/block-metadata Fix componentsUsed accuracy, missing type fields, aspect ratio issues, file pairing
Hero showcase navi/docs/showcase Set isShowcase: true on the canonical hero block template for components missing one
Import paths navi/docs/import-paths Fix incorrect relative import paths in template .doc.mjs files
Block coverage navi/docs/block-coverage-{component} Add missing block templates for components with no examples
Examples removal navi/docs/remove-examples Remove examples arrays that crept back into component .doc.mjs files
Translation coverage navi/docs/dense-coverage Add missing docsDense (and backfill drifted docsZh), fix count/prop/param/return parity, convert invalid short-key overlays, drop duplicate (required) markers
Hooks docs navi/docs/hooks-{name} Fix hook param/return drift and add missing hook docsDense
AI-slop prose navi/docs/slop-{nn} Remove AI-slop tells (em/en dashes, curly quotes, ellipsis chars, vocab filler, significance padding, structural tells, hollow intensifiers, redundancy) per check 17. One file per commit; prose strings only; meaning preserved.

Creating Fix PRs

  1. Create a worktree:

    cd /vercel/sandbox/repos/xds
    git worktree add /vercel/sandbox/repos/xds-worktrees/night-watch-docs origin/main --detach
    cd /vercel/sandbox/repos/xds-worktrees/night-watch-docs
    git checkout -b navi/docs/{category}
  2. Make fixes. For each fix:

    • Read the source file to understand the actual props/behavior
    • Update the .doc.mjs (or JSDoc @example for Storybook fixes)
    • Keep changes minimal — fix what's wrong, don't rewrite what's fine
  3. Validate fixes:

    # Verify type checking passes
    pnpm --filter @xds/core typecheck:docs
    
    # Verify block template types pass
    npx tsc --project packages/cli/tsconfig.template-docs.json --noEmit
    
    # Verify CLI renders correctly
    node packages/cli/bin/xds.mjs component {Name} --brief
    node packages/cli/bin/xds.mjs component {Name} --props
    
    # Build to ensure no breakage
    pnpm build
  4. Commit and PR:

    git add -A
    git commit -m "docs({scope}): {description}
    
    Found during Night Watch doc review."
    git push -u origin navi/docs/{category}
    gh pr create --title "docs({scope}): {description}" \
      --body "## What
    
    {summary of findings and fixes}
    
    ## Checklist
    - [ ] \`pnpm --filter @xds/core typecheck:docs\` passes
    - [ ] \`npx tsc --project packages/cli/tsconfig.template-docs.json --noEmit\` passes
    - [ ] CLI \`--brief\` output verified
    - [ ] CLI \`--props\` output verified
    - [ ] No Storybook \`\`\`tsx\` in docblocks
    - [ ] No blank lines, JS comments, or bare \`>\` inside \`@example\` code blocks
    - [ ] Single concise \`@example\` per component
    - [ ] Common props (\`xstyle\`, \`id\`, \`className\`, etc.) documented consistently
    - [ ] Block templates paired with \`.doc.mjs\` files
    - [ ] \`componentsUsed\` matches actual imports
    - [ ] No \`examples\` arrays in component \`.doc.mjs\` files
    
    ---
    *Night Watch — Doc Reviewer*"
  5. If there are no findings in a category, skip that PR. Don't create empty PRs.

Fix Guidelines

  • .doc.mjs props: Match the exact prop name and type from the TypeScript definition. Use required: true for required props. Omit default for required props or props with no default.
  • Block templates: When creating new block templates, include both a .tsx file with realistic JSX and a .doc.mjs with type: 'block', aspectRatio, and componentsUsed matching the actual imports.
  • Don't rewrite good docs. If a .doc.mjs is well-structured and just missing one prop, add the prop. Don't reorganize the whole file.
  • Don't add props that are intentionally undocumented. If a prop exists in the TypeScript interface but seems purely internal, skip it and note it in the PR description. However, common props like xstyle, id, className, and data-* attributes should always be documented.
  • Don't re-add examples arrays. If a component needs usage examples, create block templates instead.

Phase 3: Report

15. Log Results

Write a summary to memory/xds-night-watch/{date}.md (append to existing if other roles have already logged):

## Doc Reviewer — {timestamp}

### Findings
- Storybook compat: {n} files with ```tsx, {n} with blank lines/comments in code blocks, {n} missing @example
- Common props: {n} components missing xstyle/id/className docs
- Prop drift: {n} components with undocumented props ({total} props total)
- Theming sync: {n} missing, {n} stale, {n} drifted theming sections
- Block coverage: {n} components with no block templates
- Block metadata: {n} blocks with componentsUsed drift, {n} orphaned files, {n} missing type fields
- Block type safety: {n} tsc errors, {n} @ts-expect-error comments
- Hero showcase: {n} components missing an isShowcase block, {n} with multiple
- Aspect ratio: {n} blocks with bad/suspicious ratios
- Examples watchdog: {n} components with stale examples arrays
- Import paths: {n} files with wrong relative paths
- Translation coverage: {n} missing docsDense, {n} bullet-count drifts, {n} prop/param/return coverage gaps, {n} invalid short-key overlays, {n} duplicate (required) markers
- Hooks docs: {n} hooks with param/return drift, {n} hooks missing docsDense
- AI-slop prose: {n} files with tells fixed (A typographic: {n}, B vocab: {n}, C padding: {n}, D structural: {n}, E intensifiers: {n}, F redundancy: {n})

### PRs Created
- #{number}: {title}
- #{number}: {title}

### Skipped (already clean)
- {n} components passed all checks

16. Notify

Send a summary card to your human with:

  • Count of findings per category
  • Links to PRs created
  • Any components that need human judgment (e.g., "should this prop be documented or is it internal?")

Does NOT Do

  • ❌ Fix CI or push code fixes (that's QA)
  • ❌ Review PR code quality (that's Reviewer)
  • ❌ Triage issues (that's PM)
  • ❌ Rewrite component implementations
  • ❌ Add new components or features
  • ❌ Make subjective documentation style changes beyond the AI-slop rubric (check 17). Removing slop tells is in scope; re-voicing prose to taste is not.
  • Produce AI-slop in your own writing. PR bodies, commit messages, code comments, and any doc prose you author must not contain the tells in check 17: no em dashes, no "seamless/leverage/robust", no "it's worth noting", no hollow intensifiers. Fixing slop while writing slop is self-defeating.
  • ❌ Approve or merge PRs
  • ❌ Validate .doc.mjs type structure (CI handles this via tsc --checkJs)

State

Track state in memory/xds-night-watch-state.json:

{
  "role": "doc-reviewer",
  "lastRun": null,
  "lastRunDate": null,
  "prsCreated": [],
  "findings": {
    "storybookCompat": 0,
    "propDrift": 0,
    "themingSync": 0,
    "commonProps": 0,
    "blockCoverage": 0,
    "blockMetadata": 0,
    "blockTypeSafety": 0,
    "showcaseCompleteness": 0,
    "aspectRatioSanity": 0,
    "examplesWatchdog": 0,
    "importPaths": 0,
    "translationCoverage": 0,
    "hooksDocs": 0
  },
  "runsToday": 0
}

Clone this wiki locally