@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.
Files changed (182) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +82540 -50300
  3. package/dist/core.js +28967 -18937
  4. package/package.json +2 -2
  5. package/src/gdgraph/affected-report.ts +141 -0
  6. package/src/gdgraph/build.ts +170 -23
  7. package/src/gdgraph/service.ts +6 -0
  8. package/src/gdgraph/staleness.ts +253 -45
  9. package/src/gdskills/bundled/agents/codebase-navigator.md +55 -0
  10. package/src/gdskills/bundled/agents/design-advisor.md +64 -0
  11. package/src/gdskills/bundled/agents/docs-maintainer.md +56 -0
  12. package/src/gdskills/bundled/agents/end-to-end-tester.md +56 -0
  13. package/src/gdskills/bundled/agents/error-path-auditor.md +57 -0
  14. package/src/gdskills/bundled/agents/go-build-fixer.md +52 -0
  15. package/src/gdskills/bundled/agents/go-code-auditor.md +49 -0
  16. package/src/gdskills/bundled/agents/performance-auditor.md +63 -0
  17. package/src/gdskills/bundled/agents/python-build-fixer.md +52 -0
  18. package/src/gdskills/bundled/agents/python-code-auditor.md +49 -0
  19. package/src/gdskills/bundled/agents/refactoring-steward.md +61 -0
  20. package/src/gdskills/bundled/agents/security-auditor.md +62 -0
  21. package/src/gdskills/bundled/agents/test-first-driver.md +61 -0
  22. package/src/gdskills/bundled/agents/work-planner.md +62 -0
  23. package/src/gdskills/bundled/install-manifest.json +797 -0
  24. package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
  25. package/src/gdskills/bundled/rules/core/skill-lifecycle.mdc +29 -1
  26. package/src/gdskills/bundled/rules/core/skills-storage-workflow.mdc +2 -2
  27. package/src/gdskills/bundled/skills/review/code-style-review/SKILL.md +1 -1
  28. package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
  29. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +26 -0
  30. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +75 -247
  31. package/src/gdskills/bundled/skills/review/review-orchestrator/output-contract.schema.json +19 -0
  32. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +10 -0
  33. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +5 -0
  34. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-backend.md +50 -0
  35. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-frontend.md +52 -0
  36. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/review-report.md +143 -0
  37. package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
  38. package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
  39. package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
  40. package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
  41. package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
  42. package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
  43. package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
  44. package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
  45. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
  46. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
  47. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
  48. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
  49. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
  50. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
  52. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
  53. package/src/gdskills/bundled/stacks/go/agent-refs.json +3 -0
  54. package/src/gdskills/bundled/stacks/go/governance/eval.json +1745 -0
  55. package/src/gdskills/bundled/stacks/go/governance/scout.json +31 -0
  56. package/src/gdskills/bundled/stacks/go/pack.json +41 -0
  57. package/src/gdskills/bundled/stacks/go/rules/coding-style.mdc +85 -0
  58. package/src/gdskills/bundled/stacks/go/rules/patterns.mdc +65 -0
  59. package/src/gdskills/bundled/stacks/go/rules/security.mdc +73 -0
  60. package/src/gdskills/bundled/stacks/go/rules/testing.mdc +68 -0
  61. package/src/gdskills/bundled/stacks/go/skills/go-build-fix/SKILL.md +138 -0
  62. package/src/gdskills/bundled/stacks/go/skills/go-build-fix/evals.json +75 -0
  63. package/src/gdskills/bundled/stacks/go/skills/go-code-review/SKILL.md +121 -0
  64. package/src/gdskills/bundled/stacks/go/skills/go-code-review/evals.json +72 -0
  65. package/src/gdskills/bundled/stacks/go/skills/go-implementation/SKILL.md +122 -0
  66. package/src/gdskills/bundled/stacks/go/skills/go-implementation/evals.json +76 -0
  67. package/src/gdskills/bundled/stacks/go/skills/go-testing/SKILL.md +126 -0
  68. package/src/gdskills/bundled/stacks/go/skills/go-testing/evals.json +73 -0
  69. package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
  70. package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
  71. package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
  72. package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
  73. package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
  74. package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
  75. package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
  76. package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
  77. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
  78. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
  79. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
  80. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
  81. package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
  82. package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
  83. package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
  84. package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
  85. package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
  86. package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
  87. package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
  88. package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
  89. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
  90. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
  91. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
  92. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
  93. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
  94. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
  95. package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
  96. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
  97. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
  98. package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
  99. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
  100. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
  101. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
  102. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
  103. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
  104. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
  105. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
  106. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
  107. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
  108. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
  109. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
  110. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
  111. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
  112. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
  113. package/src/gdskills/bundled/stacks/python/agent-refs.json +3 -0
  114. package/src/gdskills/bundled/stacks/python/governance/eval.json +1758 -0
  115. package/src/gdskills/bundled/stacks/python/governance/scout.json +34 -0
  116. package/src/gdskills/bundled/stacks/python/pack.json +41 -0
  117. package/src/gdskills/bundled/stacks/python/rules/coding-style.mdc +63 -0
  118. package/src/gdskills/bundled/stacks/python/rules/patterns.mdc +88 -0
  119. package/src/gdskills/bundled/stacks/python/rules/security.mdc +84 -0
  120. package/src/gdskills/bundled/stacks/python/rules/testing.mdc +77 -0
  121. package/src/gdskills/bundled/stacks/python/skills/python-build-fix/SKILL.md +144 -0
  122. package/src/gdskills/bundled/stacks/python/skills/python-build-fix/evals.json +74 -0
  123. package/src/gdskills/bundled/stacks/python/skills/python-code-review/SKILL.md +155 -0
  124. package/src/gdskills/bundled/stacks/python/skills/python-code-review/evals.json +72 -0
  125. package/src/gdskills/bundled/stacks/python/skills/python-implementation/SKILL.md +143 -0
  126. package/src/gdskills/bundled/stacks/python/skills/python-implementation/evals.json +78 -0
  127. package/src/gdskills/bundled/stacks/python/skills/python-testing/SKILL.md +132 -0
  128. package/src/gdskills/bundled/stacks/python/skills/python-testing/evals.json +73 -0
  129. package/src/gdskills/bundled/stacks/react/agent-refs.json +4 -0
  130. package/src/gdskills/bundled/stacks/react/governance/eval.json +2188 -0
  131. package/src/gdskills/bundled/stacks/react/governance/scout.json +40 -0
  132. package/src/gdskills/bundled/stacks/react/pack.json +42 -0
  133. package/src/gdskills/bundled/stacks/react/rules/coding-style.mdc +58 -0
  134. package/src/gdskills/bundled/stacks/react/rules/patterns.mdc +79 -0
  135. package/src/gdskills/bundled/stacks/react/rules/security.mdc +70 -0
  136. package/src/gdskills/bundled/stacks/react/rules/testing.mdc +60 -0
  137. package/src/gdskills/bundled/stacks/react/skills/react-build-fix/SKILL.md +139 -0
  138. package/src/gdskills/bundled/stacks/react/skills/react-build-fix/evals.json +72 -0
  139. package/src/gdskills/bundled/stacks/react/skills/react-code-review/SKILL.md +148 -0
  140. package/src/gdskills/bundled/stacks/react/skills/react-code-review/evals.json +74 -0
  141. package/src/gdskills/bundled/stacks/react/skills/react-implementation/SKILL.md +140 -0
  142. package/src/gdskills/bundled/stacks/react/skills/react-implementation/evals.json +74 -0
  143. package/src/gdskills/bundled/stacks/react/skills/react-testing/SKILL.md +142 -0
  144. package/src/gdskills/bundled/stacks/react/skills/react-testing/evals.json +83 -0
  145. package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/SKILL.md +155 -0
  146. package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/evals.json +74 -0
  147. package/src/gdskills/bundled/stacks/ts-js-node/agent-refs.json +4 -0
  148. package/src/gdskills/bundled/stacks/ts-js-node/governance/eval.json +2155 -0
  149. package/src/gdskills/bundled/stacks/ts-js-node/governance/scout.json +40 -0
  150. package/src/gdskills/bundled/stacks/ts-js-node/pack.json +41 -0
  151. package/src/gdskills/bundled/stacks/ts-js-node/rules/coding-style.mdc +73 -0
  152. package/src/gdskills/bundled/stacks/ts-js-node/rules/patterns.mdc +61 -0
  153. package/src/gdskills/bundled/stacks/ts-js-node/rules/security.mdc +71 -0
  154. package/src/gdskills/bundled/stacks/ts-js-node/rules/testing.mdc +63 -0
  155. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/SKILL.md +137 -0
  156. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/evals.json +73 -0
  157. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/SKILL.md +124 -0
  158. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/evals.json +74 -0
  159. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/SKILL.md +152 -0
  160. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/evals.json +71 -0
  161. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/SKILL.md +127 -0
  162. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/evals.json +72 -0
  163. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/SKILL.md +134 -0
  164. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/evals.json +70 -0
  165. package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
  166. package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
  167. package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
  168. package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
  169. package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
  170. package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
  171. package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
  172. package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
  173. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
  174. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
  175. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
  176. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
  177. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
  178. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
  179. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
  180. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
  181. package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
  182. 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.