@mrciphersmith/keryx 0.2.164 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/cli.js +82540 -50300
- package/dist/core.js +28967 -18937
- package/package.json +2 -2
- package/src/gdgraph/affected-report.ts +141 -0
- package/src/gdgraph/build.ts +170 -23
- package/src/gdgraph/service.ts +6 -0
- package/src/gdgraph/staleness.ts +253 -45
- package/src/gdskills/bundled/agents/codebase-navigator.md +55 -0
- package/src/gdskills/bundled/agents/design-advisor.md +64 -0
- package/src/gdskills/bundled/agents/docs-maintainer.md +56 -0
- package/src/gdskills/bundled/agents/end-to-end-tester.md +56 -0
- package/src/gdskills/bundled/agents/error-path-auditor.md +57 -0
- package/src/gdskills/bundled/agents/go-build-fixer.md +52 -0
- package/src/gdskills/bundled/agents/go-code-auditor.md +49 -0
- package/src/gdskills/bundled/agents/performance-auditor.md +63 -0
- package/src/gdskills/bundled/agents/python-build-fixer.md +52 -0
- package/src/gdskills/bundled/agents/python-code-auditor.md +49 -0
- package/src/gdskills/bundled/agents/refactoring-steward.md +61 -0
- package/src/gdskills/bundled/agents/security-auditor.md +62 -0
- package/src/gdskills/bundled/agents/test-first-driver.md +61 -0
- package/src/gdskills/bundled/agents/work-planner.md +62 -0
- package/src/gdskills/bundled/install-manifest.json +797 -0
- package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
- package/src/gdskills/bundled/rules/core/skill-lifecycle.mdc +29 -1
- package/src/gdskills/bundled/rules/core/skills-storage-workflow.mdc +2 -2
- package/src/gdskills/bundled/skills/review/code-style-review/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +26 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +75 -247
- package/src/gdskills/bundled/skills/review/review-orchestrator/output-contract.schema.json +19 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +10 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +5 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-backend.md +50 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-frontend.md +52 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/review-report.md +143 -0
- package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
- package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
- package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
- package/src/gdskills/bundled/stacks/go/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/go/governance/eval.json +1745 -0
- package/src/gdskills/bundled/stacks/go/governance/scout.json +31 -0
- package/src/gdskills/bundled/stacks/go/pack.json +41 -0
- package/src/gdskills/bundled/stacks/go/rules/coding-style.mdc +85 -0
- package/src/gdskills/bundled/stacks/go/rules/patterns.mdc +65 -0
- package/src/gdskills/bundled/stacks/go/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/go/rules/testing.mdc +68 -0
- package/src/gdskills/bundled/stacks/go/skills/go-build-fix/SKILL.md +138 -0
- package/src/gdskills/bundled/stacks/go/skills/go-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/go/skills/go-code-review/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/go/skills/go-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/go/skills/go-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/go/skills/go-implementation/evals.json +76 -0
- package/src/gdskills/bundled/stacks/go/skills/go-testing/SKILL.md +126 -0
- package/src/gdskills/bundled/stacks/go/skills/go-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
- package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
- package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
- package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
- package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
- package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
- package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
- package/src/gdskills/bundled/stacks/python/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/python/governance/eval.json +1758 -0
- package/src/gdskills/bundled/stacks/python/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/python/pack.json +41 -0
- package/src/gdskills/bundled/stacks/python/rules/coding-style.mdc +63 -0
- package/src/gdskills/bundled/stacks/python/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/python/rules/security.mdc +84 -0
- package/src/gdskills/bundled/stacks/python/rules/testing.mdc +77 -0
- package/src/gdskills/bundled/stacks/python/skills/python-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/python/skills/python-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/python/skills/python-code-review/SKILL.md +155 -0
- package/src/gdskills/bundled/stacks/python/skills/python-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/python/skills/python-implementation/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/python/skills/python-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/python/skills/python-testing/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/python/skills/python-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/react/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/react/governance/eval.json +2188 -0
- package/src/gdskills/bundled/stacks/react/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/react/pack.json +42 -0
- package/src/gdskills/bundled/stacks/react/rules/coding-style.mdc +58 -0
- package/src/gdskills/bundled/stacks/react/rules/patterns.mdc +79 -0
- package/src/gdskills/bundled/stacks/react/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/react/rules/testing.mdc +60 -0
- package/src/gdskills/bundled/stacks/react/skills/react-build-fix/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/react/skills/react-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/react/skills/react-code-review/SKILL.md +148 -0
- package/src/gdskills/bundled/stacks/react/skills/react-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/react/skills/react-implementation/SKILL.md +140 -0
- package/src/gdskills/bundled/stacks/react/skills/react-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/react/skills/react-testing/SKILL.md +142 -0
- package/src/gdskills/bundled/stacks/react/skills/react-testing/evals.json +83 -0
- package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/SKILL.md +155 -0
- package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ts-js-node/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ts-js-node/governance/eval.json +2155 -0
- package/src/gdskills/bundled/stacks/ts-js-node/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/ts-js-node/pack.json +41 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/patterns.mdc +61 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/security.mdc +71 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/SKILL.md +152 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/evals.json +71 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/evals.json +70 -0
- package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
- package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
- package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
- package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-code-review
|
|
3
|
+
description: "Use when reviewing changed React component or hook code (.tsx/.jsx) for Rules of Hooks violations, effect dependency bugs, stale closures, key misuse, in-place state mutation, re-render hot spots, accessibility gaps, and dangerouslySetInnerHTML/URL XSS sinks -- read-only, no MobX store review and no repository convention-doc lookup."
|
|
4
|
+
triggers:
|
|
5
|
+
- "review this react component diff for hook and jsx bugs"
|
|
6
|
+
- "check the react hooks in this pull request for rules of hooks violations"
|
|
7
|
+
- "does this react useEffect have a dependency array bug"
|
|
8
|
+
- "review this component for accessibility"
|
|
9
|
+
- "any stale closure in this hook"
|
|
10
|
+
- "check for xss in this jsx"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: review
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# React code review
|
|
20
|
+
|
|
21
|
+
Read-only review of changed `.tsx`/`.jsx` component/hook code against
|
|
22
|
+
React's own correctness and accessibility model. This skill never edits
|
|
23
|
+
code — it reports findings for a human or a follow-up fix skill
|
|
24
|
+
(`react-build-fix`) to act on.
|
|
25
|
+
|
|
26
|
+
**Scope boundary (read this before triggering):** this skill checks
|
|
27
|
+
React's rendering/hooks model itself — Rules of Hooks, effect
|
|
28
|
+
dependencies, closures, keys, mutation, re-renders, accessibility, and DOM
|
|
29
|
+
XSS sinks. It does NOT review MobX (or any other) state-management-library
|
|
30
|
+
usage (observers, actions, stores, reactions — that is a store-review
|
|
31
|
+
skill's job) and it does NOT check a repository's own local convention
|
|
32
|
+
docs (CLAUDE.md-style rules, i18n placement, styling tokens — that is a
|
|
33
|
+
convention-review skill's job). On a diff that touches both plain React
|
|
34
|
+
concerns and a state-management store, run this skill for the component
|
|
35
|
+
side and the appropriate store-review skill for the store side; see
|
|
36
|
+
`governance/scout.json` for why these stay separate skills instead of one
|
|
37
|
+
merged reviewer.
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
### Step 1: Scope the diff
|
|
42
|
+
|
|
43
|
+
1. Identify changed `.tsx`/`.jsx` files and, within them, changed hooks,
|
|
44
|
+
effects, event handlers, and JSX return blocks — line-level, not the
|
|
45
|
+
whole file, unless the whole file is new.
|
|
46
|
+
2. Note the React major and whether the React Compiler is enabled
|
|
47
|
+
(changes whether manual memoization is a finding or a non-issue).
|
|
48
|
+
3. Skim 1 file of surrounding context per changed component to know its
|
|
49
|
+
existing prop/state shape — a review without context misreads intent.
|
|
50
|
+
|
|
51
|
+
### Step 2: Check hooks correctness
|
|
52
|
+
|
|
53
|
+
- Rules of Hooks: any hook called conditionally, in a loop, after an early
|
|
54
|
+
return, or inside a plain (non-hook, non-component) function — flag as a
|
|
55
|
+
correctness bug, not a style note. A guard clause sitting above a hook
|
|
56
|
+
call is the most common shape to catch: when a component bails out early
|
|
57
|
+
(a null-prop guard, a loading check written as `return` instead of a
|
|
58
|
+
conditional in the JSX) before it reaches its `useState`/`useEffect`/
|
|
59
|
+
`useMemo`/`useCallback` calls, those hooks run on some renders and not
|
|
60
|
+
others, which breaks React's per-render hook ordering. The fix is to
|
|
61
|
+
move every hook call above the first `return`, then branch inside the
|
|
62
|
+
returned JSX or inside the hook's own callback instead.
|
|
63
|
+
- Effect dependency arrays: every reactive value read inside the effect
|
|
64
|
+
body appears in the dependency array, or the omission is deliberate and
|
|
65
|
+
safe (a ref, a setState function) — a missing dependency that reads a
|
|
66
|
+
prop/state value is a real staleness bug, not a lint nag to suppress.
|
|
67
|
+
- Stale closures: a callback (event handler, effect body, timer callback)
|
|
68
|
+
that captures a prop/state value from an earlier render and is not
|
|
69
|
+
re-created when that value changes — look especially at
|
|
70
|
+
`useCallback`/`useMemo` dependency arrays and any handler stored in a
|
|
71
|
+
ref for later use.
|
|
72
|
+
- An effect whose only job is to call `setState` from a prop/state change
|
|
73
|
+
it could instead derive during render — flag per `rules/patterns.mdc`
|
|
74
|
+
("Derive, don't sync"), since it usually signals a design bug, not a
|
|
75
|
+
missing dependency.
|
|
76
|
+
|
|
77
|
+
### Step 3: Check rendering correctness
|
|
78
|
+
|
|
79
|
+
- List `key`s: array index used as `key` on a list that can reorder,
|
|
80
|
+
filter, or have items inserted/removed — flag as a bug (broken local
|
|
81
|
+
state/focus/animation across reorders), not a style nit.
|
|
82
|
+
- Mutation: props, state, or a value derived from either mutated in place
|
|
83
|
+
(`.push`, `.sort`, direct property assignment) instead of replaced —
|
|
84
|
+
React relies on referential identity to detect changes.
|
|
85
|
+
- Re-render hot spots: a new object/array/function literal created inline
|
|
86
|
+
in a hot render path and passed to a memoized child, defeating the
|
|
87
|
+
memoization; an expensive computation run on every render with no
|
|
88
|
+
memoization and no compiler to catch it.
|
|
89
|
+
|
|
90
|
+
### Step 4: Check accessibility and security
|
|
91
|
+
|
|
92
|
+
- Semantic elements over generic `div`/`span` with a click handler
|
|
93
|
+
(`<button>` for actions, proper heading levels, form labels associated
|
|
94
|
+
via `htmlFor`/`id` or wrapping).
|
|
95
|
+
- Focus management: a modal/dialog that traps and restores focus, a route
|
|
96
|
+
change that resets focus/announces to assistive tech where the project
|
|
97
|
+
already has a pattern for it.
|
|
98
|
+
- `dangerouslySetInnerHTML` with unsanitized input, a `javascript:`-scheme
|
|
99
|
+
URL reaching `href`/`src` from user data, or a secret read through a
|
|
100
|
+
public-env-prefixed variable inside client code — all per
|
|
101
|
+
`rules/security.mdc`.
|
|
102
|
+
|
|
103
|
+
### Step 5: Report
|
|
104
|
+
|
|
105
|
+
Findings only, grouped by severity, each with file:line, the concrete bug
|
|
106
|
+
(not a style preference), and the fix direction — never a code edit:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
CRITICAL: src/components/UserList.tsx:42 — array index used as key on a
|
|
110
|
+
filterable list; local input state will attach to the wrong row after a
|
|
111
|
+
filter change. Use item.id as the key.
|
|
112
|
+
HIGH: src/hooks/useUserSearch.ts:18 — effect reads `query` but the
|
|
113
|
+
dependency array is `[]`; search will silently use the first-render
|
|
114
|
+
query forever.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Rules
|
|
118
|
+
|
|
119
|
+
- NEVER edit code — findings only.
|
|
120
|
+
- NEVER report a plain style preference (naming, formatting) as a
|
|
121
|
+
correctness finding; route that to a style/convention reviewer instead.
|
|
122
|
+
- NEVER flag MobX observer/action/store patterns — out of scope for this
|
|
123
|
+
skill (see Scope boundary above).
|
|
124
|
+
- ALWAYS give a concrete fix direction, not just "this looks wrong."
|
|
125
|
+
|
|
126
|
+
## Red Flags
|
|
127
|
+
|
|
128
|
+
| Rationalization | Why it is wrong |
|
|
129
|
+
|---|---|
|
|
130
|
+
| "This effect's missing dependency is probably intentional, I'll skip it" | A missing dependency on a value the effect body actually reads is a staleness bug more often than an intentional omission; call it out and let the author confirm intent explicitly |
|
|
131
|
+
| "The store looks off too, I'll flag the MobX action pattern while I'm here" | Out of scope for this skill by design — flag the React-side finding and note the store needs its own store-review pass, do not blur the two |
|
|
132
|
+
| "Index-as-key is common, I won't flag it unless the list is huge" | The bug (broken identity across reorders) does not depend on list size; flag it whenever the list can reorder, filter, or splice regardless of length |
|
|
133
|
+
| "I'll just fix the dependency array myself since it's a one-line change" | This skill is read-only; even a one-line fix belongs to a fix skill or the author, reported as a finding, not applied |
|
|
134
|
+
|
|
135
|
+
## Verification
|
|
136
|
+
|
|
137
|
+
Before reporting, confirm:
|
|
138
|
+
|
|
139
|
+
- Every finding cites a specific file:line and a concrete bug, not a
|
|
140
|
+
vague concern.
|
|
141
|
+
- No finding is actually a MobX/store-pattern issue or a repository
|
|
142
|
+
convention-doc issue (those belong to other review skills).
|
|
143
|
+
- Rules of Hooks and effect-dependency findings were checked against the
|
|
144
|
+
actual reactive values read in each effect/callback body, not assumed
|
|
145
|
+
from the dependency array alone.
|
|
146
|
+
- Accessibility and security findings reference `rules/security.mdc` or a
|
|
147
|
+
concrete WCAG-relevant gap (missing label, wrong semantic element),
|
|
148
|
+
not a generic "consider accessibility" note.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"Review this pull request's React components for Rules of Hooks violations",
|
|
5
|
+
"Check this useEffect for a dependency array bug before we merge",
|
|
6
|
+
"Look at this diff for stale closures in the event handlers",
|
|
7
|
+
"Review this list render for key misuse",
|
|
8
|
+
"Check this component diff for a dangerouslySetInnerHTML XSS risk",
|
|
9
|
+
"Review this modal component for accessibility gaps"
|
|
10
|
+
],
|
|
11
|
+
"negative": [
|
|
12
|
+
"Review the MobX store actions and reactions in this diff",
|
|
13
|
+
"Review this diff against our repository's CLAUDE.md frontend conventions",
|
|
14
|
+
"Write a React Testing Library test for this component",
|
|
15
|
+
"Fix the failing react-hooks lint error in this file",
|
|
16
|
+
"Review this NestJS controller's DTO validation",
|
|
17
|
+
"Build a new component that renders a paginated user list"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"id": "flag-rules-of-hooks",
|
|
23
|
+
"prompt": "Review this snippet: `function UserPanel({ id }) { if (!id) return null; const [user, setUser] = useState(null); useEffect(() => { fetchUser(id).then(setUser); }, [id]); return <div>{user?.name}</div>; }` What's wrong?",
|
|
24
|
+
"strictness": "high",
|
|
25
|
+
"expected_behavior": [
|
|
26
|
+
{
|
|
27
|
+
"grader": "judge",
|
|
28
|
+
"rubric": "A correct review identifies that useState and useEffect are called after an early return (`if (!id) return null`), which means the hooks run on some renders and not others — a Rules of Hooks violation, not a style nit. It recommends restructuring so every hook call sits above the first return, branching inside the JSX or inside the hook's own logic instead.",
|
|
29
|
+
"pass_criteria": [
|
|
30
|
+
"Names both hooks (useState and useEffect) and states specifically that they sit after `if (!id) return null`, so on renders where id is falsy React never reaches them — a generic 'hooks are called conditionally' without pointing at this early return does not satisfy this.",
|
|
31
|
+
"Flags this as a real correctness bug (hook call order breaking React's per-render matching) rather than a style preference.",
|
|
32
|
+
"Shows or describes the concrete restructuring: hoist useState/useEffect above the `if (!id) return null` guard (or push the id-check inside the effect body), not just 'reorder things'."
|
|
33
|
+
],
|
|
34
|
+
"fail_criteria": [
|
|
35
|
+
"Says the code is fine or does not flag the early-return-before-hooks structure as a problem.",
|
|
36
|
+
"Treats the issue as a minor style nit rather than a correctness bug.",
|
|
37
|
+
"Proposes keeping the hooks reachable only conditionally (e.g. wrapping the hook call itself in a condition) instead of moving them above the early return."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"calibration": {
|
|
42
|
+
"known_right": "This violates the Rules of Hooks. `useState` and `useEffect` are called after `if (!id) return null;`, so on renders where `id` is falsy the component returns before ever reaching those hook calls, while on renders where `id` is truthy it does reach them. React relies on hooks being called in the exact same order on every render of a given component instance to match state slots correctly — a conditional early return before a hook call breaks that guarantee and can cause React to throw or silently corrupt state/effect ordering, especially once `id` toggles between falsy and truthy across renders. The fix is to move `useState` and `useEffect` above the guard clause so they always run, and move the conditional logic into the returned JSX instead: keep the two hook calls first, then do `if (!id) return null;` right before the `return <div>...`, or push the id-check into the effect body itself so the effect early-returns internally rather than the component bailing out before the hook call.",
|
|
43
|
+
"known_wrong": "This looks fine to me — the early return is just a standard loading/guard pattern, and since `id` is a prop passed in from the start, in practice the component either always has an id or never does for a given usage, so the hooks effectively always run the same way in that context. I wouldn't block this; maybe add a short comment above the guard explaining why it's there, but the hook order isn't really a concern here since React only cares about hook order within a single component's re-renders, not across different components.",
|
|
44
|
+
"vague": "This has a Rules of Hooks issue — the hooks are placed after a conditional return, which isn't allowed. You'll want to restructure it.",
|
|
45
|
+
"subtle_wrong": "Good catch potential here — technically useState/useEffect come after the early return, which is a Rules of Hooks smell. I'd flag it in a comment for awareness, but since `id` only toggles rarely in this component's actual usage (it's set once from a route param) it's unlikely to bite in practice, so I wouldn't block the PR over it — worth a follow-up cleanup ticket rather than a blocking change now."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "no-mobx-scope",
|
|
50
|
+
"prompt": "Review this component diff: it uses a MobX observer wrapping a component with a useEffect that has a missing dependency, and the store action mutates an array in place. What do you flag, and what do you leave to another reviewer?",
|
|
51
|
+
"strictness": "high",
|
|
52
|
+
"expected_behavior": [
|
|
53
|
+
{
|
|
54
|
+
"grader": "judge",
|
|
55
|
+
"rubric": "A correct review flags the missing effect dependency as a React-side correctness bug (this skill's own scope), and explicitly declines to review the MobX store's in-place array mutation, noting that MobX/store-pattern review belongs to a separate store-review skill or reviewer rather than blurring the two into one finding.",
|
|
56
|
+
"pass_criteria": [
|
|
57
|
+
"Flags the missing effect dependency as a concrete staleness bug (explains that the effect will keep using an old value from an earlier render instead of the current one), not just 'the effect is missing a dependency'.",
|
|
58
|
+
"Explicitly names MobX/store review as a separate reviewer or skill that the in-place array mutation should go to, rather than a vague 'someone else can look at that' or silently reviewing/fixing it itself."
|
|
59
|
+
],
|
|
60
|
+
"fail_criteria": [
|
|
61
|
+
"Reviews or gives fix guidance on the MobX store action's mutation pattern as if it were this skill's own finding, instead of deferring it.",
|
|
62
|
+
"Fails to flag the missing effect dependency at all."
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"calibration": {
|
|
67
|
+
"known_right": "I'll flag the useEffect: it's missing a dependency, which means it will use a stale value from an earlier render instead of reacting to the current one — that's a real staleness bug on the React side and squarely in scope here. Add the missing reactive value read inside the effect body to its dependency array (or confirm it's a stable ref/dispatch function if it's deliberately omitted). On the store action mutating an array in place — I'm noting it exists, but I'm not reviewing or fixing that pattern here. MobX observer/action/store correctness (whether in-place mutation is fine under MobX's own reactivity model, or should be an immutable replace) is out of scope for this skill; that needs a pass from the store-review skill/reviewer, since MobX has its own conventions for what's safe to mutate versus replace. Please route the store action to that review separately.",
|
|
68
|
+
"known_wrong": "Two things here. First, the missing effect dependency — add it in. Second, the store action mutating the array in place is also worth fixing: MobX generally wants actions to replace arrays immutably rather than push/splice in place so downstream computeds and reactions pick up the change reliably, so I'd rewrite that action to use `array = [...array, newItem]` (or the MobX-friendly equivalent) instead of `array.push(newItem)`. Both changes should go in before merging.",
|
|
69
|
+
"vague": "I'd flag the effect dependency issue since that's in scope here, and leave the store mutation question to whoever reviews the MobX side.",
|
|
70
|
+
"subtle_wrong": "The effect dependency looks off, so add whatever's missing there. On the array mutation — since it's a small, contained change and I already have context on it from this diff, I'll just note that MobX generally prefers immutable updates and suggest swapping the push for a spread; no need to route it to a separate MobX review for something this minor."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-implementation
|
|
3
|
+
description: "Use when building or changing a React component, hook, or form in a .tsx/.jsx file -- covers function components, deriving state instead of syncing it with effects, Suspense and error boundaries, form Actions (useActionState, useOptimistic, useTransition), ref-as-prop, and React Compiler-aware memoization."
|
|
4
|
+
triggers:
|
|
5
|
+
- "build a react component"
|
|
6
|
+
- "add a hook to this component"
|
|
7
|
+
- "convert this form to use an action"
|
|
8
|
+
- "this react component re-renders too often, memoize it or check the compiler"
|
|
9
|
+
- "add suspense boundary"
|
|
10
|
+
- "wire up useOptimistic"
|
|
11
|
+
- "when should this be a react server component vs a client component"
|
|
12
|
+
metadata:
|
|
13
|
+
origin: authored
|
|
14
|
+
category: implement
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
17
|
+
license: "MIT"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# React implementation
|
|
21
|
+
|
|
22
|
+
Build or change a React component, hook, or form in `.tsx`/`.jsx` files,
|
|
23
|
+
following current (React 19-era) practice. Scoped to React's own component
|
|
24
|
+
model — generic TypeScript/Node authoring (module structure, error
|
|
25
|
+
handling, non-component logic) is `ts-js-node`'s implementation skill, not
|
|
26
|
+
this one; see `governance/scout.json` for why this needed its own skill
|
|
27
|
+
rather than a fork.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
### Step 1: Discover the project's React conventions
|
|
32
|
+
|
|
33
|
+
1. Check the React major in use (`package.json`'s `react`/`react-dom`
|
|
34
|
+
version) and whether the React Compiler is enabled (a
|
|
35
|
+
`babel-plugin-react-compiler`/`react-compiler` dependency or babel/swc
|
|
36
|
+
config entry) — this changes whether manual memoization is wanted.
|
|
37
|
+
2. Identify the framework, if any (Next.js/Remix/plain Vite/CRA-successor)
|
|
38
|
+
— it determines whether server/client components and Actions exist as a
|
|
39
|
+
concept in this project at all.
|
|
40
|
+
3. Read 1-2 neighboring components for: function-vs-class (should always
|
|
41
|
+
be function for new code), props-typing style, state-management library
|
|
42
|
+
in use (plain hooks, MobX, Redux, Zustand, Jotai), and file/test
|
|
43
|
+
co-location.
|
|
44
|
+
4. If a state-management library is in use for this component's slice of
|
|
45
|
+
state, defer to that library's own conventions for where state lives —
|
|
46
|
+
this skill covers React's own hook/effect/render model, not a specific
|
|
47
|
+
store library's API.
|
|
48
|
+
|
|
49
|
+
### Step 2: Design the component
|
|
50
|
+
|
|
51
|
+
- Decide what is props, what is local state, and what is derived — a
|
|
52
|
+
value computable from existing props/state during render is not state
|
|
53
|
+
(`rules/patterns.mdc`, "Derive, don't sync").
|
|
54
|
+
- Decide what needs an effect: only synchronization with something outside
|
|
55
|
+
React (subscription, DOM measurement, non-React widget, browser storage).
|
|
56
|
+
A data fetch has a better home when the framework offers one
|
|
57
|
+
(a loader, a server component, a query library) — reach for a bare
|
|
58
|
+
`useEffect` fetch only when the project has none of those.
|
|
59
|
+
- For a form, decide upfront whether it needs a pending/optimistic/error
|
|
60
|
+
UI. If so, plan for `<form action={...}>` + `useActionState` (+
|
|
61
|
+
`useOptimistic` for the optimistic path) instead of manual submit-handler
|
|
62
|
+
state.
|
|
63
|
+
- For a component the caller may need to focus, measure, or imperatively
|
|
64
|
+
control, accept `ref` as an ordinary prop (React 19) rather than wrapping
|
|
65
|
+
in `forwardRef`, unless the project is still on React 18 or earlier.
|
|
66
|
+
|
|
67
|
+
### Step 3: Implement
|
|
68
|
+
|
|
69
|
+
1. Function component, typed props via `interface`/`type` (not `React.FC`).
|
|
70
|
+
2. Keep hooks unconditional and top-level (`rules/coding-style.mdc`).
|
|
71
|
+
3. Give every list item a stable, data-derived `key`.
|
|
72
|
+
4. Wrap an async read (`use(promise)` or a data-fetching boundary) with a
|
|
73
|
+
`Suspense` fallback for the pending state and an error boundary for the
|
|
74
|
+
rejected state.
|
|
75
|
+
5. In a server/client-component framework, keep data fetching and secrets
|
|
76
|
+
in server components; add `"use client"` only to the leaf that actually
|
|
77
|
+
needs state, effects, refs, or browser APIs.
|
|
78
|
+
6. Apply `rules/security.mdc` to anything touching `dangerouslySetInnerHTML`,
|
|
79
|
+
a dynamic `href`/`src`, or a public-env-prefixed variable.
|
|
80
|
+
7. Only add `memo`/`useMemo`/`useCallback` for a measured re-render cost
|
|
81
|
+
when the React Compiler is NOT enabled for this project (Step 1); when
|
|
82
|
+
it is enabled, let the compiler handle memoization.
|
|
83
|
+
|
|
84
|
+
### Step 4: Verify
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
keryx test run --changed --strict
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run the project's own type-check and lint (with the `react-hooks` plugin)
|
|
91
|
+
if `keryx test run` does not already cover them. Fix failures by changing
|
|
92
|
+
the component, not by loosening a type or disabling a hooks-lint rule.
|
|
93
|
+
|
|
94
|
+
### Step 5: Report
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Changed: src/components/UserCard.tsx
|
|
98
|
+
- converted local sync-effect to a derived value
|
|
99
|
+
- added useActionState for the save form
|
|
100
|
+
- tests: keryx test run --changed --strict passing
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Rules
|
|
104
|
+
|
|
105
|
+
- ALWAYS derive a value from existing props/state during render instead of
|
|
106
|
+
syncing it into state via an effect, when the value is computable
|
|
107
|
+
without an external round-trip.
|
|
108
|
+
- ALWAYS give hooks a stable call order — no hook inside a condition, loop,
|
|
109
|
+
or callback.
|
|
110
|
+
- NEVER use the array index as a list `key` when the list can reorder,
|
|
111
|
+
filter, or insert.
|
|
112
|
+
- NEVER pass unsanitized user-sourced HTML to `dangerouslySetInnerHTML`
|
|
113
|
+
(`rules/security.mdc`).
|
|
114
|
+
- Match the project's existing state-management library instead of
|
|
115
|
+
introducing a second one for one component.
|
|
116
|
+
|
|
117
|
+
## Red Flags
|
|
118
|
+
|
|
119
|
+
| Rationalization | Why it is wrong |
|
|
120
|
+
|---|---|
|
|
121
|
+
| "I'll add a useEffect that calls setState whenever this prop changes" | Almost always a derivable value or a `key`-based remount, not an effect — an effect that only mirrors state adds a render, a flash of stale UI, and a dependency-array bug surface |
|
|
122
|
+
| "This list is short, index as key is fine" | A short list can still reorder or have items removed; index keys corrupt component identity and local state (input focus, animation) across a reorder |
|
|
123
|
+
| "The compiler is on but I'll wrap it in useMemo anyway, can't hurt" | Manual memoization on compiled code is dead weight and can hide a case the compiler would otherwise have caught cleanly — trust the compiler when it is enabled |
|
|
124
|
+
| "I'll sanitize this HTML string somewhere upstream, not right here" | A sanitize step far from the sink is one refactor away from being silently dropped; sanitize immediately before the `dangerouslySetInnerHTML` prop is built |
|
|
125
|
+
| "I'll just cast this ref/event to any, the types are being annoying" | The types changed on purpose in recent React majors (ref-as-prop, stricter event types); route through `react-build-fix` to fix the real typing instead of casting away the check |
|
|
126
|
+
|
|
127
|
+
## Verification
|
|
128
|
+
|
|
129
|
+
Do not report the work done until all of the following hold:
|
|
130
|
+
|
|
131
|
+
- No hook is called conditionally, in a loop, or after an early return —
|
|
132
|
+
order is identical on every render.
|
|
133
|
+
- Every list `.map` render has a stable, data-derived `key`.
|
|
134
|
+
- Any effect added is synchronizing with something outside React, not
|
|
135
|
+
mirroring existing props/state into a second state value.
|
|
136
|
+
- `dangerouslySetInnerHTML`, if used, sanitizes input immediately before
|
|
137
|
+
the prop is built (`rules/security.mdc`).
|
|
138
|
+
- `keryx test run --changed --strict` (or the project's own type-check +
|
|
139
|
+
lint + test commands) exits 0.
|
|
140
|
+
- `git status` shows only the intended component/hook/test files changed.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"Implement a new React function component with useState and useEffect for the user profile page",
|
|
5
|
+
"This component has a useEffect that just copies a prop into state, can you clean it up",
|
|
6
|
+
"Add a save button to this form using useActionState with an optimistic update",
|
|
7
|
+
"Wrap this list render in a Suspense boundary with an error boundary fallback",
|
|
8
|
+
"Convert this class component to a function component with hooks",
|
|
9
|
+
"This ref prop is throwing a type error, wire it up the React 19 way without forwardRef",
|
|
10
|
+
"Add useTransition to this React component so typing in the search input does not block rendering"
|
|
11
|
+
],
|
|
12
|
+
"negative": [
|
|
13
|
+
"Write pytest fixtures for this Django view",
|
|
14
|
+
"Review this component diff for Rules of Hooks violations and stale closures",
|
|
15
|
+
"The vite build is failing with a JSX syntax error, fix the build",
|
|
16
|
+
"Upgrade our app from React 18 to React 19 and run the codemods",
|
|
17
|
+
"Write a React Testing Library test that clicks a button and checks the modal opens",
|
|
18
|
+
"Add error handling and structured logging to this Node Express route handler"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"scenarios": [
|
|
22
|
+
{
|
|
23
|
+
"id": "derive-not-sync",
|
|
24
|
+
"prompt": "This React component has a useEffect that calls setState every time the `user` prop changes, just to copy user.name into a local `displayName` state variable used only for rendering. How should I fix it?",
|
|
25
|
+
"strictness": "high",
|
|
26
|
+
"expected_behavior": [
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct answer recognizes that `displayName` is fully computable from the `user` prop and should be derived during render instead of synchronized into a second state variable via an effect. It removes the sync-effect/setState pattern and replaces it with a directly derived value (a plain expression, or useMemo if the derivation is expensive), rather than keeping the effect and just tweaking it.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"Shows or names the concrete derived replacement (a plain expression such as `user.name`, or `useMemo` for an expensive case), not just 'derive it during render' in the abstract.",
|
|
32
|
+
"Explicitly removes both the useState slot for displayName and the useEffect that set it, not just the effect alone."
|
|
33
|
+
],
|
|
34
|
+
"fail_criteria": [
|
|
35
|
+
"Keeps the useEffect that calls setState to copy the prop into state as the recommended fix, instead of eliminating the sync effect."
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"calibration": {
|
|
40
|
+
"known_right": "Drop the state variable and the effect entirely — `displayName` is just `user.name`, and any value that's fully computable from existing props/state during render shouldn't live in its own state slot, because syncing it there via an effect adds an extra render pass (the effect runs after the initial render, so there's a flash where displayName is stale/undefined) and opens up a dependency-array bug surface for no benefit. Replace it with a plain derived expression: `const displayName = user.name;` computed inline where you use it, or `const displayName = useMemo(() => computeDisplayName(user), [user])` only if the derivation is genuinely expensive (a simple property read like this isn't). Delete the `useState` for displayName and the `useEffect` that set it — there's nothing left to synchronize once the value is derived directly on every render from the prop that's already available.",
|
|
41
|
+
"known_wrong": "Keep the effect, just fix the timing — add `user.name` specifically to the dependency array instead of the whole `user` object so it only re-syncs when the name actually changes: `useEffect(() => { setDisplayName(user.name); }, [user.name]);`. That avoids extra re-renders when other parts of `user` change but the name doesn't, which should clean up the behavior you're seeing without having to restructure how displayName is stored.",
|
|
42
|
+
"vague": "Instead of syncing it with an effect, just derive displayName from the user prop directly during render.",
|
|
43
|
+
"subtle_wrong": "Keep the local displayName state, but replace the useEffect with a useMemo that recomputes it: `const displayName = useMemo(() => user.name, [user]);` then still call `setDisplayName` from a small effect that only runs when the memoized value changes. This avoids the extra render-then-sync flash on unrelated user field changes while keeping the same displayName state shape the rest of the component already relies on."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "key-as-index",
|
|
48
|
+
"prompt": "I'm rendering a filterable list of todo items with `<TodoItem key={index} .../>` inside a .map. Is that ok?",
|
|
49
|
+
"strictness": "high",
|
|
50
|
+
"expected_behavior": [
|
|
51
|
+
{
|
|
52
|
+
"grader": "judge",
|
|
53
|
+
"rubric": "A correct answer says using the array index as key is not safe for a filterable list, because filtering changes which items occupy which index, which corrupts React's identity tracking for local component state, focus, and animation across the filter change. It recommends replacing `index` with a stable, data-derived key such as the item's own `id`.",
|
|
54
|
+
"pass_criteria": [
|
|
55
|
+
"States that index-as-key is a problem specifically because this list is filterable (filtering changes which item sits at which index, so React's per-index identity tracking corrupts local state, focus, or animation across a filter change) — a generic 'index keys are bad practice' without tying it to filtering does not satisfy this.",
|
|
56
|
+
"Shows or names the concrete replacement key (the item's own stable id, e.g. `key={todo.id}`), not just 'use a stable key'."
|
|
57
|
+
],
|
|
58
|
+
"fail_criteria": [
|
|
59
|
+
"Says using the array index as key is fine/acceptable for this filterable list."
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"calibration": {
|
|
64
|
+
"known_right": "No, that's not safe here — the list is filterable, which means the mapping between index and actual todo item changes whenever the filter changes: item that was at index 2 might now be at index 0 after some items get filtered out. React uses the key to match up component instances across renders, so when the index-to-item mapping shifts, React thinks the item at a given index is 'the same' component even though it's now backing a different todo, which corrupts any local state that TodoItem holds (an open edit field, an in-progress checkbox animation, input focus) — it stays attached to the index/position instead of following the actual item. Use each todo's own stable identifier instead: `key={todo.id}` (assuming your todo objects have an id field). That keeps each TodoItem's identity tied to the actual data it represents regardless of how filtering reorders or removes items around it.",
|
|
65
|
+
"known_wrong": "That's totally fine — using the array index for key is standard practice and works well as long as you're not doing drag-and-drop reordering of the list. Since this is just a filter (items are hidden/shown, not manually reordered by the user), the index key won't cause any real problems; React will just re-render the visible items in their new positions and that's it. I wouldn't worry about switching to an id-based key unless you start seeing actual visual glitches.",
|
|
66
|
+
"vague": "Using the index as key isn't ideal for a list that can be filtered — better to use a stable key instead.",
|
|
67
|
+
"subtle_wrong": "Index-as-key is usually fine, and since this is filtering (hiding/showing items) rather than reordering them via drag-and-drop, the relative order of visible items doesn't change, so in practice you likely won't see identity bugs here. If you do notice glitches later — stale input values or animation jumps on a filtered item — that's the signal to switch to `todo.id`, but I wouldn't treat it as a must-fix up front for a simple show/hide filter."
|
|
68
|
+
},
|
|
69
|
+
"anti_patterns": [
|
|
70
|
+
"array index"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-testing
|
|
3
|
+
description: "Use when a React component's test suite needs writing, extending, or fixing with React Testing Library -- covers query-by-role/label, user-event interaction, async findBy/waitFor, network mocking at the request boundary, and act warnings, on top of the project's Vitest or Jest runner."
|
|
4
|
+
triggers:
|
|
5
|
+
- "write a test for this component"
|
|
6
|
+
- "test this react hook"
|
|
7
|
+
- "fix this act warning"
|
|
8
|
+
- "mock the api call in this component test"
|
|
9
|
+
- "add react testing library test"
|
|
10
|
+
- "write a react testing library test covering the loading and error states"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: test
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# React testing (React Testing Library)
|
|
20
|
+
|
|
21
|
+
Write, extend, or fix a React component or hook test using React Testing
|
|
22
|
+
Library on top of the project's own runner (Vitest or Jest). Scoped to
|
|
23
|
+
component-level testing through RTL's user-facing query/interaction model
|
|
24
|
+
— generic JS/TS unit testing with no component under test is
|
|
25
|
+
`ts-js-node`'s testing skill, and end-to-end browser testing (Playwright)
|
|
26
|
+
is a separate tool skill, not this one; see `governance/scout.json` for the
|
|
27
|
+
fork rationale.
|
|
28
|
+
|
|
29
|
+
## Workflow
|
|
30
|
+
|
|
31
|
+
### Step 1: Discover the project's test conventions
|
|
32
|
+
|
|
33
|
+
1. Confirm the runner (`vitest` or `jest`) from `package.json`
|
|
34
|
+
scripts/deps, and that React Testing Library
|
|
35
|
+
(`@testing-library/react`, `@testing-library/user-event`) is already a
|
|
36
|
+
dependency — do not add Enzyme or introduce a second RTL version.
|
|
37
|
+
2. Find the layout: co-located `Component.test.tsx` next to the component,
|
|
38
|
+
or a mirrored `__tests__` tree. Match whichever the project already
|
|
39
|
+
uses.
|
|
40
|
+
3. Read 1-2 neighboring component tests for: render-helper conventions (a
|
|
41
|
+
custom `render` wrapping providers), query style, and how network calls
|
|
42
|
+
are mocked (MSW handlers, a manual fetch mock, or a mocked API client
|
|
43
|
+
module).
|
|
44
|
+
|
|
45
|
+
### Step 2: Plan the states to cover
|
|
46
|
+
|
|
47
|
+
- Enumerate the component's rendered states: initial/loading, success,
|
|
48
|
+
error, empty, and each conditional branch a prop or piece of state
|
|
49
|
+
gates.
|
|
50
|
+
- For a form, cover: valid submit, validation error shown, pending state
|
|
51
|
+
during submit, and the post-submit result (including an optimistic
|
|
52
|
+
update if the component uses `useOptimistic`).
|
|
53
|
+
- For a custom hook with logic worth testing standalone, plan a
|
|
54
|
+
`renderHook` test instead of mounting a throwaway component.
|
|
55
|
+
|
|
56
|
+
### Step 3: Write
|
|
57
|
+
|
|
58
|
+
1. Query by role/label/text -- literally `getByRole(...)` (not
|
|
59
|
+
`getAllByRole` for a single element you expect exactly one of),
|
|
60
|
+
`getByLabelText`, `getByText`; use `data-testid` only when no
|
|
61
|
+
accessible query exists.
|
|
62
|
+
2. Drive interaction with `@testing-library/user-event`
|
|
63
|
+
(`userEvent.setup()` then `.click`/`.type`/`.tab`), not `fireEvent`,
|
|
64
|
+
unless the project's pinned `user-event` major cannot express it. Every
|
|
65
|
+
click/type/submit interaction in a generated test goes through
|
|
66
|
+
`userEvent`, never `fireEvent.click`/`fireEvent.change` as the
|
|
67
|
+
click/type mechanism.
|
|
68
|
+
|
|
69
|
+
A submit-and-see-an-error test looks like this:
|
|
70
|
+
```tsx
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
render(<LoginForm />);
|
|
73
|
+
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
74
|
+
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
|
|
75
|
+
```
|
|
76
|
+
3. Wait for async UI with `findBy*` or `waitFor` — never a manual
|
|
77
|
+
`setTimeout`.
|
|
78
|
+
4. Mock network calls at the request boundary (an MSW handler, or the
|
|
79
|
+
project's existing equivalent), not by mocking the component's own
|
|
80
|
+
data-fetching function — a boundary mock still exercises the real
|
|
81
|
+
fetch/parse/error path.
|
|
82
|
+
5. Assert on rendered output and accessible state (text, role, `aria-*`),
|
|
83
|
+
never on component internals, instance fields, or a snapshot of
|
|
84
|
+
private props.
|
|
85
|
+
|
|
86
|
+
### Step 4: Run and fix
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
keryx test run --changed --strict
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`src/testing/service.ts` detects the project's own configured runner — do
|
|
93
|
+
not hard-code `vitest`/`jest` flags beyond what Step 1 already found. On a
|
|
94
|
+
project with no keryx testing config, run the project's own configured
|
|
95
|
+
test command instead.
|
|
96
|
+
|
|
97
|
+
Fix failing tests (max 3 iterations) — fix the test, not the component
|
|
98
|
+
under test. An `act` warning means a state update escaped RTL's async
|
|
99
|
+
query wrapping; switch to the matching `findBy*`/`waitFor`, do not silence
|
|
100
|
+
the console.
|
|
101
|
+
|
|
102
|
+
### Step 5: Report
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Generated: src/components/UserCard.test.tsx
|
|
106
|
+
- 6 test cases (loading/success/error/empty + 2 interaction), all passing
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
- ALWAYS query the way a user or assistive technology would (role/label/
|
|
112
|
+
text) before falling back to `data-testid`.
|
|
113
|
+
- ALWAYS mock network at the request boundary, not the component's
|
|
114
|
+
internal fetch wrapper.
|
|
115
|
+
- NEVER modify the component under test to make a test pass — file it as a
|
|
116
|
+
separate implementation change (`react-implementation`) instead.
|
|
117
|
+
- NEVER replace `findBy*`/`waitFor` with a fixed `setTimeout`/`sleep`.
|
|
118
|
+
- If no RTL is configured, suggest adding it; do not add it unasked.
|
|
119
|
+
|
|
120
|
+
## Red Flags
|
|
121
|
+
|
|
122
|
+
| Rationalization | Why it is wrong |
|
|
123
|
+
|---|---|
|
|
124
|
+
| "I'll just grab it with a testid, querying by role is fiddly" | A `data-testid` fallback used by default stops the test from verifying the component is actually accessible; reach for it only when no accessible query exists |
|
|
125
|
+
| "This act warning is noisy, I'll wrap it in act() and move on" | An `act` warning usually means the test is not awaiting the right query; wrapping in `act` by hand without awaiting the real async work hides a real race instead of fixing it |
|
|
126
|
+
| "The test keeps failing on the loading state, I'll mock the component's fetch helper directly" | Mocking the internal helper skips the real request/parse/error-handling code path; mock at the network boundary (MSW or equivalent) instead |
|
|
127
|
+
| "Still red after three iterations; I'll change the component's markup to match the test" | This skill writes test files only — changing markup mid-test-authoring to force a pass is an unreviewed implementation change hiding as a test fix |
|
|
128
|
+
| "I'll snapshot the whole rendered tree, it's faster than writing assertions" | A full-tree snapshot passes on any change including regressions, and fails on cosmetic changes that carry no behavior signal — assert on the specific rendered output instead |
|
|
129
|
+
|
|
130
|
+
## Verification
|
|
131
|
+
|
|
132
|
+
Do not report the work done until all of the following hold:
|
|
133
|
+
|
|
134
|
+
- The test file sits at the project's own convention path, matching the
|
|
135
|
+
query/mocking style read in Step 1.
|
|
136
|
+
- Every state enumerated in Step 2 has a corresponding passing test, or
|
|
137
|
+
the report says why one is missing.
|
|
138
|
+
- No test reaches into component internals or asserts on a private prop.
|
|
139
|
+
- `keryx test run --changed --strict` — or the project's own discovered
|
|
140
|
+
test command — exits 0 with every generated test passing.
|
|
141
|
+
- `git status` shows only test files (and a shared test-render helper, if
|
|
142
|
+
touched) added or modified; no component source under test changed.
|