Commit Graph

33 Commits

Author SHA1 Message Date
Aiden Bai 8c2f03aea9 feat: make React cleanup first-class (#1624)
* feat: make React cleanup first-class

* refactor: remove editor integrations

* fix: harden React cleanup analysis

* fix: detect default export duplication roots

* fix: unwrap typed duplication roots

* feat: add opt-in project analysis rules

* fix: canonicalize project analysis paths

* fix: harden project analysis precision

* fix: recognize cross-platform project entries

* fix: eliminate project analysis false positives

* fix: harden project analysis reachability

* fix: canonicalize project analysis inputs

* fix: resolve project analysis review findings

* fix: eliminate residual project analysis false positives

* fix: ignore commented registry previews

* fix: eliminate project analysis false positives

* fix: normalize project analysis paths across platforms

* fix: normalize Nextra theme path identity

* test: canonicalize convention fixture paths

* fix: preserve project analysis provenance

* fix: harden project analysis precision

* fix: honor project analysis boundaries

* fix: recognize conditional config plugins

* fix: recognize executable project references

* fix: recognize Stencil tool contracts

* fix: recognize nested tool references

* fix: recognize project setup contracts

* fix: recognize generated and local package consumers

* fix: recognize static template package references

* fix: recognize nested package runtime contracts

* fix: close project analysis parser gaps

* fix: parse project conventions structurally

* refactor: replace structural scanners with parsers

* fix: recognize functional Next CSS config

* fix: close remaining project analysis gaps

* fix: apply tag filters to project analysis

* fix: preserve embedded source positions

* fix: validate static config helper bindings

* fix: bound runtime directory discovery

* fix: close final dependency analysis gaps

* fix: preserve declaration dependency references

* chore: refresh generated rule metadata

* fix: make project analysis portable and bounded

* test: stabilize cleanup scaling guard

* refactor: parse project syntax with oxc

* fix: normalize native filesystem paths

* fix: separate path identity from report paths

* fix: match project files by filesystem identity

* fix: match build glob files by package identity

* fix: use native path keys for file identity

* fix: canonicalize Windows file identities

* fix: canonicalize package ownership paths

* test: inspect Windows path identities

* test: trace Windows package ownership

* fix: keep Windows path identities consistent

* fix: classify test contracts by normalized path

* fix: scope test contracts by canonical package path

* fix: keep test package graphs conservative

* test: keep React complexity advisory
2026-08-13 16:26:08 -07:00
Aiden Bai c50e3bdbe3 fix(core): keep react detection React-specific 2026-08-13 05:02:35 +00:00
Aiden Bai acdcb236b9 fix(core): report supported library projects 2026-08-13 04:36:17 +00:00
Aiden Bai 51e198db8b perf: reuse source inventories across project scans (#1617)
* perf: reuse source inventories across project scans

* chore: upgrade Oxc toolchain

* fix: fall back for empty shared inventories
2026-08-09 01:42:22 -07:00
Ray Arayilakath c843b01a9e feat(core): add Axiom OTLP transport and an anonymizing tracer (#1608)
* feat(core): add Axiom OTLP transport and an anonymizing tracer

Adds the export path for shipping traces and metrics to Axiom, plus the
anonymization guarantee that has to come with it. Nothing emits yet — the CLI
still routes telemetry to Sentry; this is the engine-side groundwork.

`layerAxiomTraces` and `layerAxiomMetrics` are composed by hand rather than
through `Otlp.layer`, because Axiom routes each signal to a different dataset
via a different header and `Otlp.layer` passes one `headers` object to every
signal. Serialization is protobuf, not JSON: Axiom's `/v1/metrics` accepts
`application/x-protobuf` only. `OtlpSerialization.layerProtobuf` ships with
Effect, so this adds no dependency.

They are separate layers on purpose. Effect tracks delta-temporality state on
the metrics exporter instance, so a second exporter in one process starts with
no previous snapshot and re-reports every counter's full value — doubling every
metric. `telemetry-payload.test.ts` pins that behavior so a future refactor that
merges them fails loudly instead of silently doubling data.

OTLP has no `beforeSend` hook, so Sentry's central scrubber has no equivalent.
`makeScrubbingTracer` wraps the tracer and runs every span name, attribute, and
event payload through `anonymizeText` on the way to the exporter, and is applied
inside `layerAxiomTraces` so an attribute added later cannot bypass it.
`anonymize-text.ts` moves to core (both its scrubbers already lived there) and
`run-inspect.ts` now scrubs `inspect.directory` at the source rather than relying
on the backstop.

`TELEMETRY_SHUTDOWN_TIMEOUT_MS` is 1s, tighter than Sentry's 2s error flush: the
metrics exporter passes `maxBatchSize: "disabled"` internally, which skips
Effect's empty-buffer short-circuit, so it POSTs on every scope close whether or
not anything was recorded. On a firewalled machine that request cannot fail fast,
and unlike the Sentry flush it would run on every scan rather than only on crashes.

Verified against a local collector: correct per-signal datasets and protobuf
content type, and no home directory, username, or secret reaches the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): close three review findings in the Axiom transport

Scrub the failure a span ended with. The OTLP tracer runs `Cause.prettyErrors`
over the exit and exports each result as `exception.type` / `exception.message` /
`exception.stacktrace`. None of those go through `span.attribute`, so they
bypassed the scrubbing tracer entirely — and stack traces are full of absolute
paths, meaning any failing scan shipped the user's home directory. This was the
one route into the payload the backstop didn't cover. Interrupt-only causes are
left alone; the exporter emits a fixed label for them and never touches the
error text.

Honor `exportIntervalMs` for traces. It was threaded through to the metrics
exporter but not the tracer, so a long-lived process — the language server —
would hold spans for the full ten-minute default no matter what interval it
asked for.

Trim trailing slashes without a regex. `/\/+$/` backtracks quadratically on a
domain made mostly of slashes, which CodeQL flags as a polynomial-regex denial
of service because the value comes from the caller.

Also corrects a doc comment that still described metrics as being built at exit,
which stopped being true when the CLI moved to a single shared scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:14:46 -05:00
Aiden Bai 2db2a97283 perf: reduce scan startup and workspace contention (#1533) 2026-07-30 19:42:52 -07:00
Aiden Bai 938008119a feat(report): add versioned JSON v3 metadata (#1120)
* feat(report): add versioned JSON v3 metadata

Expose deterministic diagnostic identities and exact scan completeness while preserving v1 and v2 decoding.

* fix(report): make v3 identities and coverage exact

Keep workspace diagnostics distinct and align JSON completeness, telemetry verdicts, and fallback output with the versioned report contract.
2026-07-10 23:49:19 -07:00
cursor[bot] 0eb5293c1d Fix API lint opt-out and deslop traversal cleanup (#1085)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-07-08 18:57:17 -07:00
devin-ai-integration[bot] 6b21b70d1a feat(core): surface reactDetected so a gated-off scan can't pass for clean (#1062)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
2026-07-04 01:42:13 -07:00
Aiden Bai ba2af1b7fa chore: add AI training restriction to license (#936) 2026-06-22 16:28:25 -07:00
Aiden Bai 157177a49f revert: undo direct commits; changes moved to PR 2026-06-21 20:27:27 -07:00
Aiden Bai 9f2845464a chore: add AI training restriction to license (#935)
* chore: add AI training restriction to license

Adds a no-training clause to prevent use of the source code as training,
fine-tuning, or evaluation data for ML models without prior written
permission. Academic researchers can request a no-cost exemption at
founders@million.dev.

Also updates the title to reflect the additional restrictions, and removes
the "outputs" language (legally overreaching — users own output from their
own data).

* chore: simplify training restriction to uniform written-permission requirement

* feat(cli): warn when react-doctor is run inside an AI/ML training pipeline

Detects presence-based env vars for HuggingFace, CUDA/GPU, experiment
trackers (W&B, MLflow, Comet, Neptune), Ray workers, RL simulators
(MuJoCo, Gymnasium), cloud ML platforms (SageMaker, Azure ML, Vertex AI),
AI agent sandboxes (Daytona, E2B, Modal, RunPod, Harbor), and coding-agent
eval harnesses (SWE-bench, SWE-agent). Prints a warning to stderr pointing
to founders@million.dev for written permission.

* feat(cli): expand CI provider and AI/ML training environment detection

CI providers (+18): Semaphore, AppVeyor, Harness, Buddy, Codefresh,
Netlify, Railway, Vercel, Codemagic, Prow, Agola, Cirrus CI, Render,
Fly.io, Blacksmith, WarpBuild, Namespace, Ubicloud

ML/AI training (+21): Kaggle, Google Colab, Databricks, SageMaker
(SM_TRAINING_ENV + TRAINING_JOB_ARN), Azure ML (AZUREML_RUN_ID),
Vertex AI (CLOUD_ML_PROJECT_ID), W&B sweeps, DVC stages, ClearML,
Flyte, Determined AI, Lightning AI, Argo Workflows, Kubeflow Pipelines,
HuggingFace Spaces, Replicate, Vast.ai, Google TPU, ROCm

Coding agents (+3): Cline, Augment, Trae AI

* feat(api,packaging): add AI training warning to programmatic API; fix license labels

- Adds AI/ML environment detection to diagnose() (both single-dir and
  batch forms) so programmatic users (e.g. CodeRabbit integrations) see
  the same license warning as CLI users. Warns once per process via a
  module-level guard. Kept inline to avoid a CLI→API dep.
- Changes all package.json "license" fields from "MIT" to
  "SEE LICENSE IN LICENSE" so automated scanners (Fossa, Snyk,
  TLDR-Legal) report the actual terms instead of plain MIT.

* feat(website): add /license page with commercial contact CTA
2026-06-21 20:22:22 -07:00
devin-ai-integration[bot] d48e7f1695 feat: per-module scoring via diagnose({ projects }), CLI --project paths, and config projects (#771)
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
2026-06-11 21:55:48 -07:00
Nisarg Patel a254414666 feat: Socket.dev supply-chain score check (#747)
* feat(core): add Socket.dev supply-chain score check

New SupplyChain service + checkSupplyChain that score each direct dependency against Socket's free, keyless PURL endpoint (the same lookup sfw's free tier uses) and emit a Security diagnostic for any package below the configured minScore. Implemented with Effect (Effect.forEach bounded concurrency, Effect.tryPromise + Effect.timeout for cancellable fetches, fail-open per package).

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat: wire SupplyChain into the inspect pipeline

Run the supply-chain check as a scan phase in runInspect (skipped in diff/staged mode), and provide its layer at every composition site: gated on supplyChain.enabled in the CLI (build-runtime-layers) and api (diagnose), and a no-op layerOf([]) for editor scans and the run-inspect tests.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* chore: add changeset for Socket supply-chain score check

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat: run Socket supply-chain check by default

Flip the default so the supply-chain score gate runs out of the box; users opt out with `supplyChain: { enabled: false }`. Editor scans and --diff/--staged runs remain off regardless.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(cli): add --sfw demo flag to list Socket scores for all dependencies

New collectSupplyChainScores core helper fetches the Socket score of every direct dependency (reusing the free-tier fetch internals). The CLI's --sfw flag short-circuits before the scan, prints a color-coded worst-first table via renderSupplyChainScores, and exits. Mutually exclusive with --json/--score.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* fix(core): anchor supply-chain diagnostic to the declared package.json entry

The diagnostic now points at the exact line/column where the dependency is declared in package.json (instead of line 0) and names the declared spec plus the entry to edit, so a finding like next@16.2.4 traces directly to its `"next": "16.2.4"` entry. Records the declared spec + key location while collecting dependencies.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* fix(cli): drop the free-API subtitle from --sfw header

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(cli): scan every workspace package.json for --sfw in a monorepo

collectSupplyChainScores now aggregates direct dependencies across the workspace root and every workspace package (pnpm/npm/yarn/nx globs via the shared project-info resolvers), deduped by name@version and anchored at the true monorepo root even when invoked from a sub-package. The in-scan check stays per-project (the scan loop already visits each workspace).

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(core): exclude next from the supply-chain check

next's framework-specific risks are already covered by react-doctor's Next.js / server-components rules, so a low Socket score is redundant noise. Add a SUPPLY_CHAIN_IGNORED_PACKAGES set and filter it in the shared dependency collector so both the gate and the --sfw listing skip it.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* refactor(core): drop unused breakdown field and trim Socket artifact schema

The --sfw renderer only reads name/version/overall, so the per-axis breakdown on DependencyScore was speculative dead code — remove it and its construction. Also trim SocketArtifactSchema to the only field read (score); Schema.Struct already ignores the artifact's other fields.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* fix(core): anchor supply-chain diagnostic within its dependency section

locateDependencyKey previously returned the first `"name":` match anywhere in package.json, so a same-named entry under overrides / resolutions / pnpm.overrides could win over the real dependencies/devDependencies line. Scope the search to the declaring section via its `"<section>": {` header + brace-depth tracking. Fixes Cursor Bugbot finding.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* refactor(api): derive supplyChain enablement from config in buildDiagnoseLayer

Drop the standalone supplyChainEnabled parameter; buildDiagnoseLayer now takes the effective config (and optional override directory), builds the config layer from it, and reads supplyChain.enabled from that same config. One config input drives both the Config service and the supply-chain gate.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(core): emit a per-package OTel span for each Socket score lookup

Wrap fetchSocketScore in a SupplyChain.fetchScore span carrying socket.package / socket.version / socket.purl, and annotate the resolved axis scores (socket.score.overall + one per SCORE_AXES dimension, 0-100) plus socket.scored. Lets a trace backend group by package and query score distributions across a scan. No-op without a tracer.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* refactor(core): inline buildScoreSpanAttributes into its single call site

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(core): run supply-chain in diff mode when the manifest changed

Add supplyChainManifestChanged to InspectInput/InspectOptions and gate the supply-chain phase on (!isDiffMode || supplyChainManifestChanged), so a diff/PR scan that edits package.json still scores its dependencies instead of skipping the check like the other whole-project passes.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(cli): scan a workspace whose package.json changed in a PR diff

Detect per-project package.json changes (projectManifestChanged, monorepo-aware via the same prefix logic as the include-path resolver). A manifest-only-changed workspace is no longer skipped from the diff loop: it enters the scan with the manifest as the sole include (stays in diff mode, lints nothing) and supplyChainManifestChanged set, so its dependencies get scored. Threaded the flag through the inspect wrapper + scan-result cache key.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* refactor(cli): share project-relative-directory boundary logic across diff helpers

projectManifestChanged duplicated resolveProjectDiffIncludePaths's project-relative + ..-boundary handling. Extract resolveProjectRelativeDirectory and repoint both so the monorepo prefix rule lives in one place and can't drift.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* docs(changeset): reflect diff-mode manifest scoring, monorepo --sfw, and next exclusion

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* fix(cli): make --sfw exclusive with diff/staged and score the base manifest in baseline mode

Bugbot triage: (1) --sfw now errors when combined with --staged/--diff (was silently ignored). (2) Baseline diff scans now forward supplyChainManifestChanged to the base pass and include package.json in the diff include set, so the base manifest is scored and computeDiagnosticDelta filters pre-existing low-score deps instead of reporting them as newly introduced. Verified e2e: pre-existing low dep filtered, newly-added one reported.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

* feat(core): list all axis scores in the supply-chain diagnostic message

Replace the single 'Lowest area' summary with the full per-axis breakdown (supply chain / vulnerability / maintenance / quality / license, 0-100) so the message shows exactly which dimensions dragged the score down. Drops the now-redundant findLowestAxis.

Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Nisarg Patel <NisargIO@users.noreply.github.com>
2026-06-08 22:39:45 -07:00
Aiden Bai d17dc87865 feat(config): add defineConfig helper and read react-doctor.config.json fallback (#721) 2026-06-07 03:09:29 -07:00
Ray Arayilakath 9777f1ac45 feat(cli): add rules management commands and doctor-explain skill (#617)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 05:55:47 -04:00
Aiden Bai b3c3aa9401 chore: codebase audit cleanup (dead code, dedup, CI/config fixes) (#619) 2026-05-31 19:36:40 -07:00
Aiden Bai 6e59f10ef8 feat: new onboarding (#596) 2026-05-31 02:24:23 -07:00
Aiden Bai db277ee678 fix(api): surface lint failures via diagnose().skippedChecks (#559) 2026-05-28 19:17:31 -07:00
Aiden Bai 07b8a6c3e0 fix: cross-platform spawn errors (#498, #501) and add Windows/macOS CI (#502) 2026-05-26 18:19:41 -07:00
Rayhan Noufal Arayilakath 9e0f96944e chore: run the formatter lol 2026-05-26 17:17:50 -07:00
Aiden Bai e332310146 refactor(api): rename diagnoseModules → diagnoseProjects, clean up API shape
- Single object arg (`DiagnoseProjectsInput`) instead of two positional args
- "Projects" vocabulary matching the rest of the codebase
- Per-project scan options (`deadCode`, `lint`, etc.) flat on the entry
  instead of nested in `config.deadCode`
- `config` on a project entry means `ReactDoctorConfig` directly
- Discriminated union `ProjectResult` (`ok: true | false`) instead of
  separate `modules` + `errors` arrays
2026-05-26 16:23:04 -07:00
Aiden Bai a7b72e19a1 feat(api): add diagnoseModules() for native per-module scoring (#499) 2026-05-26 16:13:03 -07:00
Cursor Agent 7dff4727b9 refactor(api): use core's exported layerInspectLive instead of reimplementing it
api/diagnose.ts had a private `buildLayerStack()` that constructed
the exact same Layer.mergeAll as core's already-exported
`layerInspectLive`. Two consumers of the same set with no
divergence.

Drop the local rebuild and import `layerInspectLive` directly.
Removes 9 lines of inline service plumbing + 9 redundant imports
from the api package (Config, DeadCode, Files, Git, Linter,
LintPartialFailures, Progress, Project, Reporter, Score).

Any future change to the default production layer stack (e.g. adding
a new service to runInspect's requirements) now propagates to both
api and the core's documented default automatically.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-25 15:06:52 +00:00
Cursor Agent d23fac41db refactor(core): wire Progress service into runInspect; drop spinner Ref plumbing
The Progress service (core/src/services/progress.ts) defined layerOra,
layerNoop, and layerCapture but the CLI bypassed it: spinner state lived
at module scope (setSpinnerSilent), spinner handles flowed through
runInspect's hooks via Ref<SpinnerHandle | null>, and the dead-code
spinner ran post-orchestrator in the CLI shell.

Finish the migration:

- runInspect now requires Progress and owns the lint + dead-code
  spinner lifecycle. Each phase is a separate Stream.runCollect so
  the progress lifecycle can render a per-phase status line; the
  per-element pipeline (auto-suppress / severity / ignore / inline /
  rn-no-raw-text suppression / reporter.emit) applies uniformly to
  env + lint + dead-code streams.
- CLI provides Progress.layerOra wired to the existing ora-backed
  spinner() helper, or Progress.layerNoop for --silent / --score-only /
  lint-skipped runs.
- api provides Progress.layerNoop (programmatic callers don't want
  stderr frames).
- Drop the Ref<SpinnerHandle | null> + finalSpinnerHandle plumbing
  from react-doctor/inspect.ts. The CLI's post-orchestrator code only
  needs the supplementary 'upgrade Node X' / failure-reason hint
  lines now.
- Drop the post-orchestrator dead-code spinner — the orchestrator
  renders it via Progress.
- run-inspect.test.ts layer stacks updated with Progress.layerNoop.

setSpinnerSilent / isSpinnerSilent remain in cli/utils/spinner.ts
because printProjectDetection still calls spinner() directly for its
'static checklist' lines (Detected framework, etc.). That callsite
isn't going through the Progress service yet; a follow-up can migrate
it.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-25 11:35:00 +00:00
Cursor Agent 92a54ac2f5 refactor(core): add resolveScanTarget + share restoreLegacyThrow
The config-redirect + project-discovery dance (loadConfigWithSource +
resolveConfigRootDir + resolveDiagnoseTarget) was duplicated in three
places: api/diagnose.ts, react-doctor/inspect.ts, and the CLI's
cli/commands/inspect.ts. Each shell loaded its own config and then
the orchestrator's Config.layerNode loaded it again via its own cache.

Centralize the entry-point translation:

- New core/src/resolve-scan-target.ts exports resolveScanTarget() that
  loads the config, honors rootDir, and walks into a nested React
  subproject (raising AmbiguousProjectError as before).
- All three shells call it once at the top. The CLI command shell
  surfaces `didRedirectViaRootDir` directly instead of comparing
  pointers.

The legacy thrown-class round-trip (plain Error -> tagged
ReactDoctorError -> plain Error) was also duplicated verbatim in
inspect.ts and api/diagnose.ts via two identical restoreLegacyThrow
closures.

Centralize the boundary:

- Move restoreLegacyThrow into core/src/errors.ts. Both shells import
  it.
- The orElse clause now re-dies the ReactDoctorError instance itself
  (instead of `new Error(error.message)`). Legacy grep-stderr
  callers still see the same `error.message`; advanced callers can
  now narrow on `error.reason._tag` without losing the tagged
  shape across the api/CLI boundary.

api dist shrank from 3.82 kB -> 2.34 kB (39% smaller).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-25 11:21:57 +00:00
Aiden Bai cdb542b7ac fix 2026-05-25 02:18:22 -07:00
Aiden Bai fd6f46f631 fix 2026-05-23 02:15:11 -07:00
Aiden Bai 99eba624a5 feat(core): wire opt-in OpenTelemetry export via REACT_DOCTOR_OTLP_* (#444)
Adds `layerOtlp` in `core/src/observability.ts` and provides it to
both `inspect()` (CLI) and `diagnose()` (programmatic API). The
layer is a strict no-op unless BOTH env vars are set:

  REACT_DOCTOR_OTLP_ENDPOINT     e.g. https://api.axiom.co
  REACT_DOCTOR_OTLP_AUTH_HEADER  e.g. Bearer <token>

When configured, it provides `Otlp.layerJson({...})` from
`effect/unstable/observability/Otlp` with
`@effect/platform-node/NodeHttpClient.layerUndici` as the
transport. Every existing `Effect.fn("Service.method")` span and
the top-level `Effect.withSpan("runInspect", ...)` ship to the
configured backend without any further wiring.

Mirrors the canonical eval pattern from
`react-doctor-evals/src/Observability.ts → layerAxiom`. Closes the
"Bigger bet #7" item from the prioritized-improvements list.

**No behavior change for default users** — the layer drops to
`Layer.empty` when env vars are absent, so the unconfigured path
has zero network traffic and zero new dependencies at runtime.

Tests cover the no-op path. AGENTS.md and the CLI README document
the opt-in.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 01:44:17 -07:00
Aiden Bai aa61efae28 refactor: collapse @react-doctor/types and @react-doctor/project-info into core (#440)
* refactor: collapse @react-doctor/types and @react-doctor/project-info into core

Eliminates two private workspace packages whose only role was to
namespace shared TS types and project-discovery helpers — neither
was a real consumer boundary, and the indirection cost a `workspace:*`
edge per leaf package without saving any code.

**Layout**

- `packages/types/` → `packages/core/src/types/` (10 files, pure TS types)
- `packages/project-info/` → `packages/core/src/project-info/`
  (27 files, discoverProject + framework detection + monorepo helpers)
- `packages/types/src/react-native-dependency-names.ts` →
  `packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts`
  (the canonical RN-aware-package list lives next to its heaviest
  consumer — the rule gate — and is re-exported from core to avoid
  the rule-plugin ↔ core dependency cycle the previous types package
  was specifically designed to break).

The two packages are deleted from the workspace; every consumer
(`react-doctor`, `api`, `oxlint-plugin-react-doctor`, `actions/review`,
+130 source/test files) now imports from `@react-doctor/core` directly.

**Errors cleanup**

Dropped the legacy plain-class `ReactDoctorError` and `isReactDoctorError`
from `project-info`. The four narrow errors (`ProjectNotFoundError`,
`NoReactDependencyError`, `PackageJsonNotFoundError`,
`AmbiguousProjectError`) now extend `Error` directly — the only valid
hierarchy is the tagged `Schema.TaggedErrorClass` `ReactDoctorError`
from `core/errors.ts`, which has been the canonical one since the
Effect v4 refactor. Added `isProjectDiscoveryError` as a narrow type
guard for the four leaves.

The discovery errors are thrown synchronously BEFORE the Effect
runtime takes over (at `diagnose()` / CLI entry points); inside the
runtime, the Project service's `translateProjectInfoError` converts
each one to its tagged-reason equivalent.

**Schema vs type name collisions**

`schemas.ts` exports `Diagnostic` / `JsonReport` / `JsonReportMode` /
… as Schema.Class values, while `types/` exports the same names as
TS types. The core barrel re-exports the TS-type versions
(broad-strokes API) and exposes the Schema classes via a new
`@react-doctor/core/schemas` subpath export for the two callers
that need them (`Reporter` service, smoke validator, schema tests).

**Validation**

- `pnpm typecheck` ✓
- `pnpm lint` ✓
- `pnpm format` ✓
- `pnpm test` ✓ (1449 passing, 3 skipped — no regressions)
- `pnpm smoke:json-report` ✓

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core): keep RN-dependency constants out of types barrel to avoid pulling oxlint plugin into discovery

Bugbot caught (medium severity): re-exporting `isReactNativeDependencyName` from
`oxlint-plugin-react-doctor` via the core types barrel meant every consumer of
`@react-doctor/core` (including `discoverProject` / `discoverReactSubprojects`)
would load the entire 286-rule plugin at module-init.

Adds a tiny standalone leaf at `core/src/project-info/internal-rn-dependency-names.ts`
(zero imports, just the constants the workspace-discovery gate needs) and switches
`is-package-json-react-native-aware.ts` to use it. The types barrel no longer
re-exports the plugin-side constants — rule consumers import them directly from
`oxlint-plugin-react-doctor`.

The duplication is intentional and documented at both files; the two lists are
short enough that a future regression test in the plugin can observe the union.

Validated: typecheck + test (1449 pass) + lint + format all green.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 01:32:44 -07:00
Aiden Bai 2a02c04203 refactor: adopt v4-native Effect.catchReasons + Effect.try; ban try/catch in Effect.gen (#434)
Deep audit against the Effect v4 patterns documented in
\`tmp/effect/.patterns/\` and \`tmp/effect/migration/\` (the cloned
reference repo). Three real v3-style holdovers fixed:

### 1. Manual instanceof reason dispatch → Effect.catchReasons

\`get-diff-files.ts\`, \`api/diagnose.ts\`, and \`inspect.ts\`'s
\`restoreLegacyThrow\` all had imperative \`switch (reason._tag)\`
ladders behind an \`if (cause instanceof ReactDoctorError)\` JS
catch block. v4 ships \`Effect.catchReasons(errorTag, cases, orElse?)\`
exactly for this — exhaustive, type-safe narrowing of a
\`Schema.TaggedErrorClass\` reason union, no \`instanceof\` checks.

  - \`getDiffInfo\` →
    \`.pipe(Effect.catchReasons("ReactDoctorError", { GitBaseBranchInvalid, GitBaseBranchMissing }))\`
  - \`diagnose\` + \`inspect\`'s legacy translation →
    \`.pipe(Effect.catchReasons("ReactDoctorError", { NoReactDependency, ProjectNotFound, AmbiguousProject }, orElse))\`
    The \`orElse\` branch preserves the legacy "any other tagged
    reason throws as a plain Error with the tagged-class message
    string" contract that grep-stderr callers depend on.

The recovered branches use \`Effect.die(new LegacyClass(...))\` so
the thrown class lands on \`Effect.runPromise\`'s reject channel
unchanged.

### 2. try/catch inside Effect.gen → Effect.try + Effect.orElseSucceed

\`render-summary.ts\`'s "write the dump, swallow if it fails"
block had a literal \`try { ... yield* Console.log(...) } catch {}\`
inside an \`Effect.gen\` — a hard v4 anti-pattern (the catch only
runs for sync throws, never for Effect failures, so the shape is
misleading and the v4 patterns doc forbids it).

Replaced with:

  Effect.try({ try: () => writeDiagnosticsDirectory(...), catch: (e) => e })
    .pipe(Effect.orElseSucceed(() => null))

### 3. AGENTS.md updates

New \`Error dispatch / recovery — v4 idioms\` and \`Generator hygiene\`
subsections in the Effect v4 Conventions block. Documents:
  - When to use \`catchReasons\` / \`catchTag\` / \`catch\` / \`die\`
  - Hard rule: NEVER \`try/catch\` inside \`Effect.gen\` — use
    \`Effect.try\` and \`Effect.orElseSucceed\` / \`Effect.catch\` instead
  - \`return yield* Effect.fail(...)\` for terminal effects
  - When \`Effect.fnUntraced\` is justified (hot paths only; not
    currently needed in this codebase)

### Misc

- Removed now-unused \`isReactDoctorError\` + \`InspectOutput\`
  imports from \`inspect.ts\`.
- Removed unused \`ReactDoctorError\` import from \`api/diagnose.ts\`
  (catchReasons does the narrowing).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 19:02:18 -07:00
Aiden Bai 5c13e50940 fix(runtime): self-audit cleanup against AGENTS.md Effect v4 rules (#420)
Self-audit after #419 documented the patterns. Caught and fixed six
issues I introduced across #414 and #417 that contradict the rules
the AGENTS.md update codified.

## P0 — AGENTS.md violations

(1) packages/react-doctor/src/inspect.ts:2 used the umbrella import
'from "effect"' — replace with individual 'import * as Effect /
Layer / Ref from "effect/X"' lines. AGENTS.md "ALWAYS: import * as
X from 'effect/X'" rule.

(2) inspect.ts had three '[...output.diagnostics] as Diagnostic[]'
casts that aren't needed — spreading a ReadonlyArray<Diagnostic>
already gives Diagnostic[]. AGENTS.md "MUST: Do not type cast 'as'
unless absolutely necessary" rule.

(3) inspect.ts catch had two consecutive identical predicates:

    if (cause instanceof ReactDoctorError) restoreLegacyThrow(cause);
    if (isReactDoctorError(cause)) restoreLegacyThrow(cause);

isReactDoctorError IS instanceof ReactDoctorError; the second is
dead code (restoreLegacyThrow always throws so the first match
exits). Drop the duplicate.

## P1 — dead code / unnecessary casts

(4) packages/api/src/diagnose.ts had a try/catch around
resolveDiagnoseTarget where every branch re-threw cause unchanged:

    try { ... } catch (cause) {
      if (cause instanceof AmbiguousProjectError) throw cause;
      if (cause instanceof PackageJsonNotFoundError) throw cause;
      throw cause;
    }

The if-statements were no-ops. Drop the wrapper + the unused
PackageJsonNotFoundError import.

(5) packages/core/src/run-inspect.ts had two 'Stream.empty as
Stream.Stream<Diagnostic, never>' casts. Stream.empty is
Stream<never> which IS assignable to Stream<Diagnostic, never> via
subtyping; the cast was a TS-inference workaround. Replace with a
single typed local 'const emptyDiagnosticStream:
Stream.Stream<Diagnostic, never> = Stream.empty' shared across
both call sites.

## Other

(6) inspect.ts Score HACK comment was misleading — claimed the
orchestrator's Score would see the 'FULL diagnostic list', but it
actually sees the per-element-filtered list (just not score-surface-
filtered). Updated the comment to match reality. Also dropped the
redundant spread in 'lintPartialFailures: [...output.lintPartialFailures]'
(input is already ReadonlyArray<string>).

## Validation

- pnpm typecheck (12/12)
- pnpm test — 123 files / 1485 pass / 3 skipped
- pnpm lint, format:check
- pnpm build (8/8)
- pnpm smoke:json-report — schema-valid v1
- grep 'as Diagnostic|as Stream\.|from "effect"' across packages/
  src — zero hits

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 17:12:28 -07:00
Aiden Bai a3539c9afb feat(api): new @react-doctor/api package — diagnose() backed by runInspect (#414)
Standing up packages/api/ as the home of the programmatic public
API. Moves diagnose() into it as a thin Effect.runPromise shell
around #412's runInspect orchestrator, with tagged-error translation
back to legacy thrown classes (NoReactDependencyError /
ProjectNotFoundError / AmbiguousProjectError) for backwards compat.

inspect() stays in react-doctor/src/ for now (CLI rendering coupled
in). PR 6 (cli package) moves it.

## Files

- packages/api/ (private workspace package)
  - src/diagnose.ts: pre-resolves rootDir redirect + resolveDiagnoseTarget,
    then runInspect, translates tagged failures, returns DiagnoseResult.
  - src/index.ts: re-exports diagnose + public types + legacy errors.
  - tests/diagnose.test.ts: 4 tests — happy path, NoReactDependency,
    ProjectNotFound, elapsedMilliseconds positive.
- packages/react-doctor/src/index.ts: deletes ~120-line local diagnose,
  re-exports from @react-doctor/api.
- packages/core/src/run-inspect.ts: rejects projects without React via
  tagged NoReactDependency (used to happen in the legacy diagnose).

## Validation

- pnpm typecheck (12/12)
- pnpm test — 123 files / 1485 pass / 3 skipped
- pnpm lint, format:check, build, smoke:json-report — all green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 16:42:52 -07:00