feat: per-module scoring via diagnose({ projects }), CLI --project paths, and config projects#771
Conversation
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
commit: |
|
No React Doctor issues found. 🎉 Reviewed by React Doctor for commit |
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
End-to-end test results (built dist — what npm consumers get)Shell-only testing (Node API + CLI), so no recording. Latest (a1ec050): concurrency + Follow-up fixes — CLI multi-project concurrency +
|
| const didOverrideConfig = batchConfig !== undefined || projectConfig !== undefined; | ||
| const effectiveConfig = mergeReactDoctorConfigs( | ||
| mergeReactDoctorConfigs(scanTarget.userConfig, batchConfig), | ||
| projectConfig, | ||
| ); |
There was a problem hiding this comment.
🚩 Semantic change: per-project config now merges instead of replacing
Before this PR, a ProjectDefinition.config fully replaced the on-disk doctor.config.* for that project's scan. Now it merges additively via mergeReactDoctorConfigs (packages/api/src/diagnose.ts:163-166). This is an intentional behavioral change documented in the changeset and types, but worth noting as a potential breaking change for any existing callers that relied on the replacement semantics — e.g., a caller passing config: { rules: { "react-doctor/no-prop-drilling": "off" } } previously got ONLY that rule config, but now gets the on-disk rules merged with that override.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Correct, and intentional: replace semantics was the bug from the user's perspective — Akshay's use case (same base rules across the repo + small per-module overrides) is broken by full replacement, since a one-rule override silently dropped the entire base config. The change ships before diagnoseProjects is ever publicly exported (it was only reachable via the private, unpublished @react-doctor/api), so there are no external callers relying on the old semantics; the changeset and JSDoc document the new layering. Verified end-to-end in this comment (test 3).
| }); | ||
|
|
||
| export { diagnose } from "@react-doctor/api"; | ||
| export { diagnose, diagnoseProjects } from "@react-doctor/api"; |
There was a problem hiding this comment.
🚩 New public API surface addition — product-thinking pass requirement
AGENTS.md requires running the product-thinking pass before adding to the public surface. This PR adds diagnoseProjects, DiagnoseProjectsInput, DiagnoseProjectsResult, ProjectDefinition, ProjectResult, ProjectResultOk, and ProjectResultError to the published react-doctor package at packages/react-doctor/src/index.ts:124. The changeset exists and docs are present, but there's no evidence in the PR that the product-thinking checklist (name the user's job, reuse before adding, wire one telemetry metric, add compatibility artifacts, set a kill metric) was completed.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
The product-thinking pass was completed — see the "Product brief" section in the PR description: user's job (Rippling's per-module health dashboard, currently built with custom worker plumbing), reuse (extended the existing diagnoseProjects instead of adding a new diagnoseModules), telemetry (api package telemetry is a no-op for npm consumers, so adoption is tracked via npm usage/issues), compatibility (minor changeset + JSDoc), and kill criteria (fold back into private api if unadopted).
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
…oncurrency in diagnoseProjects Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
… field Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
projects
…config merge - diagnoseProjectBatch reuses core's existing mapWithConcurrency instead of a hand-rolled worker pool; buildDiagnoseLayer takes the scan target directly - mergeReactDoctorConfigs: spread resolves single-sided keys, only both-defined keys are re-merged additively (drops two generic helpers) - single source of truth for merge semantics docs; trimmed restated comments - resolveProjectFlag flattened to a map; consolidated redundant tests Co-authored-by: Cursor <cursoragent@cursor.com>
Multi-project CLI scans now run resolveScanTarget per selected folder, so a module's own rootDir redirect, nested React discovery, and config source directory (for relative plugin resolution) match the batch API. The diff-mode supply-chain inclusion decision now reads the per-project merged config instead of the root config. Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Default was fully parallel (projects.length): each project scan already fans out its own oxlint workers, so an 80-module batch would spawn hundreds of subprocesses. Default to DEFAULT_PROJECT_SCAN_CONCURRENCY (4, in core constants) as the PR intent stated; callers opt into more via `concurrency`. Co-authored-by: Cursor <cursoragent@cursor.com>
… them `plugins` is override-wins in mergeReactDoctorConfigs, but the resolution base was always the module config's directory (CLI) or the on-disk config's directory (API) — root-inherited relative plugins loaded from the wrong place. Base now follows the supplying layer: the module config when it declares `plugins`, the root config otherwise; for caller-supplied API overrides, the scan root. Co-authored-by: Cursor <cursoragent@cursor.com>
… route no-score message to stderr Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
…y run state The module-level run state (scanned project, active run trace) has single-scan semantics; pool members overlapped each other's resets and writes, mislabeling events and crash links. Concurrent scans now emit span-scoped telemetry only: wide events keep full per-scan attribution (they ride the root span), metrics omit the project shape during a batch, and no batch member records the active run trace. Co-authored-by: Cursor <cursoragent@cursor.com>
… of per-scan Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8af01e6. Configure here.
…nfig Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
…nose-projects-public

