* fix: prevent stack overflow in function resolution with depth limiting
Add depth limiting to resolveExactLocalFunction to prevent infinite
recursion when resolving function references through deeply nested
expressions, TypeScript path aliases, or complex import chains.
The function now enforces a configurable depth limit
(FUNCTION_RESOLUTION_MAX_DEPTH = 15) and returns null when exhausted
rather than continuing to recurse.
- Add FUNCTION_RESOLUTION_MAX_DEPTH constant (15 levels)
- Thread remainingDepth parameter through all resolution helpers
- Add unit tests for depth limiting
- Add regression test for zustand + Next.js + path aliases case
Closes#1657
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* test: exercise function resolution stack guard
* refactor: share function chain fixture builder
* test: tighten function resolution regressions
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
* feat(cli): record React-aware browser traces
Add an interactive Playwright scanner that streams DevTools traces to disk and reports React rendering hotspots for humans and agents.
* fix(cli): use shared telemetry anonymizer
Keep runtime URL redaction on the canonical core utility after the telemetry helper consolidation.
* feat(cli): visualize component renders during traces
Flash labeled component outlines during runtime scans so users can connect slow interactions to the React work they trigger.
* feat(skill): add runtime performance workflow
Guide agents through repeatable React traces so performance findings stay tied to measured interactions.
* docs(cli): guide runtime scan setup
Explain the recording flow, browser isolation, authenticated sessions, output formats, and trace privacy directly in command help.
* fix(cli): harden runtime trace capture
Preserve evidence across navigations while keeping structured output, telemetry, trace files, and attached-browser cleanup safe under failures and cancellation.
* fix(cli): preserve latest navigation evidence
Keep cross-document layout shifts comparable and retain the most recent bounded performance entries so the recorded interaction remains represented.
* fix(cli): bound runtime scan browser resources
Prevent trace deadlocks, cross-tab capture, and unbounded browser evidence while preserving navigation data through an isolated validated relay.
* fix(cli): preserve busy-page runtime snapshots
Stop tracing concurrently with final probe collection and allow busy pages enough time to flush their bounded performance evidence.
* feat(cli): prompt for runtime scan URL
Let people start an interactive runtime scan without remembering the URL argument while keeping agents and CI deterministic.
* feat(cli): suggest local apps for runtime scans
Replace the bare URL field with a polished chooser that discovers common localhost development servers while preserving custom URL entry.
* fix(cli): use scan terminology in runtime flow
Keep the interactive copy aligned with the command users invoked instead of switching between recording and profiling language.
* fix(cli): center runtime chooser in full terminal
Render the interactive URL chooser as a terminal-sized view so it no longer appears anchored to the previous cursor position.
* fix(cli): harden runtime scan edge cases
Capture popup evidence without leaking browser pages, support redirected interactive output and IPv6 localhost apps, and keep bounded reports honest.
* feat(lsp): move editor telemetry to Axiom and unify the opt-out
Completes the cutover. The language server gets the same split as the CLI —
spans and metrics to Axiom, crashes to Sentry — with one difference that matters:
an editor session can run for hours and may never shut down cleanly, so its
exporters use a periodic interval instead of relying on a shutdown flush. Its
counters also now route through `record-metric.ts` rather than calling the Sentry
SDK directly, so they finally carry the run attributes every other metric has.
`is-telemetry-enabled.ts` becomes the single gate for every backend, which fixes
a real hazard: `REACT_DOCTOR_NO_TELEMETRY` was previously honored only by the
language server, and blanking `SENTRY_DSN` no longer silences anything. The evals
harness relied on exactly that, so it would have started reporting eval scans as
production telemetry; it now sets the env flag like the benchmark and delta-audit
runners already did.
Removes what the cutover made dead: the Sentry metric scrubber (no CLI or LSP
metrics flow through Sentry anymore), `resolveTracesSampleRate` and its
constants (neither client records spans), and the span-status and HrTime
constants the deleted tracer bridge used.
Rewrites the AGENTS.md observability section, which described a Sentry-only
architecture, and updates the `--debug` docs wording.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(lsp): correct editor span timing, attribution, and startup export
Give the wide event its real duration. `Effect.makeSpan` stamps the start time
from the clock, and the event is emitted once the burst has already finished —
so start and end landed at the same instant and every editor scan reported a
duration of ~0, even though `durationMs` was correct as an attribute. The span
is now built straight off the tracer, which takes an explicit `startTime`, so
the scan's real window is supplied on both ends. This restores what the Sentry
path got from `startTime`.
Attribute editor telemetry to the editor. Routing LSP counters through
`recordCount` picked up the shared run context, which classified the process as
`command: "inspect"` / `origin: "cli"` — so editor metrics were misfiled in
Axiom and disagreed with the LSP's own Sentry scope, which tags them
`experimental-lsp` / `lsp`. `experimental-lsp` is now a recognized command and
implies the `lsp` origin.
Open the exporters at startup. They were built lazily from the first completed
scan, so a session that recorded session-start counters and was then killed
before any scan finished exported nothing — the shutdown flush found no scope.
Building at init also starts the periodic interval ticking, which is the point
of having one for a daemon.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cli): send traces, metrics, and the wide event to Axiom
Moves the CLI's telemetry onto the Axiom transport added in the previous commit.
Sentry keeps crash reporting — its source-map symbolication, issue grouping, and
quotable event id have no Axiom equivalent — so error reports, the prefilled
issue link, and the JSON report's `sentryEventId` are unchanged. Because Effect
exposes a single `Tracer` reference, the two backends are mutually exclusive for
spans, so `tracesSampleRate` is pinned to 0 and the Effect-to-Sentry bridge is
deleted.
Metrics and spans move together because they share one runtime, and that sharing
is load-bearing. `telemetry-runtime.ts` builds the layer once into a long-lived
scope; the run root span, the scan program, and the renderer all draw their
tracer from it. Child spans then nest under the run span natively, with no
`ExternalSpan` stitching. More importantly, Effect tracks delta-temporality state
per metrics exporter instance, so building the layer per program would make every
new exporter re-report each counter in full — silently doubling every metric.
`shutdownTelemetry()` is memoized for the same reason: several exit paths can
fire, and a `--debug` user error flushes before the top-level catch does.
`recordCount` / `recordDistribution` keep their exact signatures, so all ~40 call
sites are untouched; only the body changes to write into Effect's metric
registry. Two constraints differ from Sentry: Effect metric attributes are
string-only, so numbers and booleans are stringified while null and undefined are
dropped rather than coerced to "null"; and histograms need explicit buckets, so
distributions share `METRIC_DISTRIBUTION_BOUNDARIES`. Metrics never touch the
tracer, so they are scrubbed at the emit site instead of by the scrubbing tracer.
`--debug` now prints the Axiom trace id, and crashes carry that id plus `runId`
as Sentry tags — so one pasted id resolves in both backends, replacing the
propagation-context linkage that dies with Sentry tracing.
Verified end to end against a local collector with a real scan: one trace request
and one metrics request, root span with `runInspect` nested under it, the full
wide event, `scan.completed` recorded exactly once, and no home directory or
username on the wire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): close six review findings in the Axiom cutover
Restore export to a user's own collector. `applyObservability` installed a
backend only when first-party Axiom credentials resolved, so anyone who had
pointed React Doctor at their own OTLP endpoint silently stopped getting spans
once the CLI stopped falling back to `layerUserOtlp` unconditionally. The shared
runtime now builds whenever either first-party credentials or
`REACT_DOCTOR_OTLP_*` are present, and `layerObservability` still prefers the
user's endpoint.
Export metrics for commands that never scan. `version`, `install`, `ci`,
`rules`, and early `inspect` exits record counters like `cli.invoked`, but only
the scan path built the exporter — so `shutdownTelemetry` found no scope and
dropped them. It now ensures the runtime exists before closing it, which is a
no-op when a scan already opened one.
Namespace the environment overrides as `REACT_DOCTOR_AXIOM_*`. The bare
`AXIOM_TOKEN` / `AXIOM_DATASET` names are Axiom's own, and anyone already
running Axiom likely has them exported — reading them would redirect React
Doctor's telemetry into an unrelated account without the user asking.
Carry the Axiom trace id and run id on crash reports as a *context* rather than
tags. Both are unique per run, and tags are the indexed low-cardinality
dimension — AGENTS.md calls this out for `runId` specifically.
Also replaces an inline nanosecond conversion with a named constant, and defers
`LSP_TELEMETRY_EXPORT_INTERVAL_MS` to the commit that actually reads it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cli): embed the Axiom ingest token
Enables first-party telemetry. Until now the constant was empty, so
`resolveAxiomTelemetryOptions` returned null and the CLI recorded into Effect's
registry and dropped everything on exit — the merge blocker called out on this
stack.
The token is `ingest:create` only, scoped to the `react-doctor` (Events) and
`react-doctor-metrics` (OTel Metrics) datasets, with no expiry and no
organization permissions. It ships in the published tarball and is therefore
extractable; that was the deliberate trade (the alternative — proxying ingest
through `www.react.doctor`, which already hosts the score API — remains
available if abuse shows up). Detection is an Axiom monitor on anomalous ingest
volume; rotation is a patch release.
Verified against the live service: `/v1/traces` and `/v1/metrics` both return
200 for protobuf payloads with their respective dataset headers, which also
confirms the protobuf serialization choice end to end rather than just against
a local collector.
Two tests moved with it. They asserted the "no token configured" path, which is
no longer reachable through the environment; they now assert the embedded
defaults resolve, and — the part that actually matters — that Axiom's own
unprefixed `AXIOM_*` variables still cannot redirect the destination.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove the legacy interactive renderer fallback by preserving scoped scan behavior in the Ink dashboard. Includes workspace dead-code ownership and degraded-baseline gate parity.
Bundle the Ink and React renderer so inspected projects cannot supply a missing or incompatible React runtime. Exercise the packed CLI in a real PTY against a React 18 fixture.
* fix(rules): correct form and fragment diagnostics
* fix(rules): handle fragment review edge cases
* fix: handle document-top fragment links
* fix(a11y): parse ids from html tags only
* fix(a11y): resolve static fragment aliases
* fix(a11y): retain known fragment targets
* fix(a11y): tokenize html id attributes
* fix: preserve known fragment IDs in mixed spreads
* fix: preserve known fragment ID defaults
* 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>
* NEW_RULES: Reapply rule analysis foundation
Reapply the complete reviewed NEW_RULES stack on top of current main after the rollback.
* fix(rules): harden new diagnostics against false positives
* fix(rules): preserve dynamic context providers
* fix(core): resolve styled-components workspace versions
* fix(rules): harden new diagnostics after deep audit