Per AGENTS.md, all magic numbers belong in constants.ts using
SCREAMING_SNAKE_CASE with unit suffixes. Three holdouts were still
declared at module-scope in services/runners:
- CONFIG_CACHE_CAPACITY (16), CONFIG_CACHE_TTL_MS (5 * 60 * 1000)
from services/config.ts
- PREVIEW_COUNT (3) from runners/oxlint/spawn-batches.ts
Hoist all three into constants.ts with descriptive docs. Renamed
PREVIEW_COUNT to OXLINT_PARTIAL_FAILURE_PREVIEW_COUNT to make its
ownership clear from the global namespace.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
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>
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>
The streaming buildDiagnosticPipeline (used by runInspect) and the
array-shaped merge-and-filter-diagnostics + filter-diagnostics +
combine-diagnostics + array-form apply-severity-controls implemented
overlapping logic. The array path had rn-no-raw-text textComponents /
rawTextWrapperComponents short-circuiting that the streaming path
lacked — so tests asserted behavior production didn't have.
Collapse to one pipeline:
- buildDiagnosticPipeline is now the single source of truth. The
rn-no-raw-text textComponents + rawTextWrapperComponents enclosure
checks moved into the streaming pipeline (runtime now matches the
previously-test-only behavior).
- Shared raw-line scanners live in core/src/utils/
jsx-text-component-matchers.ts. resolveCandidateReadPath moved into
core/src/utils/ alongside.
- mergeAndFilterDiagnostics is now a 10-line array-shaped wrapper
over buildDiagnosticPipeline. It is the only legacy entry point we
keep, for back-compat with tests + advanced programmatic callers.
- Deleted filter-diagnostics.ts (filterIgnoredDiagnostics,
filterInlineSuppressions, the JSX wrapper helpers), combine-
diagnostics.ts (test-only consumer), and the array form of
apply-severity-controls (applySeverityControls). restampSeverity +
SEVERITY_FOR_OVERRIDE remain because the pipeline uses them.
- clearAutoSuppressionCaches is preserved as a no-op for back-compat
(the streaming pipeline holds caches in per-pipeline closures).
- Tests for the removed array helpers migrated to mergeAndFilterDiagnostics.
Net deletion: ~380 lines, plus a misleading 'single source of truth'
doc comment that is now factually correct.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The orchestrator's Score service was wired up but always disabled by the
production CLI via Score.layerOf(null) so the CLI could apply
filterDiagnosticsForSurface("score", ...) and recompute the score
outside the Effect pipeline. api/diagnose() used Score.layerHttp without
surface filtering, so api and CLI returned different scores for the
same project + config.
Push the surface filter into runInspect:
- Add scoreSurface?: DiagnosticSurface to InspectInput, default "score"
- Apply filterDiagnosticsForSurface before scoreService.compute inside
the orchestrator
- CLI provides Score.layerHttp (or layerOf(null) when --no-score) and
consumes output.score directly; the post-orchestrator calculateScore
re-run is gone
- api inherits the "score" default, aligning api and CLI numbers
Deletes the HACK-commented Score.layerOf(null) sidecar in
build-runtime-layers.ts and the calculateScore call in
react-doctor/inspect.ts. The orchestrator now owns scoring end-to-end.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>