Skip to content

feat: per-module scoring via diagnose({ projects }), CLI --project paths, and config projects#771

Merged
aidenybai merged 18 commits into
mainfrom
devin/1781083136-diagnose-projects-public
Jun 12, 2026
Merged

feat: per-module scoring via diagnose({ projects }), CLI --project paths, and config projects#771
aidenybai merged 18 commits into
mainfrom
devin/1781083136-diagnose-projects-public

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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() overload

diagnose(directory, options?)                  // unchanged
diagnose({ projects, config?, concurrency? })  // per-project scores + worst-of aggregate
  • Runs projects through a true worker pool (concurrency, default 4) with order-preserving results.
  • Config layering is additive via mergeReactDoctorConfigs: rules/categories merge per key, ignore lists union — so a per-project config overrides individual rules without discarding the base. Layers: on-disk config ← batch config ← per-project config.
  • Relative config.plugins keep their resolution base when overrides are layered (configSourceDirectory is threaded through).

CLI — --project accepts directory paths

react-doctor . --project modules/billing,modules/payroll

Reuses the existing --project flag (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 (same mergeReactDoctorConfigs semantics 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 projects array — no schema change.

Config — persistent projects field

// doctor.config.json at the repo root
{ "projects": ["modules/billing", "modules/payroll"] }

The 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 same resolveRequestedProjects path as the flag (workspace names → directory paths, "*" sentinel, same fail-fast errors — labeled Config "projects" entry "X" instead of Project "X"). Precedence: --project flag > config projects > workspace discovery/prompt. Only the config at the invocation root is consulted; projects inside 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, diff deprecation) since the file was stale on main.

Product notes

  • Reuse over adding: extended --project, selectProjects, printMultiProjectSummary, mergeReactDoctorConfigs, JSON projects array; removed the would-be second entrypoint diagnoseProjects (never published); config projects shares the flag's resolution code path.
  • Telemetry: project.path_selected counter for path-resolved --project entries; project.config_selected counter when the config field drives selection. Known blind spot: CLI telemetry is a no-op for the @react-doctor/api library, so the diagnose({ projects }) overload itself has no adoption signal — a kill decision on the API surface would have to ride issues/support load, not these counters.
  • Compat: default behavior unchanged (no flag, no config field → same flow); no changeset per maintainer request.
  • Kill metric: if project.path_selected + project.config_selected stay 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 (diagnoseProjects removed), 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 diagnoseProjects with a diagnose() overloaddiagnose({ 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-project config layers additively via new mergeReactDoctorConfigs (rules/categories per key, ignore lists union) instead of replacing on-disk config. configSourceDirectory is threaded when merging so relative plugins resolve correctly.

CLI: --project now resolves directory paths as well as workspace names (and is no longer ignored when discovery finds 0–1 packages). Root doctor.config.* projects mirrors --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 set concurrentScan so Sentry/spinner global state does not race.

Config / docs: New projects on ReactDoctorConfig; JSON schema refresh includes scope/base/supplyChain drift. Telemetry counters project.path_selected and project.config_selected.

Reviewed by Cursor Bugbot for commit dc04cf2. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@771
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@771
npm i https://pkg.pr.new/react-doctor@771

commit: dc04cf2

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit dc04cf2.

Comment thread packages/api/src/diagnose.ts Outdated
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
@devin-ai-integration

devin-ai-integration Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

End-to-end test results (built dist — what npm consumers get)

Shell-only testing (Node API + CLI), so no recording. Latest (a1ec050): concurrency + --score fixes 3/3 passed, CI 18/18 green. Earlier waves below.

Follow-up fixes — CLI multi-project concurrency + --score stderr (3/3 passed, commit a1ec050)
  • Multi-project scans run concurrently — passed. Summary reports wall-clock Scanned 37 files in 3.2s while per-project elapsed sums to 5.6s (module-a 3.0s + module-b 2.6s) — a sequential loop would make wall ≈ sum. Diagnostic counts unchanged vs baseline (module-a 243, module-b 51), aggregate render clean (one Scanning N projects… (k/N) spinner, no interleaved output).
  • --score stdout stays machine-parseable — passed. stdout = 21 exactly, stderr empty. The score-unavailable branch now writes to stderr (Console.error, packages/react-doctor/src/inspect.ts:856) — verified by code inspection only, since the score API is reachable locally and the null branch can't be triggered deterministically.
  • Regression: single-project scan unchanged — passed. react-doctor . --json in module-b → 51 diagnostics, score 21, normal single-project flow with spinners.

Verdicts on the 5 reported findings: (1) sequential CLI loop — real, fixed above; (2) 81-way default concurrency — stale, already bounded to DEFAULT_PROJECT_SCAN_CONCURRENCY = 4 in a9eadd2; (3) raw score-API label can disagree with local 75/50 thresholds — real, wire format left unchanged, dashboards should use the numeric score; (4) CLI --project fail-fast on bad paths — by design, API returns ok: false per project; (5) --score non-numeric stdout — real, fixed above.

API: 6/6 passed (commit 3c684d2). CLI flag: 5/5 passed (commit f6e6e33). Config projects: 5/5 passed (commit b45727d). CI: 18/18 green.

Config — projects field (5/5 passed, commit b45727d)

Root doctor.config.json: "projects": ["modules/module-a", "modules/module-b"], no flag passed.

  • Config field drives the multi-project scan — passed. react-doctor . -y --jsonprojects array = exactly the 2 module directories (no flag).
  • Per-module config still layers additively — passed. module-a button-has-type 20→0 (module config), both modules control-has-associated-label →0 (root config).
  • --project flag overrides the config field — passed. --project modules/module-b with both listed in config → only module-b scanned.
  • Invalid entry fails fast — passed. "projects": ["modules/missing"] → exit 1, Config "projects" entry "modules/missing" is not a directory under <root>.
  • Regression: no flag + no config field — passed. Single root scan, unchanged flow.

Unit suite: 15/15 (4 new config-selection tests). Merged origin/main (conflict only in generated schema/config.json, resolved by regenerating).

CLI — --project directory paths (5/5 passed)

Sandbox: root package.json with no workspaces (the exact case where the flag was previously silently ignored); modules/module-a (basic-react fixture), modules/module-b (nextjs-app fixture).

  • Per-module scores + worst-of aggregate render — passed. react-doctor . --project modules/module-a,modules/module-btest-basic-react 0, test-nextjs-app 21, aggregate 0 / 100 (scores match the API tests exactly).
  • Per-module config layers additively onto root config — passed. Root config disables control-has-associated-label, module-a's own doctor.config.json disables button-has-type: module-a drops BOTH (20→0, 22→0), config-less module-b drops the root rule (2→0). Replace semantics would have lost a layer.
  • Invalid entry errors clearly — passed. --project modules/does-not-exist → exit 1, Project "modules/does-not-exist" is not a directory under <root>.
  • JSON report reuses existing projects array — passed. 2 entries with directory/project/diagnostics; no schema change.
  • Regression: plain react-doctor . unchanged — passed. Single root scan, no multi-project summary.

Note: rule keys in doctor.config.json need the react-doctor/ prefix; unprefixed keys are silently ignored (pre-existing behavior, also in single-project scans).

API — diagnose({ projects }) (6/6 passed)
  • Batch scanning via diagnose() only — passed. diagnoseProjects is undefined; single-dir overload regression OK (score 0, 255 diagnostics).
  • Per-module scores + worst-of aggregate — passed. basic-react=0, nextjs-app=21, aggregate 0 (min).
  • Per-project config MERGES onto base (not replace) — passed. Override (button-has-type off) applied AND on-disk base (no-multi-comp off) preserved; control module unaffected (20 / 10 diagnostics).
  • Batch-level config applies across every project — passed. control-has-associated-label 22→0 in both; rest of rule set intact.
  • Relative config.plugins resolution with layered overrides — passed. Custom plugin fired 22x with rootDir redirect + override; pre-fix dist fired 0x (counterfactual).
  • Worker pool preserves input order — passed. 4 projects at concurrency 2, output order = input order.
Suites / CI
  • select-projects.test.ts: 15/15; api/tests/diagnose.test.ts: 16/16.
  • Full packages/react-doctor/tests locally: 1766 passed / 1 failed — the failure (install-agent-hooks › exits quietly when no react-doctor runner is available) is a local-env issue (npx sandbox on Node 20.18.1 missing the oxc-parser native binding), and the same suite is green in CI on all platforms.
  • CI on b45727d: 18 passed, 0 failed.

Devin session: https://app.devin.ai/sessions/f053cd1122b84b7e833135e8e469575f

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +162 to +166
const didOverrideConfig = batchConfig !== undefined || projectConfig !== undefined;
const effectiveConfig = mergeReactDoctorConfigs(
mergeReactDoctorConfigs(scanTarget.userConfig, batchConfig),
projectConfig,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread packages/react-doctor/src/index.ts Outdated
});

export { diagnose } from "@react-doctor/api";
export { diagnose, diagnoseProjects } from "@react-doctor/api";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

devin-ai-integration Bot and others added 3 commits June 10, 2026 19:50
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>
@devin-ai-integration devin-ai-integration Bot changed the title feat(api): ship diagnoseProjects publicly with additive config merging feat(api): multi-project scoring via diagnose({ projects }) with additive config merging Jun 11, 2026
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat(api): multi-project scoring via diagnose({ projects }) with additive config merging feat: per-module scoring via diagnose({ projects }) and CLI --project paths Jun 11, 2026
Comment thread packages/react-doctor/src/cli/commands/inspect.ts Outdated
devin-ai-integration Bot and others added 2 commits June 11, 2026 05:31
… field

Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat: per-module scoring via diagnose({ projects }) and CLI --project paths feat: per-module scoring via diagnose({ projects }), CLI --project paths, and config projects Jun 11, 2026
Comment thread packages/react-doctor/src/cli/commands/inspect.ts
rayhanadev and others added 3 commits June 10, 2026 22:45
…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>
Comment thread packages/react-doctor/src/cli/commands/inspect.ts Outdated
rayhanadev and others added 3 commits June 10, 2026 23:33
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>
Comment thread packages/react-doctor/src/cli/commands/inspect.ts Outdated
…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>
Comment thread packages/react-doctor/src/inspect.ts
… of per-scan

Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/react-doctor/src/cli/utils/render-multi-project-summary.ts
@aidenybai aidenybai merged commit d48e7f1 into main Jun 12, 2026
20 checks passed
@aidenybai aidenybai deleted the devin/1781083136-diagnose-projects-public branch June 12, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants