From 1fab58586e72972fdfac8c6c776eb485a46a1eab Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Wed, 29 Jul 2026 11:49:21 +0000 Subject: [PATCH] docs: record architecture and sync shipped skill --- .agents/references/coding-conventions.md | 57 +++ .agents/references/effect-v4.md | 74 +++ .agents/references/observability.md | 72 +++ .agents/references/release-safety.md | 43 ++ .agents/references/repository-architecture.md | 44 ++ .agents/references/testing.md | 43 ++ .agents/skills/product-thinking/SKILL.md | 4 +- .../skills/react-doctor/references/explain.md | 1 + .changeset/sync-react-doctor-skill.md | 5 + .gitignore | 2 + AGENTS.md | 430 +----------------- docs/README.md | 18 + docs/architecture/compatibility.md | 47 ++ docs/architecture/rewrite.md | 310 +++++++++++++ .../tests/react-doctor-skill-parity.test.ts | 57 +++ skills/react-doctor/references/explain.md | 2 +- 16 files changed, 795 insertions(+), 414 deletions(-) create mode 100644 .agents/references/coding-conventions.md create mode 100644 .agents/references/effect-v4.md create mode 100644 .agents/references/observability.md create mode 100644 .agents/references/release-safety.md create mode 100644 .agents/references/repository-architecture.md create mode 100644 .agents/references/testing.md create mode 100644 .changeset/sync-react-doctor-skill.md create mode 100644 docs/README.md create mode 100644 docs/architecture/compatibility.md create mode 100644 docs/architecture/rewrite.md create mode 100644 packages/react-doctor/tests/react-doctor-skill-parity.test.ts diff --git a/.agents/references/coding-conventions.md b/.agents/references/coding-conventions.md new file mode 100644 index 0000000000..bc45aeacc4 --- /dev/null +++ b/.agents/references/coding-conventions.md @@ -0,0 +1,57 @@ +# Code conventions + +Use this reference for every code change. Root [AGENTS.md](../../AGENTS.md) makes these rules binding. + +## Package commands + +- MUST use `ni` to install dependencies, `nr SCRIPT_NAME` to run a declared script, and `nun` to remove dependencies +- Run workspace scripts with `nr --filter workspace_name script_name` +- Do not invoke `npm run` or `pnpm run` manually when `nr` can run the same script +- Existing `package.json` scripts may use `pnpm` for workspace plumbing. Do not rewrite those script bodies only to replace the package-manager command + +## TypeScript + +- Use an interface for an object shape that callers construct, implement, or extend +- Use a type alias for unions, primitives, tuples, function signatures, mapped or conditional types, schema-derived types, and re-exports +- Declare shared types at module scope in the narrowest owning module. Do not add ambient global declarations unless a global runtime integration requires them +- Prefer arrow functions when an arrow and a declaration express the same behavior. Use declarations when the language or framework requires them, including generators and overloads +- Avoid type assertions. Assert only at a validated boundary that TypeScript cannot narrow, and keep the assertion next to that validation +- Use `Boolean(value)` instead of `!!value` + +## Ownership and naming + +- Use kebab-case file names +- Use descriptive variable names. Revisit names after the behavior is clear +- Keep a helper beside its only consumer. Move it into a domain `utils/` directory only when several files reuse a domain-neutral leaf operation +- Keep each utility file focused. A `utils/` directory is not the default home for domain behavior +- Keep a constant beside its owning domain. Use a domain `constants.ts` only when several files share the constants +- Extract a number when its meaning, unit, or reuse matters. Use `SCREAMING_SNAKE_CASE` and a unit suffix such as `_MS` or `_BYTES` when the value has a unit +- Remove unused code and consolidate repeated behavior +- Search the codebase and compare viable designs before choosing the smallest design that preserves the package boundaries + +## Comments + +- Do not restate code in comments. Comment only an invariant, compatibility constraint, non-obvious tradeoff, or external reason that the code cannot express +- Prefix a temporary or surprising workaround with `// HACK:` and state why it exists + +## Public surface + +Before changing a command, flag, score, config, JSON report, package API, GitHub Action, website, or terminal output, run the [product-thinking skill](../skills/product-thinking/SKILL.md). Lint rules use the rule pipeline instead. + +## Symbol search and deduplication + +`@rayhanadev/truffler` is a dev dependency that fuzzy-searches JavaScript and TypeScript symbols through `oxc-parser`. Use it to avoid duplicate code. The [find-similar-functions skill](../skills/find-similar-functions/SKILL.md) defines the full workflow. + +Before adding a utility, helper, type, constant, or rule: + +- Search for an existing symbol to reuse or extend +- Derive queries from the proposed name, domain noun, and verb +- Search the narrowest root first, then read the top matches + +After finishing a task, search for each added symbol. Delete code the change superseded. + +```bash +bunx @rayhanadev/truffler "" packages --kind function,method,interface,type,constant --limit 20 +``` + +The repository pins `@rayhanadev/truffler`. Bun runs its TypeScript entry directly. Start with a narrow root such as `packages/core/src`; broaden only if the first search finds nothing. diff --git a/.agents/references/effect-v4.md b/.agents/references/effect-v4.md new file mode 100644 index 0000000000..51f90f8ec0 --- /dev/null +++ b/.agents/references/effect-v4.md @@ -0,0 +1,74 @@ +# Effect v4 conventions + +Use this reference whenever code imports Effect. Root [AGENTS.md](../../AGENTS.md) makes these conventions binding. + +The codebase uses `effect@4.0.0-beta.70`. The conventions below are binding. Optional local checkouts of Effect and `react-doctor-evals` can provide additional examples, but they are not required. + +## Imports + +- ALWAYS: use namespace imports such as `import * as Schema from "effect/Schema"` and `import * as Effect from "effect/Effect"`. Use one Effect module per import line +- NEVER: `import { Schema, Effect } from "effect"`. The umbrella import inflates the type-resolution graph and contradicts the established project convention. + +## Errors + +- Every fallible service uses `ReactDoctorError` as its typed failure channel +- Each reason is a `Schema.TaggedErrorClass()("Tag", { fields })` with a `get message()` getter that returns human-readable text +- Opaque causes use `Cause.pretty(Cause.fail(this.cause))` in the message body +- Renderers dispatch on `error.reason._tag`, NEVER on `error.message.includes(...)` +- `formatReactDoctorError`, `isReactDoctorError`, `isSplittableReactDoctorError`, and `restoreLegacyThrow` live in `packages/core/src/errors.ts`. Reuse them instead of adding another error-shape helper + +## Error dispatch and recovery + +- **`Effect.catchReasons(errorTag, cases, orElse?)`**: dispatch on a `Schema.TaggedErrorClass` reason union. Each entry catches one reason `_tag`; optional `orElse` handles unmatched reasons. NEVER write manual reason ladders inside a catch block. See `packages/core/src/errors.ts` and `packages/api/src/diagnose.ts` +- **`Effect.catchTag(tag, handler)`**: recover one tagged error, such as `Effect.catchTag("PlatformError", ...)` +- **`Effect.catch`**: use for catch-all recovery; it replaced v3 `Effect.catchAll` +- **`Effect.die(error)`**: promote a recovered value into a defect that `runPromise` re-throws unchanged. Use it in `catchReasons` handlers where the programmatic API still requires the legacy `Error` class +- NEVER use `try/catch` inside `Effect.gen`. Wrap synchronous throws in `Effect.try({ try, catch })` and recover with `Effect.orElseSucceed` or `Effect.catch`. See `packages/react-doctor/src/cli/utils/render-summary.ts` + +## Generator hygiene + +- **`return yield* Effect.fail(...)`**: return terminal effects such as `Effect.fail`, `Effect.interrupt`, and `Effect.die` so TypeScript sees unreachable code +- **`Effect.gen({ self: this }, function* () { ... })`**: use the options object for a class-method generator bound to `this`. Plain `Effect.gen(function* () { ... })` remains valid +- **`Effect.fnUntraced(function* () { ... })`**: prefer it to a function whose body is `Effect.gen` only on a measured hot path + +## Services + +- `Context.Service()("react-doctor/Name", { make: ... })`: use the `react-doctor/X` prefix in the identifier +- Service method bodies use `Effect.fnUntraced` for hot paths and `Effect.sync` for one-liners. Test layers and orchestration use `Effect.gen` +- **`Effect.fn("Service.method")`**: name non-trivial service methods so tracing can identify them. See `packages/core/src/services/project.ts` +- Use `Service.of({ ... })` inside `Layer.succeed` and service constructors. Do not replace it with an assertion +- Use `Layer.effect` when a service has initialization work; use `Layer.succeed` when it is stateless +- Methods with more than one parameter take one object argument, such as `Files.readLines({ filePath, rootDirectory })` + +## Layer naming + +- `layerNode` for the production Node.js implementation +- `layerOf(value)` for a test layer that returns a pre-supplied value +- `layerInMemory(Map)` for filesystem-shaped services backed by an in-memory tree +- `layerCapture` for a test layer that records calls into a `Ref` exposed through a sibling `*Capture` service, such as `ReporterCapture` or `ProgressCapture` +- `layerNoop` for a production layer with void-return/discard semantics, such as Reporter or Progress. Analyzers such as Linter and DeadCode use `layerOf([])` instead +- `layerComposite(backends)` for the slot where a future second backend plugs in +- Implementation-specific names: `layerOxlint`, `layerHttp`, `layerNdjson(path)`, `layerOra(factory)` + +## Schemas + +- Use `Schema.Class("Name")({ fields })` for wire records +- Use `Schema.Literals(["a", "b"])` for literal unions and `Schema.Literal(1)` for one literal +- Use `Schema.NullOr(X)` for `X | null` and `Schema.optional(X)` for `X?` +- Use `Schema.brand("X")` through `.pipe()` for branded primitives +- Use schemas for wire types such as Diagnostic and JsonReport. Use interfaces for argument types such as InspectInput and LintInput to avoid hot-path runtime encode/decode cost + +## Ambient configuration + +- Route environment-variable reads and cache paths through `Context.Reference("react-doctor/X", { defaultValue })`. See `packages/core/src/refs.ts`; tests override references with `Layer.succeed` +- Prefer `Config.redacted("ENV_NAME")` to `Context.Reference` for secrets such as API tokens and signing keys. Group several values with `Config.all({ ... })` at the service constructor. See `packages/core/src/observability.ts` + +## Observability handoff + +Use [observability](observability.md) for OTLP, Sentry, metrics, telemetry privacy, action attributes, and run IDs. It owns the complete operational policy. This reference owns the Effect APIs that instrument those paths. + +## Console and logging + +- ALWAYS import `* as Console` from `effect/Console` and use its effects in renderers, services, and Effect-typed code. Effect's `Console` is a `Context.Reference`, so tests and silent mode can replace it +- NEVER invent a parallel logger abstraction. `packages/react-doctor/src/cli/utils/cli-logger.ts` is the remaining synchronous bridge for imperative CLI helpers outside `Effect.gen` +- Silent mode uses `Effect.provideService(Console.Console, silentConsole)` in the renderer pipeline or `installSilentConsole()` in JSON mode. Both routes preserve `Console.*`; do not add `if (silent) return` checks at call sites diff --git a/.agents/references/observability.md b/.agents/references/observability.md new file mode 100644 index 0000000000..17ba24abeb --- /dev/null +++ b/.agents/references/observability.md @@ -0,0 +1,72 @@ +# Observability and telemetry + +Use this reference before changing OTLP, Sentry, metrics, telemetry fields, privacy controls, or CLI run instrumentation. Root [AGENTS.md](../../AGENTS.md) makes these instructions binding. + +## Effect tracing and OTLP + +- Wrap the top-level entry of a multi-step operation in `Effect.withSpan("name", { attributes })`. See `packages/core/src/run-inspect.ts`. Attribute keys use dotted namespacing such as `inspect.directory` +- Per-service-method spans come from `Effect.fn("Service.method")`. The two compose: `runInspect` is the parent span, every `Service.method` is a child. +- `layerOtlp` in `packages/core/src/observability.ts` is wired into `inspect()` and `diagnose()`. It is a no-op unless both `REACT_DOCTOR_OTLP_ENDPOINT` and `REACT_DOCTOR_OTLP_AUTH_HEADER` are set. When enabled, it uses `Otlp.layerJson` with `FetchHttpClient.layer` + +## Sentry tracer selection + +Sentry tracing is CLI-only. `packages/react-doctor/src/cli/utils/apply-observability.ts` chooses the tracer backend because Effect has one `Tracer` reference. User OTLP wins and shares a `trace_id` with the Sentry root through `Tracer.externalSpan`. Otherwise, `makeSentryTracer` in `packages/react-doctor/src/cli/utils/sentry-tracer.ts` records Effect spans under the transaction from `packages/react-doctor/src/cli/utils/with-sentry-run-span.ts`. Use the native no-op tracer when neither backend is active. + +`isSentryTracingEnabled()` gates this path, so it remains inert for `@react-doctor/api`, `--no-score`, tests, and `SENTRY_TRACES_SAMPLE_RATE=0`. `scripts/sentry-sourcemaps.mjs` uploads Debug ID source maps. Its `react-doctor@version` release must match the SDK release. + +## Sentry scope ownership + +`packages/react-doctor/src/cli/utils/build-sentry-scope.ts` projects the run snapshot and scanned project into Sentry tags and contexts. `packages/react-doctor/src/instrument.ts` and `packages/react-doctor/src/cli/utils/report-error.ts` consume it. Add new shared metadata there, not at call sites. + +The `beforeLint` hook captures project info through `recordSentryProjectContext` in `packages/react-doctor/src/cli/utils/with-sentry-run-span.ts`. It stores that information for the lazy error path and sets it as root-span attributes. + +## Anonymization and fail-closed behavior + +Telemetry must stay anonymized. `Sentry.init` sets `sendDefaultPii: false`. `beforeSend` and `beforeSendTransaction` both run `scrubSentryEvent` in `packages/react-doctor/src/cli/utils/scrub-sentry-event.ts`: + +- Strip hostname, `server_name`, device name, and the IP-bearing `user` +- Drop captured stack-frame local variables +- Run every remaining string through `packages/react-doctor/src/cli/utils/anonymize-text.ts`, which composes `scrubSensitivePaths` and `redactSensitiveText` + +`buildRunContext` also scrubs `cwd` and `argv` at the source. Before adding a field to a Sentry event, confirm that it contains no username, hostname, IP, secret, or absolute path. Prefer adding it through `buildSentryScope` so the central scrub covers it. `scrubSentryEvent` returns `null` on any failure so an un-anonymized event is never sent. + +## Crash references and trace linkage + +`reportErrorToSentry` returns the Sentry event ID. CLI catch blocks pass it to `handleError`, which prints a reference and adds it to the prefilled GitHub issue. + +Errors thrown during a scan link to the run transaction through the scope's propagation context. `withSentryRunSpan` records the trace in `packages/react-doctor/src/cli/utils/active-run-trace.ts` and clears it only after success. `reportErrorToSentry` reattaches it with `scope.setPropagationContext`. + +## Sentry metrics + +Sentry metrics are CLI-only. Emit anonymized counters and distributions through `packages/react-doctor/src/cli/utils/record-metric.ts`. Each operation stays inert unless `Sentry.isInitialized()`. Metrics remain independent of `tracesSampleRate`. + +Metric names live in the `METRIC` map in `packages/react-doctor/src/cli/utils/constants.ts`. Use dotted, domain-grouped names. Put high-cardinality dimensions in attributes, never the name. `withRunAttributes` rebuilds `buildSentryScope().tags` for each emission so metrics use current run and project state. + +Emit sites pass only metric-specific attributes. Project shape comes from `recordSentryProjectContext` through `getSentryProjectInfo()`. Per-scan metrics live in `packages/react-doctor/src/cli/utils/record-scan-metrics.ts`. Keep `rule.fired` as one counter keyed by `rule`, `plugin`, `category`, and `severity` attributes. Never create a metric name per rule. + +`Sentry.init` sets `beforeSendMetric: scrubSentryMetric` in `packages/react-doctor/src/cli/utils/scrub-sentry-metric.ts`. It removes `server.address` and scrubs paths and secrets through `packages/react-doctor/src/cli/utils/anonymize-text.ts`. It returns `null` on failure. Add counters through `record-metric.ts` and the `METRIC` map, and confirm every new attribute carries no username, path, or secret. + +## Canonical run wide event + +The richest telemetry is one high-dimensionality wide event per scan, not a collection of narrow counters. `recordRunEvent` and `buildRunEventAttributes` live in `packages/react-doctor/src/cli/utils/build-run-event.ts`. + +`packages/react-doctor/src/cli/utils/render-inspect-result.ts` records a successful scan after `recordScanMetrics`. `packages/react-doctor/src/inspect.ts` records failures at the outer span boundary and rethrows the original error. Both paths preserve the `outcome.status`, `outcome.exitCode`, and `outcome.errorTag` fields. + +The root span already contains run tags and project shape. The wide event adds only the remaining fields. Namespace every attribute through `withNamespace` in `packages/react-doctor/src/cli/utils/with-namespace.ts`: + +- Scan config: `scan.mode`, `scan.parallel`, `scan.workerCount`, `scan.rulesConfigured`, `scan.rulesDisabled`, `scan.ignoredTagCount`, `scan.hasCustomConfig`, and `scan.fileCount` +- Verdict: `outcome.wouldBlock`, `outcome.blocking`, `outcome.clean`, and `outcome.skippedChecks` +- Findings: `diag.total`, `diag.errors`, `diag.warnings`, `diag.affectedFiles`, `diag.distinctRules`, `diag.topRule`, and `diag.category.*` +- Score: `score.value`, `score.label`, and `score.available` +- Pass outcomes and timing: `lint.*`, `deadCode.*`, `supplyChain.*`, and `timing.*` +- CI and pull request details: `action.actorAssociation`, `action.runnerOs`, `action.comment`, `action.reviewComments`, and `action.versionPin` + +Typing matters for querying. Numeric outcomes are numbers so Sentry can calculate expressions such as `p75(score.value)`. Dimensions are strings or booleans so Sentry can filter and group them. `toSpanAttributes` drops `null` so absent signals never become the string `"null"`. + +Query the event in Sentry Trace Explorer on the Spans dataset. Add run-level dimensions through `packages/react-doctor/src/cli/utils/build-run-context.ts` and `packages/react-doctor/src/cli/utils/build-sentry-scope.ts`. Add per-scan outcomes to the wide event through `withNamespace`, not new counters. Keep `scan.completed`, `scan.duration`, `rule.fired`, `cli.invoked`, and `cli.error` as the trace-sampling-independent counters. + +Score reachability is derivable: `!score.available && !lint.failed && !deadCode.failed && !scan.noScore`. Failed passes deliberately null the score. Score latency is the `Score.compute` child span's duration, so neither needs a dedicated field. CI detection and Action inputs live in `packages/react-doctor/src/cli/utils/is-ci-environment.ts`. `action.yml` sets the `REACT_DOCTOR_GITHUB_ACTION` marker and `REACT_DOCTOR_ACTION_*` variables. Keep all attributes free of username, path, secret, repository identity, and owner identity. + +## Run ID + +`packages/react-doctor/src/cli/utils/run-id.ts` creates one random `runId` per CLI process. It belongs in the Sentry `run` context and wide event, but NEVER in a tag or metric attribute. A workspace invocation shares one `runId` across projects. Do not add a plaintext or hashed repository ID to Sentry. diff --git a/.agents/references/release-safety.md b/.agents/references/release-safety.md new file mode 100644 index 0000000000..9c19e2df3d --- /dev/null +++ b/.agents/references/release-safety.md @@ -0,0 +1,43 @@ +# Release safety and GitHub Action versioning + +This reference governs all release work. Root [AGENTS.md](../../AGENTS.md) makes every rule here binding. + +## Release authorization + +- MUST: Discourage minor and major Changesets. Do not add one unless the user explicitly requests that release level. Patch Changesets may be added without a separate request when appropriate. +- MUST: Never merge a Changesets release PR, including any `changeset-release/*` branch, without fresh, explicit user confirmation for that exact PR and version immediately before the merge. +- General instructions to merge, ship, land, or babysit green PRs do not authorize merging a release/version PR. Treat merging a PR that triggers publication as publishing the release. +- MUST: Never publish packages, push or move release tags, or trigger, approve, rerun, or merge a release/publish workflow without fresh, explicit user confirmation for the exact versions and packages involved. +- You may prepare, validate, and babysit a release candidate, but must stop before the first publishing action. Report the exact PR, versions, packages, tags, and workflows awaiting approval. +- These confirmation requirements also apply to GitHub Action releases described below. Once the user explicitly approves a specific release, follow all required versioning and tag steps. + +## GitHub Action versioning + +The composite GitHub Action is **versioned independently from the npm packages**. "The action" is `action.yml` in the repository root and these scripts it shells out to: + +- `scripts/ensure-json-report.mjs` +- `scripts/normalize-changed-files.mjs` +- `scripts/render-github-action-comment.mjs` +- `scripts/resolve-package-spec.mjs` + +Treat a change to any listed file as an action release. Keep the list in sync with `ACTION_RELEASE_FILES` in `scripts/recommend-action-version-bump.mjs`, the release guard. + +Two tag namespaces coexist. Never conflate them: + +- npm packages: `react-doctor@X.Y.Z`, `eslint-plugin-react-doctor@X.Y.Z`, and `oxlint-plugin-react-doctor@X.Y.Z`, created by Changesets in CI through `.github/workflows/publish.yml` +- GitHub Action: `v`-prefixed semver `vX.Y.Z` plus a floating major `vN`. Check `git tag --list 'v*'` before choosing a version + +- MUST: prepare an Action version for every commit that touches the action files. Use a minor bump for `feat(action)`, a major bump for a breaking input, output, or runtime change, and a patch bump otherwise +- MUST: after the user authorizes the exact release and you create `vX.Y.Z`, move the floating `vN` tag to the same commit +- Tags are GPG-signed annotated tags (`tag.gpgsign=true`), so a bare `git tag vX` will demand a message and fail in scripts. Always create/move with an explicit message: + +```bash +# new release at the commit that changed the action +git tag -a vX.Y.Z commit_sha -m "react-doctor action vX.Y.Z" +# move the floating major (force-update only the vN pointer) +git tag -fa vN commit_sha -m "react-doctor action vN (floating major -> vX.Y.Z)" +git push origin vX.Y.Z +git push --force origin vN +``` + +- MUST: never tell consumers to reference `@main` in documentation or examples. Recommend a full commit SHA pin with a trailing version comment for hardened CI, or `@vN` for convenience diff --git a/.agents/references/repository-architecture.md b/.agents/references/repository-architecture.md new file mode 100644 index 0000000000..0d9f937333 --- /dev/null +++ b/.agents/references/repository-architecture.md @@ -0,0 +1,44 @@ +# Repository architecture + +Use this reference when choosing package ownership or an import direction. Root [AGENTS.md](../../AGENTS.md) makes this layout binding. + +## Package ownership + +- `packages/core`: private diagnostic engine, project discovery, scan policy, services, backend runners, diagnostic processing, and scoring +- `packages/api`: private programmatic `diagnose()` shell around the core engine +- `packages/react-doctor`: published CLI, public `inspect()`, terminal rendering, and runtime adapters +- `packages/oxlint-plugin-react-doctor`: published rule engine and canonical rule implementation +- `packages/eslint-plugin-react-doctor`: published ESLint mirror +- `packages/deslop-js`: published dead-code and redundancy analysis library +- `packages/deslop-cli`: published CLI for `deslop-js` +- `packages/evals`: private Daytona evaluation harness +- `packages/fuzz`: private adversarial rule fuzzing harness +- `packages/language-server`: private editor language server bundled into the CLI +- `packages/vscode-react-doctor`: private Visual Studio Code extension +- `packages/zed-react-doctor`: unpublished Zed extension + +## Core layers + +Keep dependencies pointing down this list: + +1. Foundation types in `packages/core/src/types/`, `packages/core/src/schemas.ts`, and `packages/core/src/errors.ts` +2. Project discovery and the normalized package graph in `packages/core/src/project-info/` +3. Domain logic and leaf utilities +4. Service interfaces and implementations in `packages/core/src/services/` +5. Backend implementations in `packages/core/src/runners/` +6. Scan orchestration in `packages/core/src/run-inspect.ts` +7. API, CLI, language-server, and editor adapters + +Foundation types, schemas, and errors must not import services, runners, orchestration, telemetry, or CLI code. Project discovery must remain below runtime services and orchestration. Leaf utilities must not depend on those runtime layers. + +The package graph owns workspace package boundaries, dependency declarations, catalog and workspace resolution, and package-local capability queries. Keep legacy `ProjectInfo` as a compatibility projection rather than a second discovery model. + +The `Linter` service owns the backend boundary. Keep Oxlint process management behind its layer so orchestration and post-processing do not depend on a specific backend. + +## Import boundaries + +`@react-doctor/core` remains a compatibility facade. New code inside `packages/react-doctor` must import cohesive capabilities through `packages/react-doctor/src/core/` adapters. Only those adapters may import the broad core entry point. + +Prefer direct, owned modules inside a package. Do not add a barrel that exposes unrelated internals or hides a backward dependency. + +The oxlint plugin owns rule code and its canonical dependency-name data. Core must not import the rule package at runtime or re-export rule internals. diff --git a/.agents/references/testing.md b/.agents/references/testing.md new file mode 100644 index 0000000000..8e911b3416 --- /dev/null +++ b/.agents/references/testing.md @@ -0,0 +1,43 @@ +# Testing and validation + +Use this reference to select tests and validate a change. Root [AGENTS.md](../../AGENTS.md) requires these checks before a commit. + +Tests live beside source in package `tests/` directories or next to the implementation when the package already uses colocated tests. + +- `packages/core/tests/`: engine, service, discovery, and orchestration tests +- `packages/api/tests/`: API shell and boundary tests +- `packages/react-doctor/tests/`: CLI, rendering, cache, compatibility, and end-to-end tests +- `packages/oxlint-plugin-react-doctor/src/`: rule, semantic-engine, and evaluator tests + +The test framework is `vite-plus/test`, the existing Vitest wrapper. + +Run the narrowest relevant test while iterating. Before committing, run: + +```bash +nr test +nr lint +nr typecheck +nr format:check +nr smoke:json-report +``` + +Run each additional check owned by the changed surface: + +```bash +nr architecture:check +nr test:architecture +nr compatibility:check +nr test:compatibility +nr test:build-policy +nr skills:check +nr build +nr check:published-deps +nr smoke:packed-cli-install +``` + +- Run `nr test:deslop` after changing `deslop-js` or `deslop-cli` +- Run `nr --filter oxlint-plugin-react-doctor gen:check` after changing rule registration or generated inputs +- Run `nr skills:check` after changing `AGENTS.md`, `.agents/references/`, `.agents/skills/`, or `skills/` +- Run packed compatibility checks after changing a published package's files, exports, binary, or bundled assets + +Do not treat a focused test as proof of repository-wide compatibility. Match validation depth to the affected boundary, then run the pre-commit matrix. diff --git a/.agents/skills/product-thinking/SKILL.md b/.agents/skills/product-thinking/SKILL.md index 8563d6ad86..3f62fcfe6c 100644 --- a/.agents/skills/product-thinking/SKILL.md +++ b/.agents/skills/product-thinking/SKILL.md @@ -22,7 +22,7 @@ Run the pass whenever a diff touches a surface a user — a developer running Re | GitHub Action input/output | `action.yml` | Versioned independently (`vN`); workflows in other repos break when an input or output changes. | | Terminal output / UX | `cli/utils/` renderers | The first impression and the daily experience; noise or confusion here is what makes people stop running it. | -**Not here:** lint rules go through the `rule-research` → `rule-writing` → `rule-validate` pipeline, and rule configuration through `doctor-explain` — this pass is for the surface _around_ the rules, not the rules themselves. Internal-only changes (the engine, private `core` types, tests, tooling) skip the pass entirely; note in one line why the change is internal and move on. +**Not here:** lint rules go through the `rule-research` → `rule-writing` → `rule-validate` pipeline, and rule configuration through the `react-doctor` skill's `references/explain.md` guide — this pass is for the surface _around_ the rules, not the rules themselves. Internal-only changes (the engine, private `core` types, tests, tooling) skip the pass entirely; note in one line why the change is internal and move on. ## Steps @@ -87,7 +87,7 @@ A surface nobody can discover is wasted, and a stale doc is a trust bug. Documen - The `--help` / usage text next to the new flag or command, so it's discoverable from the CLI itself. - The website page and the canonical prompt under `react.doctor/prompts/...`, which is what agents fetch at runtime. -- The distributed skills (`skills/react-doctor`, `skills/doctor-explain`) when the change alters the user-facing workflow. +- The distributed `skills/react-doctor` skill and its `references/explain.md` guide when the change alters the user-facing workflow. ### 6. Record the kill metric diff --git a/.agents/skills/react-doctor/references/explain.md b/.agents/skills/react-doctor/references/explain.md index 722c6f6424..d1cc4b20a2 100644 --- a/.agents/skills/react-doctor/references/explain.md +++ b/.agents/skills/react-doctor/references/explain.md @@ -51,6 +51,7 @@ Match the control to the intent — prefer the narrowest one: - **A whole area is unwanted** (e.g. all React Native rules) → `rules category "" off`. - **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag `. - **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output. +- **Restore test or story findings to production health** → set `surfaces.score.includeFileContexts` or `surfaces.ciFailure.includeFileContexts` to `["test"]`, `["story"]`, or both. Other surface exclusions still apply. How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs. diff --git a/.changeset/sync-react-doctor-skill.md b/.changeset/sync-react-doctor-skill.md new file mode 100644 index 0000000000..451d63a56d --- /dev/null +++ b/.changeset/sync-react-doctor-skill.md @@ -0,0 +1,5 @@ +--- +"react-doctor": patch +--- + +Keep installed React Doctor agent guidance synchronized with the current `--scope changed` workflow. diff --git a/.gitignore b/.gitignore index c62750c0a4..5aa83cb871 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ review-*.md # Track repository-owned agent skills, but keep other local agent state out. /.agents/* +!/.agents/references/ +!/.agents/references/** !/.agents/skills/ /.agents/skills/* !/.agents/skills/react-doctor/ diff --git a/AGENTS.md b/AGENTS.md index 13a9afbb00..855e0d0516 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,419 +1,27 @@ -## General Rules +# React Doctor agent guide -- MUST: Use @antfu/ni. Use `ni` to install, `nr SCRIPT_NAME` to run. `nun` to uninstall. -- MUST: Use TypeScript interfaces over types. -- MUST: Keep all types in the global scope. -- MUST: Use arrow functions over function declarations -- MUST: Never comment unless absolutely necessary. - - If the code is a hack (like a setTimeout or potentially confusing code), it must be prefixed with // HACK: reason for hack -- MUST: Use kebab-case for files -- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names). - - Example: for .map(), you can use `innerX` instead of `x` - - Example: instead of `moved` use `didPositionChange` -- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive. -- MUST: Do not type cast ("as") unless absolutely necessary -- MUST: Remove unused code and don't repeat yourself. -- MUST: Use `truffler` to find existing symbols before adding a utility, helper, type, or rule, and again after finishing a task to catch duplicates and dead code (see "Symbol Search & Deduplication"). -- MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution. -- MUST: Before adding or changing the **public surface** (CLI flags/commands, the score, config, the JSON report, package APIs, the GitHub Action, website, or terminal output), run the `product-thinking` pass (`.agents/skills/product-thinking/`): name the user's job, reuse before adding, wire one telemetry metric, add the compatibility artifacts, and set a kill metric. Lint rules use the rule pipeline instead. -- MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`). -- MUST: Put small, focused utility functions in `utils/` with one utility per file. -- MUST: Use Boolean over !!. +This file is the durable entry point for repository work. Read each reference that owns the area you change. Every `MUST`, `ALWAYS`, `NEVER`, and release-authorization rule in those references is binding. -## Symbol Search & Deduplication (truffler) +## Start here -`@rayhanadev/truffler` (dev dependency) is fuzzy JS/TS symbol search powered by `oxc-parser`. -Use it to avoid duplicating existing code. The `find-similar-functions` skill -(`.agents/skills/find-similar-functions/`) carries the full workflow; the short version: +- **Any code change**: read [code conventions](.agents/references/coding-conventions.md) for commands, types, naming, comments, ownership, and mandatory `truffler` checks +- **Package ownership or imports**: read [repository architecture](.agents/references/repository-architecture.md) +- **Effect code, services, errors, or logging**: read [Effect v4 conventions](.agents/references/effect-v4.md) +- **Telemetry, OTLP, Sentry, metrics, or privacy**: read [observability](.agents/references/observability.md) +- **Tests or a commit**: read [testing and validation](.agents/references/testing.md) +- **Changesets, package publication, tags, GitHub Action files, or release workflows**: read [release safety and GitHub Action versioning](.agents/references/release-safety.md) before any action -- WHEN PLANNING / SCOPING — before adding a utility, helper, type, constant, or rule, search - for an existing symbol to reuse or extend. Derive a few queries from the behavior (proposed - name + domain noun + verb), search the narrowest root first, then read the top matches before - writing anything. It is how you reuse an existing helper instead of duplicating it, per "don't - repeat yourself" and the one-utility-per-file `utils/` convention. -- AFTER FINISHING A TASK — re-run searches for the symbols you added to confirm you did not - duplicate an existing helper, and delete any code your change superseded. +## Non-negotiable gates -```bash -bunx @rayhanadev/truffler "" packages --kind function,method,interface,type,constant --limit 20 -``` +- Use `ni` to install, `nr SCRIPT_NAME` to run a declared script, and `nun` to uninstall. Use `nr --filter workspace_name script_name` for a workspace script +- Search with `truffler` before adding a utility, helper, type, constant, or rule, and again after the task to remove duplicates or dead code +- Before a public-surface change, run the [product-thinking skill](.agents/skills/product-thinking/SKILL.md). Public surface includes CLI flags and commands, score, config, JSON report, package APIs, GitHub Action, website, and terminal output. Lint rules follow the rule pipeline +- Before a commit, run the checks in [testing and validation](.agents/references/testing.md) +- Never publish, tag, move a release tag, or merge or operate a release workflow without the fresh, exact user confirmation required by [release safety and GitHub Action versioning](.agents/references/release-safety.md) -Run it with `bunx @rayhanadev/truffler` (the published `bin` is a TypeScript entry Bun runs -directly, and the pinned dev dependency is reused rather than re-downloaded). Narrow `` -and the root (e.g. `packages/core/src`) for precision; broaden only when nothing matches. +## Optional external references -## Package Layout +- An Effect checkout can provide additional examples through its `.patterns/effect.md` +- A `react-doctor-evals` checkout can provide additional runtime examples -``` -packages/ - core/ PRIVATE the diagnostic engine - src/ - types/ PRIVATE shared cross-package TS types (DiagnoseOptions, - ProjectInfo, JsonReport, …) — no runtime code - project-info/ project discovery (discoverProject, findMonorepoRoot, - framework detection, narrow Error subclasses thrown - BEFORE the Effect runtime takes over) - errors.ts tagged Schema.TaggedErrorClass leaves + ReactDoctorError union - schemas.ts Diagnostic / Severity / JsonReport / buildDiagnosticIdentity - (also exposed as `@react-doctor/core/schemas` subpath - since the names overlap the TS types above) - refs.ts Context.Reference for ambient env config - run-inspect.ts streaming orchestrator (the heart) - build-diagnostic-pipeline per-element filter pipeline (single source of truth) - services/ 10 Context.Service classes (Files, Git, Project, - Config, Linter, DeadCode, Score, Reporter, Progress, - NodeResolver, StagedFiles) + LintPartialFailures - ... rest of the lint / score / suppression engine - api/ PRIVATE programmatic diagnose() (Effect.runPromise shell) - react-doctor/ PUBLISHED CLI + public inspect() + bin - oxlint-plugin-react-doctor/ PUBLISHED the 100+ rules, owns the canonical - `react-native-dependency-names.ts` (re-exported from - core to break the rule-package ↔ core cycle) - eslint-plugin-react-doctor/ PUBLISHED ESLint mirror of the oxlint plugin -``` - -## Effect v4 Conventions - -Built on `effect@4.0.0-beta.70`. See `tmp/effect/.patterns/effect.md` (cloned reference) -and `~/Developer/react-doctor-evals/src/` (the application that pioneered these patterns -for this codebase) for canonical examples. - -### Imports - -- ALWAYS: `import * as Schema from "effect/Schema"`, `import * as Effect from "effect/Effect"`, - `import * as Cause from "effect/Cause"`, etc. — one module per import line. -- NEVER: `import { Schema, Effect } from "effect"` — the umbrella import inflates the - type-resolution graph and contradicts what every other Effect codebase does. - -### Errors - -- Every fallible service fails with `ReactDoctorError` (`reason: Schema.Union([...])`) -- Each leaf is a `Schema.TaggedErrorClass()("Tag", { fields })` with a - `get message()` getter (NOT `message =`) returning a human string. -- Opaque causes use `Cause.pretty(Cause.fail(this.cause))` in the message body. -- Renderers dispatch on `error.reason._tag`, NEVER on `error.message.includes(...)`. -- `formatReactDoctorError(error)` / `isReactDoctorError(error)` / `isSplittableReactDoctorError(error)` - live in `core/src/errors.ts`. Use them; don't add new error-shape helpers. - -### Error dispatch / recovery — v4 idioms - -- **`Effect.catchReasons(errorTag, cases, orElse?)`** — the v4-canonical way to - dispatch on a `Schema.TaggedErrorClass` reason union. Each entry catches one - reason `_tag`; the optional `orElse` handles unmatched reasons. NEVER write - manual `if (cause.reason instanceof X)` ladders inside a `catch` block — the - Effect pipeline gives you exhaustive, type-safe narrowing for free. See - `inspect.ts → restoreLegacyThrow` and `api/diagnose.ts` for the canonical - shape. -- **`Effect.catchTag(tag, handler)`** — for a single tagged error (e.g. - `Effect.catchTag("PlatformError", ...)` in `services/git.ts` to fold the - `ChildProcess` platform error into a `ReactDoctorError`). -- **`Effect.catch`** (renamed from v3 `Effect.catchAll`) — for catch-all. -- **`Effect.die(error)`** — promote a recovered value into a defect that - `runPromise` re-throws unchanged. Used in `catchReasons` handlers when the - programmatic contract still wants the legacy `Error` class on the throw. -- **NEVER** `try/catch` inside `Effect.gen` (v4 hard rule). Wrap the sync - throw in `Effect.try({ try, catch })` and recover via - `Effect.orElseSucceed` / `Effect.catch` instead. See - `render-summary.ts → printSummary` for the canonical shape. - -### Generator hygiene - -- **`return yield* Effect.fail(...)`** — terminal effects (Effect.fail, - Effect.interrupt, Effect.die) must be `return yield*` so TypeScript sees - the unreachable-code property. Bare `yield*` of a terminal lets unreachable - code accumulate after it. See `services/git.ts` `diffSelection` for examples. -- **`Effect.gen({ self: this }, function* () { ... })`** — v4 changed the - `self`-bound form. The plain `Effect.gen(function* () { ... })` form is - unchanged; only class-method generators bound to `this` need the options - object. -- **`Effect.fnUntraced(function* () { ... })`** — prefer over a function - whose body is `Effect.gen` when the function is called many times per - operation (hot path). Cuts tracing overhead. Not currently used in this - codebase — Git invocations and inspect-pipeline calls run once per scan, - not in a hot loop. - -### Services - -- `Context.Service()("react-doctor/Name", { make: ... })` — short - prefix in the identifier (matches react-doctor-evals' `rde/X` shape). -- Service method bodies use `Effect.fnUntraced` for hot paths, `Effect.sync` for - one-liners. Test layers + orchestration use `Effect.gen`. -- **`Effect.fn("Service.method")`** for non-trivial methods so they surface as - named spans in OTel traces. Production cost is zero when no tracer layer is - provided; with `Otlp.layerJson(...)` users see one span per service call. - Canonical eval pattern (`react-doctor-evals/src/Runner.ts` → every method). -- `Service.of({ ... })` everywhere inside `Layer.succeed` / `make:` — never - `{ ... } as const`. -- `Layer.effect` when the service has init work (e.g. `Cache.make`); `Layer.succeed` - when stateless. -- Method takes a single object arg when there are >1 parameters - (e.g. `Files.readLines({ filePath, rootDirectory })`). - -### Layer naming - -- `layerNode` for the production Node.js implementation. -- `layerOf(value)` for the test layer that returns a pre-supplied value. -- `layerInMemory(Map)` for filesystem-shaped services backed by an in-memory tree. -- `layerCapture` for the test layer that records calls into a `Ref` exposed via a - sibling `*Capture` service (e.g. `ReporterCapture`, `ProgressCapture`). -- `layerNoop` for the production layer that has void-return / discard semantics - (Reporter, Progress). Analyzers (Linter, DeadCode) use `layerOf([])` instead. -- `layerComposite(backends)` for the slot a future second backend plugs into. -- Implementation-specific names: `layerOxlint`, `layerHttp`, `layerNdjson(path)`, - `layerOra(factory)`. - -### Schemas - -- Use `Schema.Class("Name")({ fields })` for wire records. -- Use `Schema.Literals(["a", "b"])` for unions of literals (plural), `Schema.Literal(1)` - for single literals. -- `Schema.NullOr(X)` for `X | null`; `Schema.optional(X)` for `X?`. -- `Schema.brand("X")` via `.pipe()` for branded primitives. -- Schema for wire types (Diagnostic, JsonReport); interfaces for arg types - (InspectInput, LintInput) — avoid runtime encode/decode cost on hot paths. - -### Ambient config - -- Env-var reads + cache paths go through `Context.Reference("react-doctor/X", { defaultValue })`. - See `core/src/refs.ts`. Tests override via `Layer.succeed(MyRef, ...)`. -- Secrets (API tokens, signing keys) should prefer `Config.redacted("ENV_NAME")` over - `Context.Reference` so they auto-redact in logs / traces. Group with `Config.all({ ... })` - at the service constructor when you need several. (Pattern from - `react-doctor-evals/src/GitHub.ts` — not yet used in this codebase; document - the convention so the first secret-shaped config does it right.) - -### Observability - -- Wrap the top-level entry of a multi-step operation in `Effect.withSpan("name", { attributes })`. - See `core/src/run-inspect.ts → runInspect` for the canonical shape. Attribute - keys use dotted namespacing (`inspect.directory`, `inspect.isCi`). -- Per-service-method spans come from `Effect.fn("Service.method")` — see Services section - above. The two compose: `runInspect` is the parent span, every `Service.method` is a child. -- Production observability layer is `layerOtlp` in `core/src/observability.ts` - (wired into both `inspect()` and `diagnose()`). It's a no-op unless the user - sets BOTH `REACT_DOCTOR_OTLP_ENDPOINT` (e.g. `https://api.axiom.co`) and - `REACT_DOCTOR_OTLP_AUTH_HEADER` (e.g. `Bearer `) in the environment. - When both are set, it provides `Otlp.layerJson({...})` from - `effect/unstable/observability/Otlp` with `NodeHttpClient.layerUndici` as the - transport, so every `Effect.fn("Service.method")` span and every top-level - `Effect.withSpan("...")` ships to the configured backend. Eval reference: - `react-doctor-evals/src/Observability.ts → layerAxiom`. -- **Sentry tracing (CLI only).** The published CLI bridges the same Effect spans - into Sentry. `cli/utils/apply-observability.ts` is the single chooser of the - tracer backend (Effect has one `Tracer` reference, so they're mutually - exclusive): user OTLP wins (and the Effect trace is parented under the Sentry - trace via `Tracer.externalSpan` for a shared `trace_id`); otherwise, when - Sentry performance tracing is live, `cli/utils/sentry-tracer.ts` - (`makeSentryTracer`) materializes each Effect span as a child Sentry span - under the per-run transaction (`cli/utils/with-sentry-run-span.ts`); otherwise - the no-op native tracer. All of this is gated by `isSentryTracingEnabled()` so - it's a true no-op for the `@react-doctor/api` library, `--no-score`, tests, - and `SENTRY_TRACES_SAMPLE_RATE=0`. Source maps are uploaded for symbolication - by `scripts/sentry-sourcemaps.mjs` (Debug IDs); the SDK `release` - (`react-doctor@`) must match what that script uploads. -- **Sentry scope shape.** `cli/utils/build-sentry-scope.ts` is the one place that - projects the run snapshot (and the scanned project, once known) into Sentry - `tags` + `contexts`; both `instrument.ts` (`initialScope`) and - `report-error.ts` consume it, so add new metadata there, not at call sites. - Project info is captured in the `beforeLint` hook via - `recordSentryProjectContext` (`with-sentry-run-span.ts`), which both remembers - it for the lazy error path (a module-level ref read by `buildSentryScope`, - mirroring how `buildRunContext` reads ambient state at capture time) and sets - it as root-span attributes for the live transaction. -- **Sentry anonymization.** Telemetry must stay anonymized. `Sentry.init` sets - `sendDefaultPii: false`, and `beforeSend` + `beforeSendTransaction` both run - `scrubSentryEvent` (`cli/utils/scrub-sentry-event.ts`): it strips - hostname/`server_name`/device name and the IP-bearing `user`, drops captured - stack-frame local variables, and runs every remaining string (messages, - frames, contexts, extra, tags, breadcrumbs, and span attributes like - `inspect.directory`) through `scrubSensitivePaths` (home dir / username → - `~`, in `cli/utils/scrub-sensitive-text.ts`) + core's `redactSensitiveText` - (secrets/emails). `buildRunContext` also scrubs `cwd`/`argv` at the source. If - you add a new field to any Sentry event, confirm it carries no username, - hostname, IP, secret, or absolute path — and prefer adding it through - `buildSentryScope` so the central scrub covers it. `scrubSentryEvent` returns - `null` on any failure so an un-anonymized event is never sent. -- **Crash references + trace linkage.** `reportErrorToSentry` returns the Sentry - event id; the CLI catch blocks thread it into `handleError` so it's printed as - a user-quotable reference and added to the prefilled GitHub issue. Errors - thrown during a scan are linked to the run transaction by capturing them with - the run's trace as the scope's propagation context — `withSentryRunSpan` - records the live trace in `active-run-trace.ts` (cleared only on success, so - the command catch — which runs after the span ends — can still read it), and - `reportErrorToSentry` re-attaches via `scope.setPropagationContext`. -- **Sentry metrics (CLI only).** Anonymized Application Metrics (counters + - distributions) are emitted through `cli/utils/record-metric.ts` - (`recordCount` / `recordDistribution`), each guarded so it's a true no-op - unless `Sentry.isInitialized()` — inert for `--no-score`, tests, - and the `@react-doctor/api` library — and independent of `tracesSampleRate` - (metrics flow even when tracing is off). Metric names live in the `METRIC` map - in `cli/utils/constants.ts` (dotted, domain-grouped; high-cardinality - dimensions go in attributes, never the name). The run snapshot — and, once a - scan discovers it, the project shape — is merged onto **every metric at emit - time** by `record-metric.ts`'s `withRunAttributes`, which reprojects - `buildSentryScope().tags` per emit. This mirrors how events rebuild - `buildSentryScope` lazily, so metrics track runtime state (`--json` mode, a - workspace scan's project rolling over, the project clearing on - `resetSentryRunState`) rather than a stale init-time snapshot, and the - attributes pass through `beforeSendMetric` scrubbing like any other. Emit - sites pass only metric-specific attributes; the project shape comes from - `recordSentryProjectContext` → `getSentryProjectInfo()`. Per-scan metrics - (`scan.*`, `rule.fired`, `lint.failed`, …) are - emitted by `cli/utils/record-scan-metrics.ts`; `rule.fired` is one - high-cardinality counter keyed by `rule`/`plugin`/`category`/`severity` - attributes (never a metric-name-per-rule). Anonymization: `Sentry.init` sets - `beforeSendMetric: scrubSentryMetric` (`cli/utils/scrub-sentry-metric.ts`), - which drops the `server.address` hostname attribute (the SDK adds it to the - metric _before_ the hook, so the strip lands) and scrubs paths/secrets from - attribute values via the shared `cli/utils/anonymize-text.ts` (also used by - `scrubSentryEvent`), returning `null` to drop on failure. Add new counters - through `record-metric.ts` + the `METRIC` map, and confirm any new attribute - carries no username, path, or secret. -- **Sentry canonical run wide event (CLI only).** The richest telemetry is one - high-dimensionality "wide event" per scan, not a pile of narrow counters: the - per-run root span (`withSentryRunSpan`) is enriched with the full outcome by - `cli/utils/build-run-event.ts` (`recordRunEvent`, plus the pure, testable - `buildRunEventAttributes`). `inspect.ts` calls it on the success path (after - `recordScanMetrics`) and, via a `try/catch` around the span body, on the - failure path — so the event lands with an `outcome.status` (`clean`/`ok`/ - `blocked`/`error`), `outcome.exitCode`, and `outcome.errorTag` taxonomy even - when the scan throws. The run + project base context is already on the span - (run tags from `withSentryRunSpan`, project shape from - `recordSentryProjectContext`), so the event adds only what those don't — every - attribute namespaced by concept via `withNamespace` (`cli/utils/with-namespace.ts`) - so the keys tree up in Sentry's attribute browser: scan config (`scan.mode`, - `scan.parallel`, `scan.workerCount`, `scan.rulesConfigured`/`scan.rulesDisabled`, - `scan.ignoredTagCount`, `scan.hasCustomConfig`, … plus the `scan.fileCount` - extent), the verdict (`outcome.wouldBlock`/`outcome.blocking`/`outcome.clean`/ - `outcome.skippedChecks`), findings (`diag.total`, `diag.errors`/`diag.warnings`, - `diag.affectedFiles`, `diag.distinctRules`, `diag.topRule`, per-category - `diag.category.*`), `score.value`/`score.label`/`score.available`, the - `lint.*`/`deadCode.*`/`supplyChain.*` pass outcomes, `timing.*` durations, and - the CI/PR specifics (`action.actorAssociation`, `action.runnerOs`, and the - forwarded action knobs `action.comment`/`action.reviewComments`/ - `action.versionPin`). Typing matters for querying: numeric outcomes are numbers - (so Sentry can do `p75(score.value)`), dimensions are strings/bools (so they - filter/group); `null` is dropped via `toSpanAttributes` so absent signals never - become `"null"`. Query it in Sentry's **Trace Explorer** and build **Dashboard - widgets on the Spans dataset** (filter/group by any attribute) instead of - pre-aggregating counters. Put new run-level dimensions on `build-run-context.ts` - → `build-sentry-scope.ts` (now also the `eventName` + `viaAction` tags) so they - ride every event and metric; put per-scan outcome dimensions on the wide event - (wrapped in `withNamespace`), **not** new counters (we deliberately did not add - `ci.*` counters — those dims are wide-event attributes; the `scan.completed`/ - `scan.duration`/`rule.fired` metric counters stay as the cheap, - trace-sampling-independent floor alongside `cli.invoked`/`cli.error`). Score - reachability is derivable (`!score.available && !lint.failed && !deadCode.failed && !scan.noScore` — failed passes null the score deliberately) and - score latency is the `Score.compute` child span's duration, so neither needs a - dedicated field. CI detection + the official-action marker and forwarded - inputs live in `cli/utils/is-ci-environment.ts`; `action.yml` sets the - `REACT_DOCTOR_GITHUB_ACTION` marker + `REACT_DOCTOR_ACTION_*` env on its scan - step. All attributes pass through `scrubSentryEvent`; keep them free of - username, path, secret, and repo/owner identity. -- **runId.** `cli/utils/run-id.ts` mints one random `runId` per CLI run - (process). It rides the Sentry `run` context (and thus the wide event) but is - **never** a tag or metric attribute — a per-run unique value there would - explode tag/counter cardinality. A workspace invocation scanning several - projects shares one `runId`; the per-project span attributes disambiguate. Do - not add a plaintext or hashed repo id to Sentry. - -### Console / logging - -- ALWAYS: `import * as Console from "effect/Console"` and `yield* Console.log(...)` / - `Console.warn(...)` / `Console.error(...)` from inside renderers, services, and any - Effect-typed code. Effect's `Console` is a `Context.Reference` whose default sink is - `globalThis.console`, so the production path is identical to a raw `console.log` - while remaining swappable for tests / silent mode. -- NEVER: invent a parallel `Logger` / `LoggerWriter` abstraction. The historical custom - Logger service was removed when the renderer pipeline went Effect-typed; the only - remaining bridge is `cli/utils/cli-logger.ts`, a thin sync wrapper around - `Effect.runSync(Console.X)` for imperative CLI helpers that aren't yet `Effect.gen`. -- Silent mode is `Effect.provideService(Console.Console, silentConsole)` (renderer - pipeline) or `installSilentConsole()` (JSON mode, which monkey-patches the global - console because the surrounding CLI command body is imperative). Both routes leave - the underlying `Console.*` Effect intact — there is no `if (silent) return` check - at any call site. - -## Testing - -Tests live alongside source in each package's `tests/` directory: - -- `packages/core/tests/` — service tests + run-inspect orchestration tests -- `packages/api/tests/` — api shell tests -- `packages/react-doctor/tests/` — CLI + end-to-end fixture tests - -Test framework is `vite-plus/test` (the existing vitest wrapper). - -Run checks always before committing with: - -```bash -pnpm test # all packages -pnpm lint -pnpm typecheck -pnpm format # use `format:check` to verify only -pnpm smoke:json-report # validates the built CLI's JSON output against the schema -``` - -## Release authorization - -- MUST: Discourage minor and major Changesets. Do not add one unless the user explicitly requests - that release level. Patch Changesets may be added without a separate request when appropriate. -- MUST: Never merge a Changesets release PR, including any `changeset-release/*` branch, without - fresh, explicit user confirmation for that exact PR and version immediately before the merge. -- General instructions to merge, ship, land, or babysit green PRs do not authorize merging a - release/version PR. Treat merging a PR that triggers publication as publishing the release. -- MUST: Never publish packages, push or move release tags, or trigger, approve, rerun, or merge a - release/publish workflow without fresh, explicit user confirmation for the exact versions and - packages involved. -- Agents may prepare, validate, and babysit a release candidate, but must stop before the first - publishing action and report the exact PR, versions, packages, tags, and workflows awaiting user - approval. -- These confirmation requirements also apply to GitHub Action releases described below. Once the - user explicitly approves a specific release, follow all required versioning and tag steps. - -## GitHub Action versioning - -The composite GitHub Action is **versioned independently from the npm packages**. "The action" -is `action.yml` (repo root) plus the scripts it shells out to (`scripts/ensure-json-report.mjs`, -`scripts/normalize-changed-files.mjs`, `scripts/render-github-action-comment.mjs`, -`scripts/resolve-package-spec.mjs`). Treat a change to any of those files as an action release, -and keep the list in sync with `ACTION_RELEASE_FILES` in -`scripts/recommend-action-version-bump.mjs` (the release guard). - -- Two tag namespaces coexist — never conflate them: - - npm packages — `react-doctor@X.Y.Z`, `eslint-plugin-react-doctor@X.Y.Z`, - `oxlint-plugin-react-doctor@X.Y.Z` (created by Changesets in CI; see - `.github/workflows/publish.yml`). - - GitHub Action — `v`-prefixed semver `vX.Y.Z` plus a floating major `vN` (the GitHub Actions - convention; the `v` prefix keeps these distinct from the unprefixed package tags above). - Current: the `v2.x` line (check `git tag --list 'v*'` for the latest); `v2` → the same - commit. The `v0.x` line is the pre-rebuild action; the `f4035fce` PR-reporting rebuild is - `v1.0.0`. -- MUST: cut a tag on every commit that touches the action files. `feat(action)` → minor bump; - everything else (`fix` / `refactor` / `chore` / `revert` / docs-only edits to `action.yml`) → - patch bump. A breaking change to inputs/outputs or the runtime contract → major bump. -- MUST: after tagging a new `vX.Y.Z`, move the floating major `vN` to that same commit so - `uses: millionco/react-doctor@vN` keeps resolving to the latest compatible release. -- Tags are GPG-signed annotated tags (`tag.gpgsign=true`), so a bare `git tag vX` will demand a - message and fail in scripts. Always create/move with an explicit message: - -```bash -# new release at the commit that changed the action -git tag -a v2.2.3 -m "react-doctor action v2.2.3" -# move the floating major (force-update only the vN pointer) -git tag -fa v2 -m "react-doctor action v2 (floating major -> v2.2.3)" -git push origin v2.2.3 -git push --force origin v2 # the force applies to the moving major tag only -``` - -- MUST: never tell consumers to reference `@main` in docs/examples. `@main` runs whatever HEAD - points to with `pull-requests: write` granted — a supply-chain risk (issue #299). Recommend a - full commit-SHA pin with a trailing version comment for hardened CI - (`uses: millionco/react-doctor@ # v2.2.2`), or `@vN` for convenience. - -## Reference reading - -- `tmp/effect/.patterns/effect.md` — canonical Effect v4 idioms (cloned for reference, - gitignored) -- `~/Developer/react-doctor-evals/src/` — sister application this codebase's runtime - patterns are modeled on (Schemas.ts, Runner.ts, Worker.ts, errors.ts shapes) +Neither checkout is required. The tracked references above define the binding repository conventions. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..94439c2212 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Documentation + +## Architecture + +- [Internal architecture rewrite](architecture/rewrite.md) +- [Compatibility guarantees and snapshots](architecture/compatibility.md) + +## Product references + +- [JSON report](json-report.md) + +## Rule development + +- [How to write a rule](HOW_TO_WRITE_A_RULE.md) +- [Rule candidates backlog](rule-candidates-backlog.md) +- [MobX rule research](mobx-rule-research.md) +- [React Router rule research](react-router-rule-research.md) +- [Zustand rule research](zustand-rule-research.md) diff --git a/docs/architecture/compatibility.md b/docs/architecture/compatibility.md new file mode 100644 index 0000000000..829e159ff3 --- /dev/null +++ b/docs/architecture/compatibility.md @@ -0,0 +1,47 @@ +# Compatibility guarantees and snapshots + +In this repository, a compatibility guarantee is observable behavior that existing users can rely +on. Examples include package exports, CLI flags and help text, diagnostics, JSON reports, exit +codes, and runtime errors. + +The old root `contracts/` directory did not contain interfaces or specifications. It contained +machine-generated golden snapshots, so that evidence now lives beside the repository-wide tooling +that owns it under `scripts/compatibility/`. + +## Repository layout + +- `scripts/compatibility/snapshots/public-packages.json` records published package manifests and + exports; +- `scripts/compatibility/snapshots/packed-public-entry-points.json` records installed runtime entry + points, export keys, and packed-file policies; +- `scripts/compatibility/snapshots/cli-help.json` records every supported CLI help surface; +- `scripts/compatibility/approved-deltas.json` records reviewed temporary differences between old + and new implementations. + +The public `oxlint-plugin-react-doctor/contracts` package subpath is different: it is a supported API +entry point for shared rule and capability vocabulary. Renaming it would break consumers, so the +compatibility rewrite preserves it. + +## Commands + +```bash +nr compatibility:check +nr test:compatibility +nr smoke:packed-cli-install +``` + +After intentionally changing a published package surface: + +```bash +nr compatibility:update +nr compatibility:packed:update +``` + +Snapshot updates are review evidence, not an automatic fix. Review the diff and confirm that the +change is additive or otherwise authorized before committing it. + +## Approved differences + +An old/new mismatch must not be hidden by loose normalization. Add a temporary entry to +`scripts/compatibility/approved-deltas.json` only when it has an owner, rationale, exact observed +difference, expiry condition, and removal issue. The normal state is an empty list. diff --git a/docs/architecture/rewrite.md b/docs/architecture/rewrite.md new file mode 100644 index 0000000000..5b61e7834d --- /dev/null +++ b/docs/architecture/rewrite.md @@ -0,0 +1,310 @@ +# Internal architecture rewrite + +- Status: implemented and validated +- Started: 2026-07-27 +- Goal: simplify React Doctor's internals without changing observable behavior + +This document records durable design decisions. Git history and the pull request retain the +implementation diary. + +## Goals + +The rewrite makes it clear: + +- which package owns each responsibility; +- which direction dependencies may flow; +- where behavior is tested; +- which implementation details can change without affecting consumers; +- how replacements prove exact compatibility before old code is removed. + +This was an internal migration, not a product redesign. Existing package APIs, commands, flags, +diagnostics, reports, scores, terminal behavior, and production lint backend remain intact. + +## Reference codebases + +The design study used: + +- [aidenybai/react-grab](https://github.com/aidenybai/react-grab) at + `2a39bc29e5f8bdbd69095cf1d33d91634576cd20`; +- [aidenybai/bippy](https://github.com/aidenybai/bippy) at + `051a9b28f0a23da29b2e6e94a6e1ddae687b8926`; +- this repository's `AGENTS.md` and its owned references. + +These are sources of design judgment, not templates. + +### Lessons retained from React Grab + +**Deliberate public surfaces.** React Grab exposes small capability-oriented entry points rather +than mirroring its source tree. React Doctor follows the same principle: published exports describe +supported jobs, while implementations remain private and movable. + +**One-purpose utilities.** Stateless, domain-neutral helpers use descriptive kebab-case filenames +and stay focused. Domain behavior remains with its owner instead of collecting in a global utility +bucket. + +**Transactional state and cleanup.** Replacement state is prepared before it becomes active, and +partial setup is rolled back. React Doctor applies this to scan planning, caches, temporary files, +subprocesses, telemetry scopes, and progress reporting. + +**Facade and mechanism tests.** Published behavior, private mechanisms, failure paths, ordering, +cleanup, and legacy behavior are tested at their appropriate boundaries. + +**Performance-aware design.** AST traversal, path resolution, batching, cache fingerprints, and +process startup are measured rather than assumed to be cheap. + +**Explicit build targets.** Package entry points, runtime environments, declarations, bins, and +packed contents are tested as product behavior. + +React Grab's large orchestration and type files were not copied. Its useful patterns are the +facades, lifecycle ownership, testing boundaries, and explicit packaging. + +### Lessons retained from Bippy + +**A small conceptual center.** Bippy has one clear job and organizes optional capabilities around +it. React Doctor keeps scanning and diagnostics at the center while CLI, API, LSP, evaluation, and +editor behavior stay in adapters. + +**Visible side-effect ordering.** Process-global initialization belongs at composition roots. +Sentry, console mutation, environment capture, cache initialization, and CLI instrumentation do +not leak into library entry points. + +**Shared parity suites.** Bippy runs equivalent renderer behavior through several adapters. React +Doctor uses the same idea for real and virtual resources, evaluator and Oxlint execution, CLI +modes, package surfaces, and cached versus cold scans. + +**Composable cleanup.** Unsupported and disabled environments use no-op implementations, while +acquired resources have explicit scoped cleanup. + +**Packaging and adversarial integration tests.** Missing bindings, malformed configuration, +worktrees, symlinks, timeouts, partial failures, repeated state, and installed tarballs receive +direct coverage. + +Bippy's intentionally centralized React-internals code was not copied. React Doctor's domains are +more separable. + +## Compatibility guarantees + +[Compatibility guarantees and snapshots](compatibility.md) owns the update process and repository +paths. + +The rewrite preserves: + +- package names, bins, subpath exports, module formats, declarations, and packed files; +- exported names, signatures, runtime identities, and error behavior; +- CLI commands, aliases, flags, defaults, validation, streams, prompts, and exit codes; +- JSON report schemas 1, 2, and 3; +- diagnostic identity, ordering, locations, severity, suppression, and scoring; +- configuration filenames, schema, precedence, and path resolution; +- LSP diagnostics, data, hovers, actions, and commands; +- GitHub Action inputs, outputs, environment behavior, comments, and failures; +- telemetry names, attribute types, privacy filtering, and disabled no-op behavior; +- supported Node, package-manager, Windows, macOS, Linux, and terminal behavior. + +Only nondeterministic values already outside the product guarantee may be normalized, such as +elapsed time, generated temporary roots, and Oxlint's generated `start_time`. Every other old/new +difference must be eliminated or recorded in the reviewed delta ledger. + +## Package ownership + +| Package | Owner | +| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `packages/core` | Diagnostic engine, project discovery, scan policy, services, runners, post-processing, and scoring | +| `packages/api` | Programmatic `diagnose()` shell | +| `packages/react-doctor` | Published CLI, public `inspect()`, rendering, and runtime composition | +| `packages/oxlint-plugin-react-doctor` | Canonical rule engine and rule implementation | +| `packages/eslint-plugin-react-doctor` | ESLint mirror | +| `packages/language-server` | Editor protocol adapter | +| `packages/evals` | Repository evaluation harness | +| `packages/fuzz` | Adversarial rule fuzzing | +| `packages/deslop-js` and `packages/deslop-cli` | Dead-code and redundancy products | +| editor packages | Thin editor-specific adapters | + +## Dependency direction + +```text +published adapters + CLI / API / LSP / ESLint + | + v +application workflows + resolve request -> plan -> execute -> assemble + | + v +domain capabilities + diagnostics / projects / config / scoring / suppression + | + v +service interfaces + files / git / linter / dead code / supply chain / reporting + | + v +infrastructure + filesystem / subprocesses / Oxlint / HTTP / persistent caches +``` + +Rules: + +1. Foundation types, schemas, and errors do not import services, runners, orchestration, telemetry, + or CLI code. +2. Project discovery remains below runtime services and orchestration. +3. Domain-neutral leaf utilities do not depend on runtime layers. +4. Infrastructure implements service interfaces and owns third-party details. +5. Workflows coordinate through services. +6. API, CLI, and LSP adapters select layers and translate results. +7. Rendering and telemetry consume completed events and results; they do not own scan policy. +8. Compatibility facades may point inward; new internals never point back to a broad facade. + +The architecture checker parses production imports and enforces these directions. + +## Implemented architecture + +### Scan workflow + +The former Core orchestrator was split into owned stages: + +- resolve scan settings; +- build lint and dead-code execution plans; +- run lint, dead-code, supply-chain, and project checks; +- coordinate background analyzers; +- finalize diagnostic output; +- assemble score metadata and the final result. + +The old facade remains where callers need it, but policy and lifecycle code now have focused owners. + +### Project model + +A normalized package graph collects workspace packages, package boundaries, dependency +declarations, versions, workspace/catalog resolution, and package-local capabilities once. + +Legacy `ProjectInfo` fields remain as a compatibility projection. Rules and scans can query the +package that owns the current file without repeating nearest-`package.json` walks or collapsing a +monorepo into one dependency answer. + +### Git and project services + +Git command execution, output parsing, revision policy, diff selection, and the Effect service are +separate modules. Project checks have an explicit service boundary. Errors remain typed +`ReactDoctorError` values with existing external behavior. + +### React Doctor adapter + +The published package consumes Core through cohesive private adapters for configuration, +diagnostics, errors, presentation, product metadata, project discovery, reporting, runtime +composition, scan caching, scoring, types, and version control. + +An AST boundary test prevents broad Core imports from leaking back into CLI, Ink, telemetry, +cache, or public-facade code. + +Option resolution, project scan planning, baseline comparison, rendering, cache policy, cache +lifecycle, and result construction now have explicit owners. + +### Rule evaluation + +The production-owned private evaluator reuses the parser, visitor, scope, control-flow graph, rule +registry, source locations, and suppression infrastructure. + +It accepts single-source and virtual-project inputs through one resource-host boundary. Real and +in-memory hosts use the same normalized resource semantics. Unsupported rules fail explicitly +instead of silently returning incomplete diagnostics. + +The evaluator remains private because its supported-rule matrix, Flow policy, fixes, secondary +labels, open-source evidence, and resource budgets are not yet broad enough for a stable public +API. + +### Backend boundary + +The `Linter` service owns backend selection. Production still uses fresh Oxlint subprocesses with +the existing OOM splitting and serial fallback. + +### Backend-neutral diagnostic processing + +Suppression, severity overrides, deduplication, ordering, score filtering, and result assembly are +owned outside a specific linter backend. Shared tests freeze their exact behavior. + +### Build and repository policy + +The repository now enforces: + +- source dependency directions; +- public package manifests and exports; +- installed tarball entry points and runtime dependencies; +- CLI help surfaces; +- JSON report smoke behavior; +- published dependency policy; +- generated rule-registry freshness; +- synchronized skills and valid agent references. + +Compatibility evidence and its tooling live together under `scripts/compatibility/`. + +## Validated proposal decisions + +| Proposal | Decision | +| ------------------------------------------- | ------------------------------------------------------------------------------- | +| Public in-process rule runner | Keep private until the supported-rule matrix and product semantics are complete | +| Virtual-project evaluation | Implemented privately through the resource host | +| Normalized package graph | Implemented as the source of workspace and dependency truth | +| Capabilities derived from package graph | Implemented while preserving legacy `ProjectInfo` fields | +| Package-aware rule context | Implemented with legacy activation unchanged by default | +| In-process production backend | Rejected for now; the host surface and parity corpus are incomplete | +| Oxlint/in-process parity suite | Implemented for the supported evaluator corpus | +| Explicit `--workers` CLI option | Deferred as a public product decision | +| Global scan worker budgeting | Implemented internally across concurrent project scans | +| Backend-neutral post-processing | Implemented and protected by shared behavior tests | +| Separate `diagnose()` and `evaluate()` APIs | `diagnose()` remains public; `evaluate()` remains private | + +## Exact-compatibility proof + +Each migration slice was checked at several boundaries: + +1. **Source surface:** exports, signatures, identities, errors, rule IDs, configuration, CLI input, + and schema versions. +2. **Built product:** real workspace tarballs installed into an empty project, with every supported + entry imported and the installed CLI executed. +3. **Behavior:** diagnostics, ordering, locations, severity, suppression, score input, output + streams, exit status, errors, cache replay, and editor protocol data. +4. **Differential execution:** real resources, in-memory resources, the private evaluator, and + built Oxlint compared through common fixtures where supported. +5. **Resilience:** OOM splitting, serial fallback, deadlines, partial failures, aborts, output + limits, repeated state, and cleanup. +6. **Architecture:** parsed import boundaries, adapter bypass checks, generated outputs, and skill + integrity. + +The normal approved-delta ledger is empty. + +## Required validation + +Run the repository gates from `AGENTS.md`: + +```bash +nr test +nr lint +nr typecheck +nr format:check +nr smoke:json-report +nr architecture:check +nr test:architecture +nr compatibility:check +nr test:compatibility +nr test:build-policy +nr skills:check +nr build +nr check:published-deps +nr smoke:packed-cli-install +``` + +CI additionally covers supported Node versions, Windows, macOS, Linux, CodeQL, terminal recording, +and the React Doctor self-scan. + +## Deferred work + +- Expand virtual-project support and differential evidence across the remaining cross-file rule + families. +- Add fixes, secondary labels, Flow policy, budgets, and open-source-hit review before considering + a public `evaluate()` API. +- Revisit an in-process backend only after effectively complete parity over a representative + corpus. +- Treat `--workers` as a separately reviewed CLI decision. +- Keep legacy `ProjectInfo` compatibility fields until a separately authorized breaking release. +- Add a downstream TypeScript consumer fixture to the installed-tarball checks. + +These are future product or backend decisions, not unfinished structural cleanup. diff --git a/packages/react-doctor/tests/react-doctor-skill-parity.test.ts b/packages/react-doctor/tests/react-doctor-skill-parity.test.ts new file mode 100644 index 0000000000..7d3fe799bf --- /dev/null +++ b/packages/react-doctor/tests/react-doctor-skill-parity.test.ts @@ -0,0 +1,57 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describe, expect, it } from "vite-plus/test"; +import { + CANONICAL_REACT_DOCTOR_SKILL_DIRECTORY, + findReactDoctorSkillTreeMismatches, +} from "../../../scripts/sync-react-doctor-skill.mjs"; +import { resolveScope } from "../src/cli/utils/resolve-scope.js"; +import { stripUnknownCliFlags } from "../src/cli/utils/strip-unknown-cli-flags.js"; + +const readCanonicalSkillDocuments = (): string => + [ + path.join(CANONICAL_REACT_DOCTOR_SKILL_DIRECTORY, "SKILL.md"), + path.join(CANONICAL_REACT_DOCTOR_SKILL_DIRECTORY, "references", "explain.md"), + ] + .map((filePath) => fs.readFileSync(filePath, "utf8")) + .join("\n"); + +describe("React Doctor skill source parity", () => { + it("keeps the repository adapter identical to the canonical distributed skill", () => { + expect(findReactDoctorSkillTreeMismatches()).toEqual([]); + }); + + it("reports changed generated files", () => { + const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-skill-parity-")); + const canonicalDirectory = path.join(temporaryDirectory, "canonical"); + const adapterDirectory = path.join(temporaryDirectory, "adapter"); + try { + fs.mkdirSync(canonicalDirectory); + fs.mkdirSync(adapterDirectory); + fs.writeFileSync(path.join(canonicalDirectory, "SKILL.md"), "canonical\n"); + fs.writeFileSync(path.join(adapterDirectory, "SKILL.md"), "stale\n"); + + expect(findReactDoctorSkillTreeMismatches(canonicalDirectory, adapterDirectory)).toEqual([ + "changed adapter entry: SKILL.md", + ]); + } finally { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); + } + }); + + it("documents the supported changed-scope workflow", () => { + const skillDocuments = readCanonicalSkillDocuments(); + expect(skillDocuments).toContain("--scope changed"); + expect(skillDocuments).not.toMatch(/npx react-doctor@latest[^\n]*--diff/); + + expect( + stripUnknownCliFlags(["node", "react-doctor", ".", "--scope", "changed"]).slice(2), + ).toEqual([".", "--scope", "changed"]); + expect(resolveScope({ scope: "changed" }, null)).toEqual({ + scope: "changed", + base: undefined, + usedDeprecatedDiff: false, + }); + }); +}); diff --git a/skills/react-doctor/references/explain.md b/skills/react-doctor/references/explain.md index c3a10089ec..d1cc4b20a2 100644 --- a/skills/react-doctor/references/explain.md +++ b/skills/react-doctor/references/explain.md @@ -21,7 +21,7 @@ npx react-doctor@latest rules explain react-doctor/no-array-index-as-key 5. Validate the change did what they wanted: ```bash -npx react-doctor@latest --verbose --diff +npx react-doctor@latest --verbose --scope changed ``` ## Commands