Think like a product manager before changing React Doctor's public surface — CLI commands/flags, the 0–100 score, config (doctor.config.*), the JSON report sche
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-product-thinking-b715440b40b7 ,按照其中的说明把「product-thinking」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
A runbook for changing anything a user interacts with. The agent defaults to building exactly what was asked; this pass adds the product checks it skips — whether the change earns its permanent place on the surface, and how you'll know it worked. Work the steps before you write code, fill in the brief as you go, and fold that brief into the PR. Use it to push back when a change isn't justified, not just to wave one through.
Run the pass whenever a diff touches a surface a user — a developer running React Doctor, or their CI — depends on. Every surface is a contract: once it ships, people build on it and you can't quietly take it back. Find where the surface lives first, so you edit the right place and can search it for something to reuse before adding.
| Surface | Where it lives | Why it's load-bearing |
|---|---|---|
| CLI command / flag | cli/index.ts, cli/commands/*.ts, cli/utils/inspect-flags.ts | Every flag is forever — it has to be parsed, documented, tested, and kept working across releases. |
| 0–100 score | core/src/calculate-score.ts, core/src/constants.ts | The number people screenshot and gate CI on; changing the math silently moves every user's score. |
| Config option | core/src/types/config.ts, core/src/services/config.ts | A long-lived contract that interacts with existing precedence; a new knob is rarely removable later. |
| JSON report field | core/src/schemas.ts, core/src/build-json-report.ts | A wire format other tools parse; a shape change breaks integrations unless schemaVersion is handled. |
| Package API / exit code | react-doctor/src/inspect.ts, api/src/diagnose.ts | Programmatic consumers pin to it, so changes are semver-breaking and need a changeset. |
| 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.
- [ ] 1. Write the job + the change (2 lines)
- [ ] 2. Search for a reuse candidate; extend it or justify new surface
- [ ] 3. Wire exactly one metric
- [ ] 4. Pick the default + add the compatibility artifacts
- [ ] 5. Update docs
- [ ] 6. Record the kill metric
- [ ] 7. Run checks
Write two lines before anything else — they anchor every later step:
If you can't name a job the current surface can't already serve, stop — there is nothing to build, and the right move is to point the user at the existing way to do it.
Then run two filters before you commit to building:
Reuse beats adding every time — the cheapest surface is the one you never create. Before adding a flag, option, or report field, search for an existing one to extend:
bunx @rayhanadev/truffler "<name or behavior>" packages --kind function,interface,type,constant --limit 20
rg -n "<flag-or-option-name>" packages/react-doctor/src/cli packages/core/src/types
Read the top matches before writing anything. Extend an existing surface when it does almost the same job — a new value on an existing flag, a new field on an existing config object, an extra column on output that's already there. Only add a brand-new surface when the job is genuinely distinct and no existing entry can carry it without becoming confusing. If you do add one, record in the brief what you searched for and why nothing fit, so the next person inherits the reasoning instead of re-litigating it.
You can't manage a surface you can't see, so a change isn't done until it emits a signal. Decide that signal before you build — if you can't describe which number would move, you don't yet know what success means. CLI telemetry is anonymized Sentry and is already a no-op for the @react-doctor/api library, tests, and --no-score, so all you wire is the emit:
METRIC map in cli/utils/constants.ts and emit it via cli/utils/record-metric.ts. Use this for "did people turn this flag on".cli/utils/build-run-event.ts (a run-level dimension goes on cli/utils/build-sentry-scope.ts). Prefer one rich wide-event attribute over a pile of narrow counters — it stays queryable after the fact without new code.Anonymization is non-negotiable: never put a username, path, secret, or repo identity in an attribute — everything must ride scrubSentryEvent / scrubSentryMetric. Then write the metric name and the threshold that would mean "this worked" into the brief.
North-star to judge against: the one durable metric is CI cohort retention — are teams still running React Doctor in CI weeks later? Treat most other numbers as vanity. For "is this loved rather than merely used," the proxy is a PMF survey scoring ≥40% "very disappointed" if it went away.
The default is a product decision, not an implementation detail — most users never change it, so the default is the behavior for almost everyone. Pick one that preserves today's behavior and make anything risky opt-in. Then ship the artifacts that keep existing users from breaking:
nr changeset) so the change is versioned and lands in the changelog.core/src/schemas.ts, preserve the existing shape or bump schemaVersion, and run nr smoke:json-report. CI pipelines parse this output, so an unannounced shape change breaks them silently.action.yml changed → cut a vX.Y.Z tag and move the floating vN, because other repos pin to the major and won't receive the change otherwise.A surface nobody can discover is wasted, and a stale doc is a trust bug. Documentation is part of "done," not a follow-up:
--help / usage text next to the new flag or command, so it's discoverable from the CLI itself.react.doctor/prompts/..., which is what agents fetch at runtime.skills/react-doctor, skills/doctor-explain) when the change alters the user-facing workflow.Surfaces accrete: every unused flag and dead config key is permanent maintenance and one more thing for users to misread. Decide up front how you'll know this one didn't earn its keep. Write one line in the brief — the metric from step 3, a threshold, and a horizon, e.g. "remove if <metric> stays under N after 2 releases". Schedule the check instead of trusting yourself to remember it, and treat deleting unused surface as a win, not an admission of failure.
nr typecheck && nr test && nr lint
nr smoke:json-report # only if you touched the JSON report
Fill this in as you work the steps; fold it into the PR's "Why".
## Product brief: <change>
Job: <who / goal / what they do today>
Change: <smallest surface that does it>
Reuse: <what you searched; what you extended, or why new surface was needed>
Metric: <METRIC name or wide-event attribute + the number that means success>
Compat: <default state; changeset / schemaVersion / action tag>
Kill: <metric threshold + horizon that triggers removal>
These are the moments to surface a decision instead of pushing through — each one is a sign the change needs a human call:
schemaVersion, the score, or action I/O) → confirm the changeset, version bump, and migration path before landing.To land the change once the pass is clean, use ship (deslop, changeset, PR, babysit).
Tie-breakers when a step is ambiguous — when two options both seem fine, pick the one these favor: