Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .agents/references/coding-conventions.md
Original file line number Diff line number Diff line change
@@ -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 "<query>" 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.
74 changes: 74 additions & 0 deletions .agents/references/effect-v4.md
Original file line number Diff line number Diff line change
@@ -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<Self>()("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<Self, Interface>()("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<Self>("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<T>("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
72 changes: 72 additions & 0 deletions .agents/references/observability.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading