Skip to content

Navigation Menu

Sign in
Sign up

Night Watch Doc Reviewer

Cindy Zhang edited this page Aug 11, 2026 · 18 revisions

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, keep the CLI's own colocated docs (FunctionDoc/CommandDoc/SchemaDoc/EnumDoc) present and in sync with the live CLI, and catch content-level issues the type checker can't see.

Grading vs enforcement. The graded doc criteria — what must exist, at what severity, and how it scores — live in Component Audit Rubric §8. This page is the enforcement layer the rubric cites by name: the per-check detail and the scripts that produce the findings. Keep both in step; when they disagree, the rubric states the bar and this page states how it is measured.

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 (astryx component --brief, --compact, --lang zh, --lang dense, and astryx 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 (exported from @astryxdesign/cli/authoring).

The CLI imports these directly with import() — no markdown parsing. Type checking runs via tsc --checkJs in CI (pnpm --filter @astryxdesign/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 component and hook .tsx source file under packages/core/src and packages/lab/src (excluding *Context*, *.test.*, *.story*/*.stories.*) for JSDoc @example blocks. NOTE: components were unprefixed in the Astryx rebrand — they are NO LONGER named Astryx*.tsx. A glob like packages/core/src/*/Astryx*.tsx matches ZERO files now and makes this check silently pass on nothing. Scan bare-named .tsx recursively instead.

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 (bare-named components/hooks, both packages; exclude tests)
grep -rn '```tsx' packages/core/src packages/lab/src --include='*.tsx' | grep -viE '\.(test|story|stories)\.|Context'
# Find components missing @example
# (check each component/hook .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
tsx_files = glob.glob('packages/core/src/**/*.tsx', recursive=True) + \
 glob.glob('packages/lab/src/**/*.tsx', recursive=True)
for f in sorted(tsx_files):
 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/assets/templates/blocks'
pages_dir = 'packages/cli/assets/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/assets/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 themeProps() 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 themeProps('button', ...)astryx-button
  • visualProps — the prop names passed as the second argument to themeProps() (the variant classes)
  • states — an ALTERNATIVE to visualProps on a target: state-based selector names (e.g. selected, disabled, today, in-range) rather than variant props. A target may carry visualProps OR states. A states-only target is a valid, intentional theme surface and must NOT be flagged as a visualProps drift; the audit tracks the two separately.

Audit steps:

  1. Extract all themeProps() calls from source (excluding test files)
  2. Extract all theming.targets from .doc.mjs files
  3. Flag any mismatches:
    • Component has themeProps() 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 themeProps()drift

Audit script:

python3 -c "
import re, glob, os

# Collect themeProps 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\"themeProps\('([^']+)'(?:,\s*\{([^}]*)\})?\)\", fh.read()):
 cn = m.group(1)
 if cn not in source_map: source_map[cn] = set()
 if m.group(2):
 # Capture only the prop NAME (object key), not the value. Handles
 # shorthand ({variant}), key:value ({size: resolvedSize}), and spreads.
 for part in m.group(2).split(','):
 part = part.strip()
 if not part: continue
 key = part.split(':')[0].strip().lstrip('.').strip()
 if re.match(r'^\w+$', key):
 source_map[cn].add(key)

# Collect theming targets from docs. Targets are objects that may span multiple
# lines and expose EITHER visualProps (variant props) OR states (state selectors
# like selected/disabled/today) — both are valid theme surfaces. A states-only
# target is intentional and must NOT be flagged as a visualProps drift.
doc_map = {} # className -> set of visualProps
doc_states = {} # className -> set of state names (tracked separately)
for f in sorted(glob.glob('packages/core/src/*/*.doc.mjs')):
 with open(f) as fh: content = fh.read()
 # Match each target OBJECT containing a className (non-greedy, DOTALL for multiline)
 for tm in re.finditer(r\"\{[^{}]*?className:\s*'astryx-([^']+)'[^{}]*?\}\", content, re.S):
 cn = tm.group(1); block = tm.group(0)
 doc_map.setdefault(cn, set())
 vp = re.search(r\"visualProps:\s*\[([^\]]*)\]\", block, re.S)
 if vp:
 doc_map[cn] |= set(re.findall(r\"'(\w+)'\", vp.group(1)))
 st = re.search(r\"states:\s*\[([^\]]*)\]\", block, re.S)
 if st:
 doc_states[cn] = doc_states.get(cn, set()) | set(re.findall(r\"'[\w-]+'\", st.group(1)))

# Compare (visualProps only; states-only targets are a separate, intentional surface)
for cn in sorted(set(source_map) & set(doc_map)):
 # target documents states but no visualProps -> state selector surface, not a drift
 if cn in doc_states and not doc_map[cn]:
 continue
 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 themeProps() but no theming doc')
for cn in sorted(set(doc_map) - set(source_map)):
 print(f'STALE astryx-{cn}: in docs but no themeProps() in source')
if all(source_map.get(cn, set()) == doc_map.get(cn, set()) for cn in source_map) \
 and not (set(source_map) ^ set(doc_map)):
 print('All theming docs are in sync with source!')
"

Fix: Add or update the theming section. Derive targets directly from themeProps() 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 @astryxdesign/cli/authoring.

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 @astryxdesign/cli/authoring). 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/assets/templates/blocks/components/<Component>/ with isShowcase: true.

Audit script:

python3 -c "
import glob, os, re

base = 'packages/cli/assets/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 @astryxdesign/core/* and compare against the componentsUsed array in the matching .doc.mjs.

KNOWN-NOISY CHECK — do NOT auto-PR from its raw output. The componentsUsed convention is not uniform in the source, so a naive comparison produces hundreds of false positives (400+ on recent main). Two independent causes:

  1. Module vs named import. The old script captured the module path segment (@astryxdesign/core/ChatChat), but componentsUsed usually lists the named import (ChatComposer, ChatMessageList). Comparing module names against named entries flags nearly everything.
  2. Layout-primitive aliasing is inconsistent in the data itself. HStack/VStack/Center/Spacer are imported from @astryxdesign/core/Stack (or /Layout), but componentsUsed records them sometimes as the alias ('VStack'), sometimes as the module ('Stack'/'Layout') — different blocks do it differently. There is no single normalization rule that reconciles them, because the source is not internally consistent.

As a result this check cannot be made reliably green by a regex tweak. Treat it as advisory only: use it to eyeball obviously-wrong entries (a listed component that is genuinely not used at all), never as a batch fix source. A real fix requires a maintainer decision on the canonical componentsUsed contract (named exports vs module names, and whether sub-components must be listed), followed by a one-time normalization. Until then, do NOT open block-metadata PRs off this check's output. The detection helper below compares against the UNION of named imports and module names, which removes cause (1) but not (2); its remaining hits are still dominated by the aliasing inconsistency.

Audit script (advisory — union of named imports + module names; still noisy, see above):

python3 -c "
import glob, re, os

blocks_dir = 'packages/cli/assets/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()

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

 # Collect BOTH the named imports and the module names, since componentsUsed
 # mixes the two. A documented entry is accepted if it matches either.
 named, mods = set(), set()
 for im in re.finditer(r\"import\s*\{([^}]+)\}\s*from\s*['\\\"]@astryxdesign/core/(\w+)['\\\"]\", tsx_content):
 mods.add(im.group(2))
 for n in im.group(1).split(','):
 n = n.strip()
 if n and not n.startswith('use') and n[:1].isupper():
 named.add(n)

 valid = named | mods
 stale = documented - valid # listed but neither a named import nor a used module
 missing = named - documented - mods # imported component absent under its own name AND module

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

if issues:
 print(f'componentsUsed advisory ({len(issues)} blocks — EXPECT layout-alias false positives, do not batch-fix):')
 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/assets/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: @astryxdesign/cli/authoring (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/assets/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/assets/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/assets/templates/blocks/**/*.doc.mjs', recursive=True)):
 with open(f) as fh: content = fh.read()
 # Match a bare number OR a ratio like `16 / 9` (with optional spaces). The old
 # pattern [\d./-]+ stopped at the space in `16 / 9` and matched just `16`, which
 # then tripped the >10 SUSPICIOUS branch on every widescreen block (271 false hits).
 m = re.search(r'aspectRatio:\s*(\d+(?:\.\d+)?\s*/\s*\d+(?:\.\d+)?|-?\d+(?:\.\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

Every .doc.mjs types itself from the package, not from a relative path: import('@astryxdesign/cli/authoring'). The old core/src/docs-types path no longer exists, and depth-specific ../../../.. chains are gone with it — a relative import is now the defect, whatever its depth.

Flag any doc file that types itself some other way:

python3 -c "
import glob

for f in sorted(glob.glob('packages/**/*.doc.mjs', recursive=True) +
 glob.glob('apps/**/*.doc.mjs', recursive=True)):
 with open(f) as fh: content = fh.read()
 if 'docs-types' in content or \"import('../\" in content:
 print(f'STALE doc type import: {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 astryx component <Name> --lang zh.
  • docsDense — token-compressed English. Served by astryx component <Name> --lang dense.

Both are typed as TranslationDoc (components) or HookTranslationDoc (hooks) from @astryxdesign/cli/authoring. The CLI loader (packages/cli/foundation/discovery/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 astryx 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/clients/cli/bin/astryx.mjs component Card --lang dense | head -20
node packages/cli/clients/cli/bin/astryx.mjs hook useFocusTrap --lang dense | sed -n '/Parameters/,/Returns/p' # expect single **(required)**

Reference docs are a separate overlay system with a different shape. Everything above covers component and hook docs (TranslationDoc / HookTranslationDoc, merged by component-loader.mjs). Reference docs are the topic docs served by astryx docs <topic> --lang zh|dense (theme, tokens, principles, layout, and friends), and they use their own contract:

  • Base files: packages/cli/assets/docs/<topic>.doc.mjs. Overlays live in sibling files <topic>.doc.dense.mjs / <topic>.doc.zh.mjs, exporting docsDense / docsZh typed as ReferenceTranslationDoc. The loader is packages/cli/api/docs/docs.mjs.

Reference overlays are anchored, prose-only, and fail-safe. Generate them in this shape only:

/** @type {import('@astryxdesign/cli/authoring').ReferenceTranslationDoc} */
export const docsDense = {
 description: 'compressed doc description',
 sections: [
 {
 section: 'Quick Start', // base section title, verbatim English (the anchor)
 title: 'Quick Start', // optional translated or compressed heading
 blocks: [
 { id: 'built-setup', text: 'compressed prose' }, // base prose block id + prose
 { id: 'core-ideas', items: ['compressed', 'list'] }, // base list block id + items
 ],
 },
 ],
};

Hard rules:

  • Anchor by name, never by position. Sections key on section (the base title). Blocks key on id (the base block's id). Do NOT use the old positional content: [null, ...] array. The gate rejects it.
  • Prose only. A block override may carry id plus text (for a base prose block) or items (for a base list block), and nothing else. Structure stays canonical: types, code, and tables are never in an overlay.
  • Every id must resolve to a real block id in the anchored base section. text requires a prose base block; items requires a list base block.
  • Whatever the overlay does not name falls back to canonical English, so cover the blocks worth compressing or translating and leave the rest.

Canonical block ids: packages/cli/assets/docs/tokens.doc.mjs is generated, so its ids come from scripts/generate-token-docs.mjs (rerun after edits, then --check in CI). The hand-written reference docs (theme, layout, principles) carry an id on each block; a block with no id cannot be overlaid.

Gate: packages/cli/api/docs/docOverlays.test.mjs enforces all of the above (id resolves, prose only, unique base ids, no legacy content). Run it after any reference-overlay change:

node_modules/.bin/vitest run packages/cli/api/docs/docOverlays.test.mjs

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.

Escape sequences count. A tell authored as a JS escape (\u2014, \u2013, \u2018/\u2019, \u201c/\u201d, \u2026) is the same tell as the raw character: it decodes to an em/en dash, curly quote, or ellipsis and reaches rendered output. Fix these too. When you do, prefer replacing the escape with the correct plain character directly (\u2014 for an aside becomes ; or , or : ; \u2026 becomes ...; \u2019 becomes a straight ', escaped as \' inside a single-quoted string). The same exemptions below apply (\u2014\u2014 is CJK, numeric-range en dashes are fine).

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 (raw Unicode chars):
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
# ALSO catch the SAME tells authored as JS escape sequences. A byte/char scan
# misses these, but they decode to em/en dash, curly quotes, and ellipsis at
# runtime and reach rendered CLI/docs output just the same. Scan for the escapes
# too (\u2014 em, \u2013 en, \u2018/\u2019 curly single, \u201c/\u201d curly
# double, \u2026 ellipsis); \u2014\u2014 is CJK double, exempt.
grep -rnP '\\u201[89cd]|\\u2013|\\u2014|\\u2026' 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.

18. CLI Config & Integration Doc Drift

The CLI README (packages/cli/README.md) documents two typed APIs in field tables: the consumer astryx.config surface (under ## Configuration) and the author astryx.integration manifest (under ## Integrations). Both are validated at runtime by Zod schemas that are the source of truth. After the CLI restructure the schemas are module-private (not exported) and live in two files: the config schema is const configSchema in packages/cli/authoring/config/parse.mjs, and the integration schema is const integrationSchema in packages/cli/authoring/integration/parse.mjs. When a field is added, removed, or renamed in a schema, the README table drifts and consumers get docs that no longer match what the CLI accepts.

For each schema, compare the top-level field names against the README's field table (the table whose header is Field | Type | Purpose):

  • Field in schema but not in the README table: a new or renamed field is undocumented. Add a row.
  • Field in the README table but not in schema: the field was removed or renamed and the doc is stale. Remove or fix the row.

Nested config fields are surfaced in the README with dotted names: hookshooks.postCodemod, experimentalexperimental.xle.components. Treat those dotted names as the schema keys when comparing the ## Configuration table. The manifest fields (components, templates, codemods, issuesUrl) map one-to-one.

This is a names-in-sync check, not a prose check. Flag added/removed/renamed fields. Do not rewrite descriptions to taste (that is a subjective style change and out of scope per "Does NOT Do"); only add a row for a genuinely new field, or remove one for a genuinely deleted field. When the config source of truth in packages/cli/authoring/config/type.ts (the AstryxConfig TypeScript interface) and the Zod schema disagree, trust the Zod schema. It is what the CLI enforces at load time. Note the interface drift in the report for human follow-up.

Audit script (detection only; exits 1 on drift, 0 in sync; run from the repo root):

python3 - <<'PY'
import re, sys, pathlib
ROOT = pathlib.Path('.')
# Schemas are module-private after the CLI restructure: `const configSchema` in
# authoring/config/parse.mjs and `const integrationSchema` in
# authoring/integration/parse.mjs (NOT the old exported AstryxConfigSchema /
# AstryxIntegrationSchema in src/lib/config-schema.mjs, which no longer exists).
CONFIG_SCHEMA = (ROOT/'packages/cli/authoring/config/parse.mjs').read_text()
INTEG_SCHEMA = (ROOT/'packages/cli/authoring/integration/parse.mjs').read_text()
README = (ROOT/'packages/cli/README.md').read_text()
def zod_block(src, name):
 # match a private `const <name> = z` (also tolerates an `export const` form)
 m = re.search(rf'(?:export\s+)?const {name}\s*=\s*z', src)
 if not m: return ''
 tail = src[m.start():]
 end = re.search(r'\n\s*\.strict\(\);', tail)
 return tail[:end.end()] if end else tail
def top_keys(block):
 body = re.search(r'\.object\(\{(.*)\}\)\s*\.strict\(\);', block, re.S)
 if not body: return set()
 return set(re.findall(r'^ ([A-Za-z_][\w-]*)\s*:', body.group(1), re.M))
cfg = top_keys(zod_block(CONFIG_SCHEMA, 'configSchema'))
integ = top_keys(zod_block(INTEG_SCHEMA, 'integrationSchema'))
if 'hooks' in cfg: cfg.discard('hooks'); cfg.add('hooks.postCodemod')
if 'experimental' in cfg: cfg.discard('experimental'); cfg.add('experimental.xle.components')
def field_tables(section_heading):
 idx = README.find(section_heading)
 if idx < 0: return set()
 nxt = README.find('\n## ', idx+1)
 section = README[idx: nxt if nxt>0 else len(README)]
 fields=set(); in_ft=False
 for line in section.splitlines():
 if line.strip().startswith('|'):
 header = re.sub(r'[`*]','',line).lower()
 if 'field' in header and 'purpose' in header: in_ft=True; continue
 if set(line.replace('|','').strip()) <= set('-: '): continue
 if in_ft:
 m = re.match(r'\|\s*`([^`]+)`', line)
 if m: fields.add(m.group(1).strip())
 else: in_ft=False
 return fields
def report(label, truth, doc):
 missing, extra = truth - doc, doc - truth
 ok = not missing and not extra
 print(f"[{'OK' if ok else 'DRIFT'}] {label}")
 if missing: print(f" MISSING from README: {sorted(missing)}")
 if extra: print(f" STALE in README (not in schema): {sorted(extra)}")
 return not ok
drift = report('astryx.config (## Configuration)', cfg, field_tables('## Configuration'))
drift |= report('astryx.integration (## Integrations)', integ, field_tables('## Integrations'))
sys.exit(1 if drift else 0)
PY

When it reports DRIFT, update the README field table to match the schema (add the missing row with a short type + purpose, or remove the stale one), then re-run until it prints OK for both. PR category: docs (branch navi/docs/config-readme-sync).

19. CLI Colocated Docs (FunctionDoc / CommandDoc / SchemaDoc / EnumDoc)

The CLI documents its own surface with typed .doc.mjs files colocated next to what they describe, the same pattern as component docs. They are the source that feeds --help and the capability manifest today (via the defineCommand converter), and the source astryx docs and the doc site will read from as those surfaces land. Their type structure is checked in CI by tsc --checkJs through pnpm -F @astryxdesign/cli typecheck:strict, which runs checkJs over api/, clients/, foundation/, and authoring/. So, as with component docs, this role does not re-check structure; it checks coverage and the content-level drift the type checker cannot see.

The doc kinds and where they live:

Surface Doc-type Location
An @astryxdesign/cli/api function (or a hook) FunctionDoc packages/cli/api/<name>/<fn>.doc.mjs
A CLI command CommandDoc (references its function via fn) packages/cli/clients/cli/commands/<name>.doc.mjs
An authored object (config, integration, codemod, the doc-types, the response envelope) SchemaDoc beside the schema (packages/cli/authoring/**, packages/cli/foundation/response/)
A closed vocabulary (error codes, response types) EnumDoc packages/cli/foundation/response/

Coverage. Every command in astryx manifest --json (including subcommands like theme build), every function exported from @astryxdesign/cli/api, and the config, integration, and codemod schemas should each have a colocated doc. A command or API function that ships without a .doc.mjs is a finding.

Drift and structure. Two committed checks enforce accuracy. Run both and flag any failure:

# Docs mirror the live CLI: every CommandDoc's fn/args/options resolve to a real
# function and match the manifest, and the error-codes / response-types EnumDocs
# equal ERROR_CODES / the manifest response-type set exactly.
node packages/cli/test/drift/docs-drift.mjs
# Structure: every doc-type ships type.ts + parse.mjs + <kind>.doc.mjs, re-exports
# its parser from the authoring barrel, and appears in parseDoc's @returns union;
# every api/<name>/ ships its typedefs, a FunctionDoc, and a test.
pnpm check:cli-structure

Because --help is built from each CommandDoc via defineCommand, editing a command doc changes user-visible help. After any edit, run astryx <command> --help and confirm the rendered output still reads correctly.

Documented examples must actually run. Every CommandDoc carries examples, and nothing executes them — the drift harness compares flags and args, not whether the invocation works. A stale example is the most user-visible kind of doc rot, because it is the line someone copies.

Run them all in a throwaway cwd, since several commands write files. That cwd must live inside the repo, not in /tmp: the CLI resolves @astryxdesign/core by walking up from the cwd, so a /tmp sandbox fails every example with Could not find @astryxdesign/core package and reports the whole set as broken.

node - <<'EOF'
const {spawnSync} = require('node:child_process');
const fs = require('node:fs'), path = require('node:path');
const BIN = 'packages/cli/clients/cli/bin/astryx.mjs';
// Inside the repo so `@astryxdesign/core` still resolves by walking up.
const box = fs.mkdtempSync(path.join(process.cwd(), '.nw-examples-'));
fs.writeFileSync(path.join(box, 'package.json'), '{"name":"nw","version":"0.0.0","private":true}');

const manifest = JSON.parse(spawnSync('node', [BIN, 'manifest', '--json'], {encoding: 'utf8'}).stdout).data;
const examples = [];
const walk = c => { (c.examples ?? []).forEach(e => examples.push(e)); (c.subcommands ?? []).forEach(walk); };
manifest.commands.forEach(walk);

for (const ex of examples) {
 const argv = (ex.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).slice(1).map(s => s.replace(/^['"]|['"]$/g, ''));
 if (argv.includes('--watch')) continue; // long-running
 const r = spawnSync('node', [path.resolve(BIN), ...argv], {cwd: box, encoding: 'utf8', input: '', timeout: 90000});
 const out = `${r.stdout ?? ''}${r.stderr ?? ''}`;
 if (/\n\s+at .+:\d+:\d+|TypeError|Cannot read propert/.test(out)) console.log(`CRASH ${ex}`);
 else if (r.status !== 0) console.log(`exit ${r.status} ${ex}`);
}
fs.rmSync(box, {recursive: true, force: true});
EOF

A crash is always a finding. A non-zero exit is only a finding if the example was meant to succeed — some legitimately demonstrate an error path. Fix by correcting the example in the doc (the command is the source of truth), not by changing the command to match a stale doc.

Detect fake subjects by running them, never by matching names. The failure useToggle represents is broader than examples: a doc names a subject that does not exist — a hook, component, topic, or template that was renamed or never existed — while the surrounding command is perfectly valid. It is tempting to catch these by extracting astryx <cmd> <subject> with a regex and checking the subject against --list. Do not. That approach was tried and was wrong 8 times out of 11:

  • Aliases still resolve. astryx component XDSButton is absent from component --list (components were unprefixed in the rebrand) yet exits 0 and prints the Button doc. Flagging it as fake is a false positive.
  • Prose reads like invocations. A sentence containing "...only contains astryx component strings." matches the same regex and is not an invocation at all.

Running the invocation settles both: the alias exits 0, the prose is never executed, and a genuinely fake subject exits non-zero with ERR_UNKNOWN_HOOK/ERR_UNKNOWN_COMPONENT. Extend the runner above to the invocations inside reference-doc code blocks (packages/cli/assets/docs/*.doc.mjs) and the README, since those are copy-paste targets too — but keep execution, not pattern matching, as the test.

A subject that resolves only through a legacy alias is worth a separate, softer finding: it works, so it is not broken, but it teaches the old name. Prefer the current one.

Never hand-write a .d.mts. The declarations under api/, authoring/, and foundation/ are generated from the .mjs JSDoc by packages/cli/scripts/sync-api-types.mjs (the emitted declarations are gitignored, regenerated at prepack, stamped @generated). If a type looks wrong, fix the JSDoc and run pnpm -F @astryxdesign/cli sync:api-types — never edit the emitted file, and never add one by hand to satisfy a missing-declaration error. The published surface is verified end to end by .github/scripts/cli-api-types-verify.mjs, which is the check to run when a type-level finding involves what consumers actually install.

Layering is linted, not reviewed. ESLint pins the import directions (authoring/ imports no other layer, foundation/ never imports api/ or clients/, api/ never imports clients/) and requires commands to register through defineCommand. A finding here is an ESLint error, not something to eyeball — if a fix seems to need an upward import, the code being imported probably belongs one layer down.

Fix. Author the missing .doc.mjs, copying the search docs as a template (packages/cli/api/search/search.doc.mjs for the function, packages/cli/clients/cli/commands/search.doc.mjs for the command), or update the doc until the drift harness passes. Do not edit a .doc.mjs to paper over a real behavior change in the command itself; that is a QA or feature concern, not a doc fix. PR category: cli-docs (branch navi/docs/cli-<scope>).


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 themeProps() 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.
CLI colocated docs navi/docs/cli-{scope} Add a missing FunctionDoc/CommandDoc/SchemaDoc/EnumDoc, or update one so the drift harness passes (check 19). Never edit a doc to mask a real behavior change in the command itself.

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 @astryxdesign/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/clients/cli/bin/astryx.mjs component {Name} --brief
    node packages/cli/clients/cli/bin/astryx.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 @astryxdesign/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})
- Config/README drift: {n} config fields, {n} manifest fields out of sync (added/removed/renamed)
### 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,
 "configDocDrift": 0
 },
 "runsToday": 0
}

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /