-
Notifications
You must be signed in to change notification settings - Fork 27
Night Watch Doc Reviewer
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.
Astryx docs serve three audiences simultaneously:
-
Humans browsing Storybook or reading
.doc.mjsfiles for API reference -
LLMs consuming CLI output (
xds component --brief,--compact,--lang zh,--lang dense, andxds hook) for code generation -
Storybook autodocs parsing JSDoc
@exampleblocks 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.
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.
Templates are split into two categories:
-
Page templates (
templates/pages/) — Full-page scaffolding (dashboard, login, settings, etc.). Each has atemplate.doc.mjswithtype: 'page'. -
Block templates (
templates/blocks/) — Smaller UI patterns and component examples. Each has a.doc.mjswithtype: 'block',aspectRatio, andcomponentsUsed.
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.
This role runs once per night. Check state to see if it has already run today — if so, skip.
Run all checks against origin/main. Collect findings into categories.
Scan every Astryx*.tsx file (excluding *Context*, *test*, *story*) for JSDoc @example blocks.
Rules:
-
No
```tsxlanguage tag. Storybook's autodocs parser chokes on thetsxtag — 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
@exampleblocks 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@examplecode blocks can confuse Storybook's markdown parser. If context is needed, put it in the JSDoc description above the@exampletag, 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
@exampleshould 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)
"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:
- Props in the TypeScript interface (
Astryx*Props) - Props in the
.doc.mjspropsarray
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.
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.tsxfile -
Block templates have matching
.doc.mjsfiles — every.tsxintemplates/blocks/should have a matching.doc.mjs, and vice versa. Flag orphaned files. -
Block
.doc.mjsfiles havetype: 'block'withaspectRatio(number > 0) andcomponentsUsed(string array) -
Page
.doc.mjsfiles havetype: 'page'— everytemplate.doc.mjsintemplates/pages/must havetype: '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.')
"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 onlystylex.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:
- For each component with
xstylein its props interface, check the JSDoc comment. If it's a generic description like "StyleX styles to apply to the container" — flag it for thestylex.create()guidance. - For
.doc.mjsfiles, check if common props are present when the component accepts them. Add missing entries. - Don't add common prop entries for components that don't accept them (e.g., don't add
xstyleto a component that doesn't have it in its interface).
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 fromxdsThemeProps('button', ...)→astryx-button -
visualProps— the prop names passed as the second argument toxdsThemeProps()(the variant classes)
Audit steps:
- Extract all
xdsThemeProps()calls from source (excluding test files) - Extract all
theming.targetsfrom.doc.mjsfiles - Flag any mismatches:
- Component has
xdsThemeProps()but nothemingsection in docs → missing -
theming.targetslists a className that doesn't exist in source → stale -
visualPropsin docs don't match the props passed toxdsThemeProps()→ drift
- Component has
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.
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:
- 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
- For each
--astryx-*property found, check if the component's.doc.mjshas a matchingcssPropertiesentry - Flag any mismatches:
- Component uses
--astryx-*property but nocssPropertiesin docs → missing -
cssPropertieslists a property not found in source → stale - Description or default is inaccurate → drift
- Component uses
Updated (block-template model): The old
showcasefield on component.doc.mjswas removed — it no longer exists inComponentDoc. The canonical preview is now a block template flaggedisShowcase: truein its.doc.mjs(BlockTemplateDoc.isShowcase, seedocs-types.ts). Do NOT check for ashowcasefield 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).
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
Buttonfrom@xds/core/Buttonbut doesn't list'Button'incomponentsUsed→ missing -
componentsUsedlists'Button'but the block doesn't import from@xds/core/Button→ stale
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.')
"Run the block template typecheck and flag any new errors:
npx tsc --project packages/cli/tsconfig.template-docs.json --noEmit 2>&1 | head -50Also 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"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
childrento render visibly (Card, Dialog, Section) - Component has required props that benefit from better defaults (
label: 'Click me'vslabel: '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
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}')
"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
"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')
"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}')
"Astryx ships translated/compressed doc overlays alongside the English docs export in the same .doc.mjs file:
-
docsZh— Chinese Simplified translation. Served byxds component <Name> --lang zh. -
docsDense— token-compressed English. Served byxds 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 viaxds component <ThemeName>). Hooks live atpackages/core/src/hooks/*.doc.mjsand co-locatedpackages/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
docsexport MUST have adocsDenseexport. (docsZhcoverage is tracked but lower priority — backfill where present and drifted, flag where wholly missing.) -
Count parity 1:1: overlay
usage.bestPracticesmust match English length, order, ANDguidance: true/falseflag per position. Same forfeatures,notes,accessibilityif 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 everydocs.components[].name(including hook sub-entries likeuseImperativeDialog). -
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 canonicalTranslationDocshape (d→description,p→propDescriptions, dropn/kw/ex). -
No duplicate
(required)markers: the renderer auto-appends**(required)**from the Englishrequired: truefield. 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");bestPracticesare capital-leading imperative sentences. -
Signal words + identifiers preserved: keep
if/when/unless/only/must/never/always, fullAstryx*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)**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.mjsparams[]andreturns[]should match the hook's TypeScript signature (param/return drift — the hook analog of prop drift). - Hook docs use
HookDoc(withparams/returns), notComponentDoc— do NOT flag missingprops/showcase/themingon hooks (those fields don't apply). - Every hook should have a
docsDense(see check 13).
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,showcaseblocks/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_modulesAfter 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.
Group findings and create PRs. Each PR should be focused on one category of fix.
| 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. |
-
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}
-
Make fixes. For each fix:
- Read the source file to understand the actual props/behavior
- Update the
.doc.mjs(or JSDoc@examplefor Storybook fixes) - Keep changes minimal — fix what's wrong, don't rewrite what's fine
-
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
-
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*"
-
If there are no findings in a category, skip that PR. Don't create empty PRs.
-
.doc.mjsprops: Match the exact prop name and type from the TypeScript definition. Userequired: truefor required props. Omitdefaultfor required props or props with no default. -
Block templates: When creating new block templates, include both a
.tsxfile with realistic JSX and a.doc.mjswithtype: 'block',aspectRatio, andcomponentsUsedmatching the actual imports. -
Don't rewrite good docs. If a
.doc.mjsis 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, anddata-*attributes should always be documented. -
Don't re-add
examplesarrays. If a component needs usage examples, create block templates instead.
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 checksSend 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?")
- ❌ 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.mjstype structure (CI handles this viatsc --checkJs)
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
}