diff --git a/.agents/skills/product-thinking/SKILL.md b/.agents/skills/product-thinking/SKILL.md index 8563d6ad8..c452f29c3 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 `react-doctor/references/explain.md` — 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 (`skills/react-doctor`, `skills/react-doctor/references/explain.md`) when the change alters the user-facing workflow. ### 6. Record the kill metric diff --git a/packages/core/src/project-info/count-source-files.ts b/packages/core/src/project-info/count-source-files.ts index e48c51707..5ac895cbd 100644 --- a/packages/core/src/project-info/count-source-files.ts +++ b/packages/core/src/project-info/count-source-files.ts @@ -40,7 +40,7 @@ const countSourceFilesViaGit = (rootDirectory: string): number | null => { // filesystem walk in countSourceFilesViaFilesystem. const result = spawnSync( "git", - ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], + ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], { cwd: rootDirectory, encoding: "utf-8", diff --git a/packages/core/src/utils/list-source-files.ts b/packages/core/src/utils/list-source-files.ts index 929bb3771..2c402215b 100644 --- a/packages/core/src/utils/list-source-files.ts +++ b/packages/core/src/utils/list-source-files.ts @@ -35,7 +35,7 @@ const listSourceFilesViaGit = (rootDirectory: string): string[] | null => { // skipping submodule files entirely. const result = spawnSync( "git", - ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], + ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], { cwd: rootDirectory, encoding: "utf-8", diff --git a/packages/core/tests/list-source-files-with-size.test.ts b/packages/core/tests/list-source-files-with-size.test.ts index 426b28721..34ee76dd4 100644 --- a/packages/core/tests/list-source-files-with-size.test.ts +++ b/packages/core/tests/list-source-files-with-size.test.ts @@ -1,8 +1,10 @@ +import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import os from "node:os"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { MINIFIED_MIN_SIZE_BYTES } from "../src/project-info/constants.js"; +import { countSourceFiles } from "../src/project-info/count-source-files.js"; import { listSourceFiles, listSourceFilesWithSize } from "../src/utils/list-source-files.js"; describe("listSourceFilesWithSize", () => { @@ -56,4 +58,23 @@ describe("listSourceFilesWithSize", () => { listSourceFilesWithSize(temporaryDirectory).map((entry) => entry.path), ); }); + + it("scopes git listings to the requested subdirectory", () => { + const workspaceDirectory = path.join(temporaryDirectory, "workspace"); + const appDirectory = path.join(workspaceDirectory, "apps", "web"); + const packageDirectory = path.join(workspaceDirectory, "packages", "ui"); + fs.mkdirSync(appDirectory, { recursive: true }); + fs.mkdirSync(packageDirectory, { recursive: true }); + fs.writeFileSync(path.join(appDirectory, "App.tsx"), "export const App = () => null;\n"); + fs.writeFileSync( + path.join(packageDirectory, "Button.tsx"), + "export const Button = () => null;\n", + ); + + spawnSync("git", ["init"], { cwd: workspaceDirectory }); + spawnSync("git", ["add", "."], { cwd: workspaceDirectory }); + + expect(listSourceFiles(appDirectory)).toEqual(["App.tsx"]); + expect(countSourceFiles(appDirectory)).toBe(1); + }); }); diff --git a/packages/core/tests/services/dead-code.test.ts b/packages/core/tests/services/dead-code.test.ts index 9ee4d7dd6..2b01a12e6 100644 --- a/packages/core/tests/services/dead-code.test.ts +++ b/packages/core/tests/services/dead-code.test.ts @@ -2,7 +2,7 @@ import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Stream from "effect/Stream"; import { describe, expect, it } from "vite-plus/test"; -import type { Diagnostic, ProjectInfo } from "@react-doctor/core"; +import type { Diagnostic } from "@react-doctor/core"; import { DeadCode } from "../../src/services/dead-code.js"; const sampleDiagnostic: Diagnostic = { @@ -17,14 +17,6 @@ const sampleDiagnostic: Diagnostic = { category: "Maintainability", }; -const sampleInput = { - rootDirectory: "/repo", - userConfig: null, -} satisfies { - rootDirectory: string; - userConfig: ProjectInfo["framework"] extends string ? null : null; -}; - describe("DeadCode.layerOf", () => { it("emits the supplied diagnostics as a stream", async () => { const collected = await Effect.runPromise( @@ -70,5 +62,3 @@ describe("DeadCode.layerNode", () => { } }); }); - -void sampleInput; diff --git a/packages/react-doctor/src/cli/utils/render-multi-project-summary.ts b/packages/react-doctor/src/cli/utils/render-multi-project-summary.ts index ba6eda091..749411e54 100644 --- a/packages/react-doctor/src/cli/utils/render-multi-project-summary.ts +++ b/packages/react-doctor/src/cli/utils/render-multi-project-summary.ts @@ -23,8 +23,8 @@ interface ProjectScanEntry { const getScoreLabel = (score: number): string => { if (score >= SCORE_GOOD_THRESHOLD) return "Great"; - if (score >= SCORE_OK_THRESHOLD) return "OK"; - return "Needs work"; + if (score >= SCORE_OK_THRESHOLD) return "Needs work"; + return "Critical"; }; const buildSummaryLine = (entry: ProjectScanEntry, longestProjectNameLength: number): string => { diff --git a/packages/react-doctor/src/cli/utils/resolve-cli-inspect-options.ts b/packages/react-doctor/src/cli/utils/resolve-cli-inspect-options.ts index 6e8ae4806..2f766e0ed 100644 --- a/packages/react-doctor/src/cli/utils/resolve-cli-inspect-options.ts +++ b/packages/react-doctor/src/cli/utils/resolve-cli-inspect-options.ts @@ -45,6 +45,7 @@ export const resolveCliInspectOptions = ( noScore: flags.score === false || flags.telemetry === false || (userConfig?.noScore ?? false), isCi: isCiEnvironment(), silent: Boolean(flags.json), + suppressRendering: Boolean(flags.json), concurrency: resolveParallelFlag(flags.parallel), categoryFilters: resolveCliCategories(flags.category), }; diff --git a/packages/react-doctor/tests/resolve-cli-inspect-options.test.ts b/packages/react-doctor/tests/resolve-cli-inspect-options.test.ts index b8f09b8eb..a5be4b8a0 100644 --- a/packages/react-doctor/tests/resolve-cli-inspect-options.test.ts +++ b/packages/react-doctor/tests/resolve-cli-inspect-options.test.ts @@ -101,6 +101,13 @@ describe("resolveCliInspectOptions: --no-telemetry alias", () => { it("keeps scoring on by default", () => { expect(resolveCliInspectOptions({}, null).noScore).toBe(false); }); + + it("suppresses rendering when JSON owns stdout", () => { + expect(resolveCliInspectOptions({ json: true }, null)).toMatchObject({ + silent: true, + suppressRendering: true, + }); + }); }); describe("resolveCliInspectOptions: category filtering", () => { diff --git a/packages/website/public/llms.txt b/packages/website/public/llms.txt index 2fe7ea914..e97c51634 100644 --- a/packages/website/public/llms.txt +++ b/packages/website/public/llms.txt @@ -2,7 +2,7 @@ Diagnose React codebase health. Scans for security, performance, correctness, and architecture issues, then outputs a 0–100 score with actionable diagnostics. -Note: dead-code detection was removed in v0.2 (previously powered by knip). For dead-code analysis, run `npx knip` directly. +Dead-code detection is included by default and powered by deslop-js. Use `--no-dead-code` when you only want lint diagnostics. ## Usage diff --git a/packages/website/src/components/terminal.tsx b/packages/website/src/components/terminal.tsx index ac2099956..2ce49a496 100644 --- a/packages/website/src/components/terminal.tsx +++ b/packages/website/src/components/terminal.tsx @@ -52,7 +52,7 @@ const DIAGNOSTICS: RuleDiagnostic[] = [ location: "src/components/Dashboard.tsx:42", }, { - ruleKey: "react-doctor/no-server-action-auth", + ruleKey: "react-doctor/server-auth-actions", severity: "error", message: 'Server action "deleteUser" has no auth check, so any caller could run it', help: "Add an authentication check at the top of every server action.", @@ -60,7 +60,7 @@ const DIAGNOSTICS: RuleDiagnostic[] = [ location: "src/app/actions/users.ts:18", }, { - ruleKey: "react/no-array-index-key", + ruleKey: "react-doctor/no-array-index-key", severity: "error", message: "Array index is used as a key, so reordered items can keep the wrong state", help: "Use a unique, stable identifier from each item as the key prop.", @@ -171,6 +171,7 @@ const DiagnosticItem = ({ diagnostic }: { diagnostic: RuleDiagnostic }) => { return (
@@ -272,14 +273,11 @@ const markAnimationCompleted = () => { }; const Terminal = () => { - const [state, setState] = useState(INITIAL_STATE); + const [state, setState] = useState(() => + didAnimationComplete() ? COMPLETED_STATE : INITIAL_STATE, + ); useEffect(() => { - if (didAnimationComplete()) { - setState(COMPLETED_STATE); - return; - } - const abortController = new AbortController(); const { signal } = abortController; @@ -408,6 +406,7 @@ const Terminal = () => { {state.showCta && (