* feat(rules): add 10 motion, accessibility, and design lint rules
Derived from a cross-resource design reference (every principle from the
design knowledge base), deduped against the full existing rule inventory.
Motion: extend no-transition-all to the Tailwind `transition-all` class
(was inline-only) and add no-tailwind-layout-transition for arbitrary
`transition-[<layout-prop>]`.
Accessibility: no-autoplay-without-muted, no-uninformative-aria-label,
no-target-blank-without-rel, and no-low-contrast-inline-style (computes the
real WCAG 2.1 ratio from co-located inline color + backgroundColor).
Design/Tailwind hygiene: no-redundant-display-class, prefer-truncate-shorthand,
no-full-viewport-width, and no-svg-currentcolor-with-fill-class.
All register via codegen (388 rules); 77 co-located tests pass; typecheck,
lint, and format clean.
* fix(rules): correct two false positives found in rule-validate
- no-low-contrast-inline-style: when the inline style has no `fontSize`,
the text may be sized large via a class (`text-5xl`), so fall back to the
lenient 3:1 large-text threshold instead of 4.5:1 to avoid flagging large
text that only needs 3:1. Stricter 4.5:1 applies only when the size is a
visible normal size.
- no-redundant-display-class: `<li>` defaults to `display: list-item`, not
`block`, so `block` on an `<li>` is meaningful — removed `li` from the
block-default set.
Adds regression tests for both.
* fix(rules): resolve Cursor Bugbot findings
- no-low-contrast-inline-style: use a solid `background` shorthand color as
the background (was skipped entirely, missing low-contrast pairs); skip only
gradients/images/ambiguous (both backgroundColor + background). Also treat a
string `fontWeight: "700"` as bold for the large-text threshold.
- no-transition-all: match `transition-all` as a whole Tailwind token (segment
after variant prefixes) so compound classes like `transition-all-custom` are
not falsely flagged.
- no-svg-currentcolor-with-fill-class: exclude stroke-WIDTH utilities
(`stroke-2`, `stroke-[1.5]`) — they set thickness, not color — so they no
longer false-conflict with `stroke="currentColor"`.
Adds regression tests for each.
* feat(rules): add 3 Tailwind-canonicalization rules from ui.sh guidance
- no-deprecated-tailwind-class: flag Tailwind v4 renames (bg-gradient-* →
bg-linear-*, flex-shrink-* → shrink-*, flex-grow-* → grow-*,
overflow-ellipsis → text-ellipsis). Gated on a new `tailwind:4` capability.
- no-arbitrary-px-font-size: `text-[13px]` → rem so text scales with the
user's root font size (px stays fine for border-*/outline-*).
- prefer-dvh-over-vh: `h-screen`/`min-h-screen`/`h-[100vh]` → dvh (tailwind:3.4).
Adds `tailwind:4` capability to @react-doctor/core and extracts a shared
`get-class-name-tokens` util (variant-aware token splitting), reused by
no-transition-all and no-svg-currentcolor-with-fill-class.
* fix(rules): adversarial-review false positives in the new design rules
- no-tailwind-layout-transition: match transition-[…] property names exactly
(comma-split + set) so SVG `stroke-width` / `border-width` — which merely
contain "width" — are no longer flagged as HTML layout thrash.
- no-full-viewport-width: drop `max-w-*` / `maxWidth` (a defensive cap, not the
overflow footgun); keep `w-screen`/`w-[100vw]`/`width`/`minWidth`.
- prefer-dvh-over-vh: drop `max-h-*` / `maxHeight` (a valid height cap, e.g. a
scrollable modal); keep `h-screen`/`min-h-screen`/`height`/`minHeight`.
- no-deprecated-tailwind-class: only rename `bg-gradient-to-*` → `bg-linear-to-*`
(v4 radial/conic are `bg-radial`/`bg-conic`, not `bg-linear-radial`).
Adds regression tests for each false positive.
* fix(rules): resolve thermo-review false positives
- no-svg-currentcolor-with-fill-class: only treat UNPREFIXED `fill-*`/`stroke-*`
color classes as conflicts; `hover:fill-blue-600` / `dark:fill-white` (state-
gated, no static conflict with the base `fill="currentColor"`) are no longer
flagged — a very common icon pattern.
- no-low-contrast-inline-style: reject 4-arg `rgb(0,0,0,0.5)` / slash-form alpha
(was judged opaque), and bail when the style object contains a `{...spread}`
that could override the colors. Tag `test-noise` to match sibling no-tiny-text.
- no-full-viewport-width: drop the redundant `category: "Architecture"` override
(it's the design-bucket default).
- Extract `has-jsx-spread-attribute` util; use it in the two a11y rules.
Adds regression tests for the variant-prefixed svg classes, rgb-alpha, opaque
rgb, and the style-spread bail.
* refactor(rules): deslop the design/motion/a11y rule additions
Consolidate duplication surfaced by an adversarial deslop pass; no behavior
change (the same diagnostics fire for the same inputs):
- no-autoplay-without-muted: fold isStaticallyTrue/isStaticallyFalse into one
tri-state resolveStaticBoolean, mirroring the existing parseJsxValue idiom
- no-svg-currentcolor-with-fill-class: collapse the duplicated fill/stroke
branches into a single loop over the two paint attributes
- no-low-contrast-inline-style: hoist the repeated `properties` fallback
- no-uncontrolled-input: drop the local hasJsxSpreadAttribute copy in favor of
the branch's new shared utils/has-jsx-spread-attribute, completing the extraction
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(rules): no-deprecated-tailwind-class stops flagging overflow-clip
`overflow-clip` is a current Tailwind utility for `overflow: clip` (v3.1+,
still present in v4) — it was never renamed to `text-clip`, which sets the
unrelated `text-overflow: clip`. The stray mapping produced an incorrect
"renamed in v4" suggestion (Cursor Bugbot). Only `overflow-ellipsis ->
text-ellipsis` is a real rename; keep that. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(capabilities): require a confirmed Tailwind major for the tailwind:4 gate
`tailwind:4` gates no-deprecated-tailwind-class, which tells users a class was
renamed/removed in v4. The optimistic-on-null policy (assume latest for an
unparseable spec like `workspace:*`) is right for the suggestion rule behind
`tailwind:3.4`, but for a deprecation rule it surfaces confidently-wrong
"renamed in v4" warnings on a v3 project. Require a CONFIRMED parsed major >= 4
for tailwind:4 — favor a false negative over a false positive (Cursor Bugbot).
`tailwind:3.4` stays optimistic. Adds an unparseable-version regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(rules): resolve Bugbot/Devin review findings on design/a11y rules
- no-full-viewport-width: match variant-prefixed `md:w-screen` / `lg:min-w-screen`, mirroring the sibling `prefer-dvh-over-vh` regex (overflow happens at every breakpoint).
- no-target-blank-without-rel: resolve `rel={'noopener'}` expression-container literals instead of treating them as dynamic, so reverse-tabnabbing isn't silently missed; share one literal resolver with the `target` check.
- Extract the bare `16` root-font-size divisor into `ROOT_FONT_SIZE_PX` per the magic-number convention.
* fix(rules): no-uninformative-aria-label resolves braced string literals
Extracts no-target-blank-without-rel's inline getStringLiteralAttributeValue
into a shared util and uses it in no-uninformative-aria-label, so a static
braced literal like aria-label={'icon'} is judged instead of silently skipped
(getJsxPropStringValue only handles plain string literals). Adds regression
tests for the braced uninformative and braced descriptive cases.
Addresses Bugbot review on #850.
* test(rules): add liveness fixtures after rebase
* fix(rules): address automated review findings
* fix(rules): handle leading-dot stroke widths
* fix(rules): skip dynamic background overrides
* fix(rules): address target blank and transition reviews
* fix(rules): skip svg layout transitions
* fix(rules): preserve font line height suggestion
* fix(rules): match complete transition utilities
* fix(rules): share layout transition properties
* fix(rules): bound deprecated utility prefixes
* fix(rules): clear dynamically overridden colors
* fix(rules): tighten style and svg precision
* fix(rules): preserve competing display utilities
* fix(rules): preserve animation property scopes
* feat(rules): expand deterministic design checks
* feat(rules): add structural design diagnostics
* feat(rules): complete source-auditable design checks
* fix(rules): skip unresolved input types
* fix(rules): tighten layout and boundary detection
* fix(rules): close motion and padding gaps
* fix(rules): tighten surface and line-height proofs
* fix(rules): evaluate opaque hsl contrast
* fix(rules): honor effective static values
* fix(rules): honor final inline style declarations
* fix(rules): respect variants and share surface checks
* fix(rules): traverse fragments and preserve variant scope
* feat(rules): deepen UI quality diagnostics
* feat(rules): add deterministic animation diagnostics
* fix(rules): retain in-house accessibility checks
* feat(rules): expand deterministic UI diagnostics
* feat(rules): detect generated interface patterns
* fix(rules): respect dynamic style overrides
* feat(rules): expand deterministic design diagnostics
* fix(rules): close design precision gaps
* feat(rules): expand deterministic design and accessibility checks
* feat(rules): detect unsafe Motion API usage
* test(fuzz): remove duplicate module snippets
* feat(cli): add focused design command
* fix(design): include the complete rule bucket
* fix(core): align opt-in rule gates
* feat(rules): expand deterministic motion and interaction checks
* feat(rules): harden deterministic design diagnostics
* fix(rules): harden design expansion diagnostics
* fix(rules): apply large-corpus precision review
* fix(rules): suppress large-corpus accessibility noise
* fix(core): detect compiler in rstack configs
* fix(a11y): avoid responsive navigation false positives
* fix(rules): reduce design corpus false positives
* fix(rules): restore will-change build after rebase
* fix(rules): harden all-rules corpus precision
---------
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(effect-needs-cleanup): simplify loop cleanup review deslop
- doesCleanupFunctionReleaseUsage: hoist the duplicated ChainExpression unwrap into one binding and short-circuit async/generator checks before helper recursion
- resolveIteratorCollectionKey: collapse the split symbol/kind guards back into one
- isAssignmentFormForOfIteratorReference: consolidate the two near-duplicate walkAst reference-matching blocks into one assignsReferencedSymbol closure
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(effect-needs-cleanup): address review findings on fix/effect-cleanup-loop-pairs
- effect-needs-cleanup.ts:1692 — require an exhaustive removal loop in doesReleaseCallMatchUsage's for-of pairing so retained-handler analysis (fileContainsReleaseForUsage) no longer treats a break-truncated loop as a full release; pinned both directions plus the effect-cleanup path in effect-needs-cleanup.regressions.test.ts
- utils/resolve-event-listener-capture.ts — folded the rule-local resolveStaticEventListenerCapture fork into the shared resolveEventListenerCapture behind allowComputedString/allowIndeterminateEntries options; both consuming rules keep their exact prior semantics
- packages/fuzz/corpus — relocated the true-positive fixture effect-needs-cleanup--assignment-form-loop-mismatch.tsx out of the confirmed-false-positive regressions/ directory into new true-positives/, and documented (README, smoke test, fuzz skill) that firing expectations live in the rule's unit suite, not the fuzz harness
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(changeset): document effect-needs-cleanup loop cleanup fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fuzz: add the verdict-drop oracle — semantics-preserving source rewrites
(parenthesized/cast/non-null receivers, concise-arrow-to-block returns,
no-op prologues, optional-chain and computed-member spellings) applied to
firing programs; a rule that goes silent under one keys on incidental
token shape. Wired into fuzz-rule as an advisory finding kind, a strict
per-rule robustness suite over the audited liveness fixtures, a
registry-wide census script, and new regression-corpus entries.
Rules: add no-locale-format-in-render (locale/timezone-dependent
formatting during render in SSR-capable projects → hydration mismatch,
gated off client-only frameworks) and extend
prefer-use-sync-external-store to detect hand-rolled module-scope stores
(let + listener set + subscribe fn consumed via useState snapshot +
useEffect subscription).
Robustness sweep: shared is-no-op-statement / is-global-method-call /
executes-during-render utilities and cast-wrapper unwrapping across the
rule registry so detection survives the rewrites above; census is 0
drops across all fixture-covered rules.
Updated the version of the react-doctor skill from 1.1.0 to 1.1.1. The playbook description has been refined for clarity, emphasizing its role as the single source of truth in the triage process. No functional changes were made to the skill's operation.
The skill body now points the agent at https://www.react.doctor/prompts/react-doctor-agent.md
when the user types `/doctor` or asks for a full triage. The remote playbook is the
single source of truth (same scan -> filter -> triage -> fix -> validate loop as the
daily GitHub Actions bot, adapted for local use), so prompt edits propagate to every
installed agent on the next invocation without re-publishing the skill.
Bumps SKILL.md version 1.0.0 -> 1.1.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
README (594 → 394 lines, -34%):
- collapse 3 redundant YAML examples in `PR blocking` to one explanation
- shrink Husky/lint-staged walkthrough to one config block + one warning
- compress `Custom rules` from 60 lines of numbered tutorial to a single example
- merge `Agent and CI integration` bullet list (duplicated CLI ref + PR blocking + Node API)
- collapse 5 prose paragraphs after the config table into a 5-bullet list
- cut implementation-detail bullet list of `rn-no-raw-text` Platform.OS forms
- drop NickvanDyke "we ported these rules" trivia paragraph
- replace duplicated install block in `Resources & Contributing Back` with one line
- inline single-row `Optional companion plugins` table
CONTEXT.md + .specs/inspect-pipeline.md:
- drop `actions/review` / `@react-doctor/review-action` section (reverted in #443)
- replace `@react-doctor/project-info` / `@react-doctor/types` refs with
`core/src/project-info` / `core/src/types` (folded into core in #440)
- fix "13 leaf reasons today" miscount → 12
- update service count 9 → 11
Deletes:
- TODOS.md (442-line working backlog — belongs in issues, not the repo)
- .agents/skills/thermo-nuclear-code-quality-review/ (200-line skill)
- packages/react-doctor/CHANGELOG.md.bak (68KB tracked backup file)
Validated: `pnpm format`, `pnpm lint`, `pnpm typecheck`, `pnpm test` (1449 pass, 3 skip).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: detect namespace hook calls (React.useState, React.useEffect) in all rules
Fixes#117: isHookCall now handles MemberExpression callee types
(e.g. React.useEffect, React.useState) in addition to plain Identifier
callee types. This fixes silent misses in no-derived-state-effect,
no-fetch-in-effect, no-cascading-set-state, no-effect-event-handler,
rerender-functional-setstate, rendering-hydration-no-flicker, and
the countSetStateCalls helper.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: remove unsupported jsx-a11y/no-noninteractive-element-interactions rule
Fixes#113, #119: This rule does not exist in the bundled oxlint
binary, causing 'Failed to parse oxlint configuration file' errors
and score suppression on all scans.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --no flag to skip prompts and always run full scan
Fixes#106: Adds -n/--no flag that skips all interactive prompts
(like --yes) but specifically declines the diff-only scan prompt,
ensuring a full project scan runs. Useful for CI agents that cannot
interact with TTY prompts.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* test: add namespace hook detection tests (React.useEffect, React.useState, etc.)
Adds 13 tests verifying that all rules correctly detect namespace-style
hook calls (import * as React; React.useEffect, React.useState, etc.).
Covers: no-derived-state-effect, no-fetch-in-effect, no-cascading-set-state,
no-effect-event-handler, no-derived-useState, rerender-lazy-state-init,
rerender-functional-setstate, rerender-dependencies, rendering-hydration-no-flicker,
no-usememo-simple-expression, prefer-useReducer, plus a regression guard for
direct-import fixtures.
Relates to #117.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* test: add edge case tests for project discovery and catalog resolution
Adds tests for:
- peerDependencies-only with catalog reference resolution
- Catalog reference to nonexistent catalog name (fallback to default)
- Empty pnpm-workspace.yaml handling
- Malformed package.json in workspace subdirectories
- Nonexistent root directory for discoverReactSubprojects
- File entries mixed with directory entries in project root
Relates to #101, #87, #105, #71.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: handle malformed JSON in workspace package.json files gracefully
readPackageJson now catches SyntaxError from malformed JSON and returns
an empty object instead of crashing. This prevents workspace discovery
from aborting when a subdirectory has an invalid package.json.
Previously, a broken package.json in any workspace subdirectory would
crash the entire scan with an unhandled SyntaxError.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add customRulesOnly config to disable builtin react/jsx-a11y rules
Fixes#109: Teams with existing ESLint setups can now set
customRulesOnly: true in react-doctor.config.json to only run
react-doctor/* custom rules, skipping the bundled react/, jsx-a11y/,
react-perf, and react-compiler rules.
This avoids rule duplication for projects that already lint with
eslint-plugin-react, eslint-plugin-jsx-a11y, etc.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: inherit config from ancestor directories for monorepo support
Fixes#73: loadConfig now walks up the directory tree to find
react-doctor.config.json or package.json reactDoctor key from
ancestor directories. This means a single config at the monorepo
root applies to all workspace packages without needing to duplicate
the config in each package.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --annotations flag for GitHub Actions annotation output
Fixes#81: The --annotations flag outputs diagnostics in GitHub
Actions annotation format (::error / ::warning) so they appear
as inline annotations on PR diffs. Example:
::error file=src/App.tsx,line=42,title=react-doctor/no-fetch-in-effect::fetch() inside useEffect
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* refactor: remove Ami integration
Removes all Ami-related code:
- --ami flag, --fix flag, fix subcommand, install-ami subcommand
- Ami detection, installation, deeplinks, and open-URL helpers
- Fix prompt (maybePromptFix) with Ami banners
- Skill installation prompt (maybePromptSkillInstall)
- fetchEstimatedScore and EstimatedScoreResult
- AMI_WEBSITE_URL, AMI_INSTALL_URL, AMI_RELEASES_URL, OPEN_BASE_URL,
ESTIMATE_SCORE_API_URL, ERROR_ESTIMATED_FIX_RATE, WARNING_ESTIMATED_FIX_RATE
- skill-prompt.ts utility
CLI bundle reduced by ~39KB (324KB → 285KB).
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: remove .ami directory and all remaining Ami references
- Delete .ami/ directory (42 skill/config files)
- Delete packages/website/src/app/open/ (Ami redirect page)
- Remove Ami references from README.md, share page, install-skill
route, llms.txt, and install-skill.sh
- Zero Ami references remain in the codebase
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: move .ami/skills to .agents/skills
Relocates all skill files from the deleted .ami/ directory into
.agents/skills/ and removes .agents from .gitignore so they are
tracked in version control.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: recognize custom text components in rn-no-raw-text rule
Fixes#93: The rn-no-raw-text rule now recognizes a much wider set of
text-handling components:
1. Expanded built-in names: Typography, Paragraph, Span, H1-H6
2. Keyword matching instead of suffix-only: any component whose name
contains Text, Title, Label, Heading, Caption, Subtitle, Typography,
Paragraph, Description, or Body is treated as text-handling
(e.g. StyledText, BodyText, CustomLabel, MyTypography)
3. New textComponents config option: users can specify arbitrary
component names in react-doctor.config.json that should be treated
as text-handling:
{
"textComponents": ["MyCustomComp", "NativeWindText"]
}
Diagnostics inside these components are filtered out post-lint.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: upgrade dependencies
- oxlint: 1.47.0 → 1.59.0 (fixes SIGABRT crashes on large projects #84/#77)
- oxfmt: 0.32.0 → 0.44.0
- typescript: 5.9.3 → 6.0.2
- vitest: 4.0.18 → 4.1.4
- tsdown: 0.20.3 → 0.21.7
- knip: 5.83.1 → 6.3.1
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: add file count limit per oxlint batch to prevent OOM crashes
Mitigates #84/#77: Adds OXLINT_MAX_FILES_PER_BATCH (500) to cap the
number of files per oxlint invocation. Combined with the oxlint upgrade
from 1.47 to 1.59, this prevents SIGABRT/OOM crashes on large projects
with 100+ source files.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --staged flag for pre-commit hook scanning
Fixes#115: The --staged flag scans only files in the git index
(staged with git add). It materializes staged content to a temp
directory so oxlint reads the exact index version, not the working
tree version. This is critical for pre-commit hooks where the working
tree may differ from what's being committed.
Usage:
npx react-doctor . --staged --fail-on error
For lint-staged:
"*.{tsx,jsx}": "npx react-doctor . --staged --fail-on error"
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* refactor: code review fixes per AGENTS.md rules
- DRY: Extract shared createFileLinesCache in filter-diagnostics.ts
- DRY: Extract resolveFailOnLevel in cli.ts (was duplicated in
staged and normal paths)
- DRY: Reuse exported getCalleeName from helpers.ts instead of
duplicating in state-and-effects.ts and performance.ts
- Magic number: Move maxBuffer 10MB to GIT_SHOW_MAX_BUFFER_BYTES constant
- Variable naming: prevLine → previousLine, tagName → fullTagName,
leafName → leafTagName, configFiles → projectConfigFilenames
- Remove .pop()! non-null assertion in favor of .at(-1) with fallback
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: prevent shell injection in staged file content reading
Replace execSync with string interpolation with spawnSync using an
args array. File paths with shell metacharacters (quotes, semicolons,
etc.) in their names are now passed safely without shell interpretation.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: pass resolved config to staged scan to preserve ancestor inheritance
The staged scan materializes files to /tmp, where loadConfig's ancestor
traversal would never find a monorepo root's config. Now the CLI resolves
config from the real project directory first and passes it via the new
configOverride ScanOption, so customRulesOnly, share, ignore rules,
and textComponents all apply correctly during --staged scans.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>