Summary
Monorepo owners (Rippling-style: 82 modules, daily health dashboard) need per-module scores without custom worker plumbing or per-module configs that clobber the shared base. This PR makes that native, on the API, the CLI flag, and the config file.
API —
diagnose()overloadconcurrency, default 4) with order-preserving results.mergeReactDoctorConfigs:rules/categoriesmerge per key,ignorelists union — so a per-projectconfigoverrides individual rules without discarding the base. Layers: on-disk config ← batchconfig← per-projectconfig.config.pluginskeep their resolution base when overrides are layered (configSourceDirectoryis threaded through).CLI —
--projectaccepts directory pathsreact-doctor . --project modules/billing,modules/payrollReuses the existing
--projectflag (no new flag): entries resolve as workspace package names first, then as directory paths against the scan root. Previously the flag was silently ignored when workspace discovery found 0 or 1 packages — exactly the gap for monorepos whose modules aren't workspace packages.--project "*"with no packages falls back to the root (GitHub Action default unaffected).Each project's own
doctor.config.*now layers additively onto the root config in multi-project scans (samemergeReactDoctorConfigssemantics as the API), so per-module rule overrides apply on the CLI too.Output reuses the existing multi-project rendering (per-project score lines + worst-of aggregate) and the JSON report's existing
projectsarray — no schema change.Config — persistent
projectsfieldThe config-file equivalent of
--project, for repos that always want per-module scoring without passing the flag on every run. Entries resolve through the exact sameresolveRequestedProjectspath as the flag (workspace names → directory paths,"*"sentinel, same fail-fast errors — labeledConfig "projects" entry "X"instead ofProject "X"). Precedence:--projectflag > configprojects> workspace discovery/prompt. Only the config at the invocation root is consulted;projectsinside a module's own config is ignored. The JSON schema (schema/config.json) is regenerated — the diff also picks up previously-undocumented drift (supplyChain,scope,base,diffdeprecation) since the file was stale on main.Product notes
--project,selectProjects,printMultiProjectSummary,mergeReactDoctorConfigs, JSONprojectsarray; removed the would-be second entrypointdiagnoseProjects(never published); configprojectsshares the flag's resolution code path.project.path_selectedcounter for path-resolved--projectentries;project.config_selectedcounter when the config field drives selection. Known blind spot: CLI telemetry is a no-op for the@react-doctor/apilibrary, so thediagnose({ projects })overload itself has no adoption signal — a kill decision on the API surface would have to ride issues/support load, not these counters.project.path_selected+project.config_selectedstay near zero a quarter after release, revert the path-resolution branch and config field.Link to Devin session: https://app.devin.ai/sessions/f053cd1122b84b7e833135e8e469575f
Requested by: @aidenybai
Note
Medium Risk
Touches core scan orchestration, published API shape (
diagnoseProjectsremoved), and concurrent inspect/Sentry behavior; default single-project flows stay the same but multi-project and config layering paths are new surface area.Overview
Adds native per-module scoring across the API, CLI, and config, aimed at monorepos that need separate scores without duplicating base config.
API: Replaces standalone
diagnoseProjectswith adiagnose()overload —diagnose({ projects, config?, concurrency? })runs a bounded worker pool (default 4 concurrent projects) and returns per-project results plus a worst-of aggregate score. Batch and per-projectconfiglayers additively via newmergeReactDoctorConfigs(rules/categories per key, ignore lists union) instead of replacing on-disk config.configSourceDirectoryis threaded when merging so relativepluginsresolve correctly.CLI:
--projectnow resolves directory paths as well as workspace names (and is no longer ignored when discovery finds 0–1 packages). Rootdoctor.config.*projectsmirrors--project(flag wins). Multi-project scans use the same pool as the API, merge each module’s config onto the root, filter CI/summary diagnostics per merged config (filterScansForSurface), and setconcurrentScanso Sentry/spinner global state does not race.Config / docs: New
projectsonReactDoctorConfig; JSON schema refresh includesscope/base/supplyChaindrift. Telemetry countersproject.path_selectedandproject.config_selected.Reviewed by Cursor Bugbot for commit dc04cf2. Bugbot is set up for automated code reviews on this repo. Configure here.