@lariocpt/flawless-react 0.0.0-stage → 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 (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +611 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,403 @@
1
+ ---
2
+ name: flawless-react
3
+ description: Generate and maintain React/Next.js/Expo test suites with flawless-react — task selection, Next.js and Expo modes, provider wrappers, MSW fixtures from an OpenAPI spec, colocated specs, route e2e, crust load pipes, the programmatic API, and how to read its findings. Use when adding component/e2e/load tests to a React codebase, when a generated flawless suite is red, or for "scaffold tests for this component" work. Deeper guidance lives in the sibling skills flawless-nextjs, flawless-unit-browser, flawless-perf-harness, flawless-perf-static, flawless-a11y, flawless-react-rules, flawless-findings, flawless-e2e, flawless-load, flawless-api-fixtures, flawless-regen, flawless-api, and flawless-expo.
4
+ ---
5
+
6
+ # flawless-react
7
+
8
+ Point it at a `.tsx` file or glob and it reads the component's props, state,
9
+ hooks, children and fetches, then writes a runnable suite. Everything it emits
10
+ is yours to edit, but most of it is REGENERATED — the suite improves, and a
11
+ frozen file never receives the improvement. Only what the tool cannot rebuild is
12
+ protected: `budgets.json`, `host/main.tsx`, `provider.fixtures.ts`,
13
+ `e2e/global-setup.ts`, `e2e/params.json`, `mocks/handlers.ts`, and colocated
14
+ specs. To keep a hand-hardened component spec, use `--colocate`; specs written
15
+ inside the out dir are rewritten.
16
+
17
+ ```bash
18
+ # one component, unit specs only
19
+ bunx @lariocpt/flawless-react src/components/Widget.tsx --tasks unit-browser --no-interactive
20
+
21
+ # a Next.js app: real fixtures, colocated specs, route e2e and load pipes
22
+ cd apps/web
23
+ bunx @lariocpt/flawless-react "src/app/dashboard/page.tsx" \
24
+ --nextjs . --api openapi.json --swagger-url=off \
25
+ --tasks unit-browser,perf-static,a11y,react-rules,e2e,load \
26
+ --colocate --no-interactive
27
+ ```
28
+
29
+ ## Which skill to load
30
+
31
+ This skill is the map. Each plane's traps live in its own skill.
32
+
33
+ | situation | skill |
34
+ | --- | --- |
35
+ | `--nextjs`: provider derivation and drops, module stubs, React pins, `package.json` merge, `.env` order | flawless-nextjs |
36
+ | unit + browser spec shapes, provider wrapper, `TODO(flawless)`, registered mocks | flawless-unit-browser |
37
+ | the runtime perf plane: `host/`, `profile/`, budgets, `--heaps`, port 5173 | flawless-perf-harness |
38
+ | the static analyser's nine `perf/*` rules, sidecars, what a clean report means | flawless-perf-static |
39
+ | accessibility findings (biome a11y on web, the six native rules) | flawless-a11y |
40
+ | the curated biome correctness/suspicious sweep | flawless-react-rules |
41
+ | triaging a findings list, REPORT.md accounting, suppressions, the counts ledger | flawless-findings |
42
+ | route e2e against a real app: login, `params.json`, the auth wall | flawless-e2e |
43
+ | crust load pipes, `LOAD_BASE`/`LOAD_AUTH`, baselines, anti-vacuous probes | flawless-load |
44
+ | `--api` / `--swagger-url`, MSW handlers, synthesised fixture bodies | flawless-api-fixtures |
45
+ | re-running over an existing suite: `--force`, `--changed`, kept vs refreshed | flawless-regen |
46
+ | `runFlawless()` / `analyse()` from a script | flawless-api |
47
+ | `--expo`: what runs, what refuses, native specs and selectors | flawless-expo |
48
+
49
+ ## Tasks
50
+
51
+ | id | what it writes | needs |
52
+ | --- | --- | --- |
53
+ | `unit-browser` | Vitest + Testing Library specs (+ Playwright browser specs when `perf-harness` is also selected) | — |
54
+ | `perf-harness` | `host/` bundle + Profiler/react-scan Playwright suite + heap snapshots | — |
55
+ | `perf-static` | `reports/<C>.perf-static.json` findings, no files to run | — |
56
+ | `a11y` | web: biome's a11y rule group; `--expo`: the tool's six native `a11y/*` rules | biome in the target project (web path only) |
57
+ | `react-rules` | biome `correctness` + `suspicious`, curated; scratch config, local `biome.json` bypassed | biome |
58
+ | `e2e` | `flawless/e2e/` route specs against a real dev server | running app + seeded data |
59
+ | `load` | `flawless/load/routes.pipes` crust load pipelines | crust to run them |
60
+
61
+ Pick tasks explicitly with `--tasks a,b`; with no args in a TTY you get a
62
+ checkbox menu.
63
+
64
+ Over an EXISTING suite, a re-run exits 0: `perf-harness` refreshes its derived
65
+ files exactly as a fresh scaffold would (write-once files untouched; TaskResult
66
+ and REPORT.md carry `status: "refreshed"`), and under `--changed` it keeps the
67
+ harness entirely — nothing written, `status: "kept"` — so the affected-test run
68
+ happens with any task selection. `--force` is only for regenerating what a
69
+ plain re-run keeps (colocated specs, e2e route specs, the provider wrapper).
70
+ See the flawless-regen skill for the full regeneration contract.
71
+
72
+ ## Next.js mode
73
+
74
+ `--nextjs [path]` detects the router, auto-selects a provider, writes
75
+ everything under `<root>/flawless/`, merges devDeps + `flawless:*` scripts into
76
+ the project's `package.json`, and appends gitignore entries.
77
+
78
+ Ignore entries are appended in **every** mode, not only under `--nextjs`. Outside
79
+ Next.js mode they go to the git root (then the nearest `package.json`, then the
80
+ out dir's parent). Until that was fixed a plain run ignored nothing it
81
+ generated, including `<out>/e2e/.auth.json` — the file the emitted global-setup
82
+ tells you to persist a logged-in storageState into.
83
+
84
+ **Providers.** App-router `layout.tsx` is usually an async server component
85
+ importing `next/font` or `next/headers` — unusable under jsdom. flawless
86
+ detects that and derives `flawless/provider.tsx` from the JSX chain that
87
+ encloses `{children}`, keeping only `"use client"` components and literal
88
+ attributes. What it leaves out is named where you will read it: a dropped
89
+ attribute as a `TODO(flawless)` comment beside the wrapper, a dropped provider
90
+ layer in a block at the top of the file with the reason and the fix. A provider
91
+ layer whose module directly imports an auth library (`@clerk/*`, `next-auth`)
92
+ is skipped rather than mounted: the unit and browser planes replace those
93
+ libraries with a signed-out stub, so a provider gating its children on a
94
+ session renders *nothing* and every spec inherits the blank screen — while the
95
+ stub already satisfies the context reads auth-consuming components make. Expect
96
+ the components under it to fail on "Could not find Convex client" or "could not
97
+ find … in the context" — that is the drop, not the component. `--force`
98
+ regenerates it, so a change to the layout's provider chain reaches an existing
99
+ suite — put the values you supply (a seeded auth state, a theme) in
100
+ `provider.fixtures.ts` beside it, which is never regenerated under any flag.
101
+ Pass `--provider <file>` to override — that is also the way to supply a context
102
+ the derived wrapper dropped, since editing `provider.tsx` does not survive
103
+ `--force`. In plain (non-Next) mode there is no derived `provider.tsx` at all:
104
+ every render is wrapped in a per-component nest derived from hook-call,
105
+ namespace-compound, transitive-requirement and dynamic-render evidence (a
106
+ dynamic `<Tag />` bound from a project hook's return owes what that hook's
107
+ values can hand out) — the app's own providers,
108
+ convention matches for bare-module hooks, and generated shims for library
109
+ contexts — and `--provider` sits outside that nest.
110
+
111
+ **API fixtures.** Static route walking only sees `app/api/**/route.ts`, which
112
+ is useless for catch-all APIs (Hono, tRPC). Pass `--api openapi.json` (or
113
+ `--swagger-url http://localhost:3000/api/docs`) and real example bodies flow
114
+ into `flawless/mocks/handlers.ts` and into each generated fetch spec. Use
115
+ `--swagger-url=off` to skip the live-spec probe.
116
+
117
+ Everything below that summary is in flawless-nextjs — module stubs and the rule
118
+ that a named one outranks a pattern one, the Testing Library pins and how a stale
119
+ range is repaired, the `package.json` merge with `pnpm.ignoredBuiltDependencies`,
120
+ the two `.env` load orders, and where gitignore entries land.
121
+
122
+ ## React 17 / 18 / 19
123
+
124
+ The app's React major (`detectAppReact` in the root `package.json`) is the
125
+ single switch. At or above 18 the host mounts through `createRoot`; below 18
126
+ it falls back to legacy `ReactDOM.render`, document roots render with
127
+ `renderToStaticMarkup`, and promise-valued props are not passed. The suite's
128
+ `react`/`react-dom` are pinned to the version the app has INSTALLED, exactly:
129
+ `detectAppReact` reads `node_modules/react` before the declared range, because a
130
+ caret is a floor and `^19.2.0` installs whatever is newest. Where the analysed
131
+ sources resolve a different copy than that walk finds — a workspace whose root
132
+ declares no react while a sibling package holds one — `appReactPin` reads the
133
+ pin off the copy the emitted `react` aliases anchor on instead, because the pin
134
+ and the alias have to name the same physical copy or one plane ends up with two
135
+ React copies. Where no manifest declares a version at all — a library whose
136
+ react is a peer, `"^18.0.0 || ^19.0.0"`, declares no version — the nearest
137
+ installed pair is used instead,
138
+ scoped so an ancestor's tooling react never out-votes a real declaration. Two
139
+ React copies is the classic red-suite cause, and a pair that merely drifts is
140
+ just as fatal — one patch off dies at load on react-dom's guard
141
+ (`Incompatible React versions`), one major off on `Cannot read properties of
142
+ undefined (reading 'ReactCurrentOwner')`. A stale or loose pin left in an
143
+ existing suite manifest is rewritten by the next run (`repairReactPin`).
144
+ Testing Library follows the app: React ≤17 gets
145
+ `@testing-library/react@^12.1.5` + `@testing-library/dom@^8.20.1` — 13/14
146
+ peer-require react ^18 and 14 hard-requires the `react-dom/client` entry that
147
+ does not exist in 17. A stale 13+ range left by an older run is repaired by
148
+ the next one (`repairRtlPin`).
149
+
150
+ Two footprints complete the merge: for pnpm apps the `package.json` merge
151
+ records `pnpm.ignoredBuiltDependencies` for the deps it introduces
152
+ (`@playwright/test`, `msw`) — pnpm ≥10 errors on an unrecorded build-script
153
+ decision, and `ERR_PNPM_IGNORED_BUILDS` killed the app's own `pnpm run dev`
154
+ before Next could boot; and the suite's Vitest config reads the app's
155
+ `.env.test.local` → `.env.example` (first value wins, env vars beat them) —
156
+ `.env.example` last because template repos ship only the example, and its
157
+ placeholders exist to satisfy module-scope construction.
158
+
159
+ ## Expo / React Native mode
160
+
161
+ `--expo [<path>]`, mutually exclusive with `--nextjs`. Reads the project's SDK major, expo-router root, React Compiler and NativeWind settings off disk, defaults the entry glob to the router root, and writes to `<expoRoot>/flawless/`.
162
+
163
+ **Every task runs under `--expo` except one, which refuses by a measured decision — and the tool says which.**
164
+
165
+ | task | under `--expo` |
166
+ |---|---|
167
+ | `unit-browser` | **runs** — jest-expo + @testing-library/react-native |
168
+ | `perf-static` | **runs** |
169
+ | `react-rules` | **runs** |
170
+ | `a11y` | **runs** — the native analyser: six `a11y/*` rules, no biome involved |
171
+ | `e2e` | **runs** — route specs against `expo start --web`; cookie auth via storageState, copy-witness anti-vacuity |
172
+ | `load` | **runs** — API-only crust pipes, harvested one hop through the app's fetch wrapper (screen → api fn → apiFetch); a 401 auth probe, not a redirect check |
173
+ | `perf-harness` | **refuses** — measured-and-declined (~0 host-mountable components on an all-route-screens app); the refusal names the recorded evidence |
174
+
175
+ A refusal is a failed `TaskResult`, not a throw: it lands in `results[]`, the `--ci` blob and `REPORT.md`. A mixed selection still runs what it can.
176
+
177
+ **Do not work around a refusal by dropping `--expo`.** The DOM suite pointed at React Native emits specs that run, pass, and touch nothing — `container.querySelector("Pressable")` matches no element, and `getByRole("button")` finds nothing because React Native has no implicit roles.
178
+
179
+ Two things worth knowing before reading a native run's output, both measured (`docs/archive/2026-08-expo-probes.md`):
180
+
181
+ - **`a11y` on native is the tool's own analyser, and its zero carries a denominator.** Biome's 29 rules are HTML/ARIA and returned zero over 31 RN files with 82 unlabelled `<Pressable>`s (3/3 on the DOM offender fixture), so the task refused until wave 5 shipped the native engine. Its summary always says how many press targets / text inputs / icons were examined — a native zero WITHOUT that denominator is a bug, not a clean bill.
182
+ - **The React Compiler changes what `perf-static` means, and the task honours it.** On a project with `experiments.reactCompiler: true`, 136 of 168 findings were the three memoisation rules — work the compiler already does. `perf-static` reads that state from the project's configs in every mode (`app.json`, `next.config.*`, a babel or recognised bundler config) and suppresses those three, naming the evidence in REPORT.md; the hook-deps, effect and `set-state-in-render` rules run either way. See the `flawless-perf-static` skill for the three states and what each one prints.
183
+
184
+ See the `flawless-expo` skill for the native spec shapes, the selector ladder, and what a skip means.
185
+
186
+ ## What the generated unit specs contain
187
+
188
+ Driven by static analysis, so they only assert what can be derived:
189
+
190
+ - the rendered root tag, when the analyser could pin it down;
191
+ - `children` and `className` passthrough, but only when the value provably
192
+ reaches the DOM — accepting a prop is not rendering it;
193
+ - for a cva/`tv` component, that every option of every key renders a **distinct**
194
+ class list. Never a specific Tailwind token: `cn()` is usually tailwind-merge,
195
+ which drops base classes a later variant conflicts with;
196
+ - literal copy the component renders unconditionally;
197
+ - one test per detected interaction, with negative assertions on every other
198
+ handler prop (a click that fires two callbacks fails);
199
+ - disclosures opened first: a control behind a closed `Dialog`/`Menu`/`Popover`
200
+ is not in the DOM, so the spec clicks its trigger — by prop label, literal
201
+ text, or its fixture child — clears the spies, then queries with `findBy*`;
202
+ static-copy assertions skip closed content outright. A root written `open`,
203
+ `defaultOpen` or `open={true}` is never gated;
204
+ - a named local handler read through: `onClick={handleConfirm}` whose
205
+ declaration calls exactly ONE prop handler directly is asserted as that prop.
206
+ Two props means the handler claims neither — canary;
207
+ - `children` handed to an `asChild`/`Slot`/`Slottable` site mounted as a
208
+ `<span>`, never bare text — Slot clones elements and a text child throws;
209
+ - id-typed props never default to `""` (`flawless-<prop>` instead) — providers
210
+ validate ids, they do not render them;
211
+ - state-derived tests: initial-state witnesses and handler→setter tests;
212
+ - one `describe` per `useEffect` fetch with **success / loading / error /
213
+ empty** cases via `server.use(...)` overrides;
214
+ - async server components get `await Component(props)` plus `vi.mock` stubs
215
+ for their non-component imports (no browser spec — they cannot mount).
216
+
217
+ Everything above describes the render the component produces **at the props the
218
+ spec passes**, not the last `return` in its body. A guard the analyser can decide
219
+ is followed; one it cannot that returns nothing falls through to the tail; one it
220
+ cannot that returns content produces no description at all.
221
+
222
+ Client specs mock an **allowlist only** — `next/navigation`, `next/headers`,
223
+ `server-only`, `next/link`, `next/font/*` and any `"use server"` module. Mocking
224
+ a `cn` util would strip every className off the markup, and mocking a UI
225
+ component would replace the element the interaction tests click.
226
+
227
+ When nothing is derivable the spec emits `test.skip` naming which preconditions
228
+ failed. **A skip with a reason is the intended output there — do not "fix" it by
229
+ deleting the assertion or by hand-writing one.** Fix the generator, or give the
230
+ component the `data-testid` / fixture the skip asks for.
231
+
232
+ Anything the analyser could not derive is a `TODO(flawless)` marker, never a
233
+ silently passing assertion. **The hardening loop is: run the suite, fill the
234
+ TODOs with real fixtures, rerun.** Regenerating later will not clobber your
235
+ work if the specs are colocated (see below).
236
+
237
+ ## Spec placement
238
+
239
+ Default is `flawless/unit/<Name>.test.tsx`. Same-named components from
240
+ different files get `<Parent>__<Name>`.
241
+
242
+ `--colocate` writes `<source-dir>/<file>.flawless.test.tsx` beside the
243
+ component instead (multi-component files get `<file>.<Component>.flawless...`).
244
+ Colocated specs are **never overwritten without `--force`** — they are the ones
245
+ you hand-harden. Run them with `bun run flawless:unit`.
246
+
247
+ If the project's own test script is a bare `bun test`, it will also collect
248
+ `*.flawless.test.tsx` and fail on vitest globals. Scope it: `"test": "bun test tests"`.
249
+
250
+ ## e2e and load
251
+
252
+ Route specs, the global-setup login and the auth wall are flawless-e2e; crust
253
+ pipes, `LOAD_BASE`/`LOAD_AUTH` and their baselines are flawless-load. The short
254
+ version:
255
+
256
+ e2e runs against the app's **real dev server** — MSW cannot intercept a server
257
+ component's direct database access, so mocking it would be a lie. Seed the
258
+ database first, fill in `flawless/e2e/global-setup.ts` (it ships a worked login
259
+ example that persists `storageState`), and pass ids for dynamic segments:
260
+
261
+ ```bash
262
+ bun run db:reset
263
+ FLAWLESS_E2E_ID=<seeded-id> bun run flawless:e2e
264
+ ```
265
+
266
+ For contract-checked runs, put crust's validation proxy in front and assert no
267
+ response violated the spec:
268
+
269
+ ```bash
270
+ crust -c 'mock-server --swagger openapi.json --port 4747 --proxy http://localhost:3001' &
271
+ FLAWLESS_E2E_BASEURL=http://localhost:4747 bun run flawless:e2e
272
+ crust -c 'GET :4747/__crust/violations | (v => v.violations.filter(x => x.direction === "response")) | assert (a => a.length === 0)'
273
+ ```
274
+
275
+ Load pipes target the analysed page routes plus the GET endpoints its
276
+ components actually fetch (mutations are excluded on purpose). Each line gates
277
+ p95 against a baseline: copy a good `last-<name>.json` to `baseline-<name>.json`
278
+ and later runs fail when p95 doubles.
279
+
280
+ Two things decide whether the numbers mean anything, and both are easy to get
281
+ wrong:
282
+
283
+ - **`LOAD_BASE` must be a production build** (`bun run build && bun run start`),
284
+ not `next dev` — the dev server compiles routes on demand, so the first
285
+ request to each measures the compiler.
286
+ - **`LOAD_AUTH` is a header line**, and for a cookie-session app that is
287
+ `cookie: session=<value>`, not a Bearer token. Prove it authenticates first:
288
+ an unauthenticated request to a dashboard route redirects to the login page,
289
+ which answers 200, so every pipe passes while measuring the login screen.
290
+
291
+ A pipe whose ids are unfilled is emitted **commented out**, above a line naming
292
+ which key in `flawless/e2e/params.json` to fill — a blank segment measures the
293
+ app's 404 handler, not the route. Fill the id and regenerate with `--force`,
294
+ which merges rather than overwrites: edited rates, hand-added flows and pipes
295
+ you commented out yourself all survive. The task summary reports live versus
296
+ commented counts, so REPORT.md does not call a gated pipe covered.
297
+
298
+ ## Reading findings
299
+
300
+ `perf-static` and the biome tasks print `file:line:col rule message` and write
301
+ `flawless/reports/*.json`; a `<Component>.flawless.json` sidecar lands next to
302
+ each analysed source (`--no-inline-summary` to disable). Its paths are relative
303
+ to the sidecar — resolve against its own directory — and its `a11y` and
304
+ `reactRules` entries name the run's single aggregate, not a per-component file.
305
+ Rules worth knowing:
306
+
307
+ - `perf/hook-missing-dependency` / `hook-extraneous-dependency` — stale-closure
308
+ and over-firing effects, deliberately conservative (skipped when deps contain
309
+ member access or spreads, so a clean report is not proof).
310
+ - `perf/unstable-child-prop` — inline object/array/arrow passed to a component
311
+ child; flagged harder when that child is `React.memo`-wrapped.
312
+ - `perf/set-state-in-render` (error) — unguarded setState during render.
313
+ - `perf/unresolved-prop` — the analyser could not synthesise a default, so the
314
+ generated spec passes `undefined`. Reported apart from the findings, bucketed per
315
+ component under REPORT.md's `## Spec quality`; fix with `--props '{"x":1}'` or by
316
+ editing the spec's `DEFAULTS`.
317
+
318
+ Triage order, what a suppression costs, and which rule belongs to which task are
319
+ the three skills flawless-perf-static, flawless-a11y and flawless-react-rules;
320
+ flawless-findings keeps the REPORT.md accounting and the counts ledger.
321
+
322
+ ## Programmatic use
323
+
324
+ ```ts
325
+ import { runFlawless } from "flawless-react";
326
+
327
+ const { results, trees, outDir } = await runFlawless({
328
+ entry: "src/components/Widget.tsx",
329
+ nextjs: ".",
330
+ tasks: ["unit-browser", "perf-static"],
331
+ colocate: true,
332
+ });
333
+ ```
334
+
335
+ It never calls `process.exit`; bad input throws `UsageError`. `analyse()`,
336
+ `analyseTree()` (the perf rules) and the scaffolders are exported too, so you
337
+ can build your own reporting on `ComponentTree` without the CLI. `--ci` is
338
+ shorthand for `--no-interactive --json`.
339
+
340
+ ## Style
341
+
342
+ Generated files are reformatted with the **host project's** biome or prettier
343
+ before the run finishes, and JSON merges preserve the file's existing
344
+ indentation — generation should produce a clean `git diff`, not a reformat of
345
+ your `package.json`.
346
+
347
+ ## Troubleshooting
348
+
349
+ - **`Cannot read properties of null (reading 'useState')`** — two React copies.
350
+ The generated vitest config pins react/react-dom to the `node_modules` that
351
+ also holds `@testing-library/react`; if you moved things, fix the alias block.
352
+ - **`window.matchMedia is not a function`** — `flawless/setup.ts` ships jsdom
353
+ polyfills; make sure your config still lists it in `setupFiles`.
354
+ - **Browser specs missing** — they need the `perf-harness` host; add that task.
355
+ - **RSC spec renders nothing** — its `vi.mock` stubs return `vi.fn()`; give
356
+ them real return values (that is the TODO).
357
+ - **Playwright has no browser** — `bunx playwright install chromium`. The
358
+ runner attempts this automatically; `FLAWLESS_SKIP_BROWSER_INSTALL=1` opts out.
359
+ - **Spec fails `ReferenceError: <name> is not defined`, host build printed
360
+ nothing** — a host resolve-plugin fall-through dropped a bare specifier's
361
+ module onto a variant without that export (`msw` named imports were the
362
+ casualty). Run `bun host/server.ts` from the suite dir to see the build;
363
+ check the plugin's `args.path` gates, not the spec.
364
+ - **Every e2e route 404s while `/` loads in a browser** — the app's own
365
+ middleware is an auth wall (`auth.protect()`); with placeholder keys every
366
+ route behind it 404s. Real credentials in `e2e/global-setup.ts`, or do not
367
+ claim those routes.
368
+ - **e2e dies on "Timed out waiting …ms from config.webServer", no spec run** —
369
+ the same wall one stage earlier. Playwright calls the server up only on a
370
+ 2xx/3xx from the readiness url, which is the app root, and retries a 404
371
+ forever (`[WebServer] GET / 404` repeating in the log). Fill the credentials,
372
+ or set `FLAWLESS_E2E_BASEURL` to a server you started yourself — the emitted
373
+ config drops its own `webServer` block for that.
374
+ - **Both Playwright configs collide** (`test.describe() called here` /
375
+ `No tests found`) — two `@playwright/test` copies (app root + suite). Run the
376
+ plane from the suite dir with its own config:
377
+ `node node_modules/@playwright/test/cli.js test --config e2e/playwright.config.ts`.
378
+ Same for vitest: `node ../node_modules/vitest/vitest.mjs run --config vitest.config.mts`.
379
+
380
+ ## Limits worth stating up front
381
+
382
+ Static analysis only, no type checker. Imported prop types resolve two hops —
383
+ including types from the repo's own **workspace packages** (`@scope/shared`,
384
+ resolved through the workspace map into the package's source, barrel
385
+ re-exports chased to the declaring file); intersections, host-element props
386
+ (`ComponentProps<"div">`), `ComponentProps<typeof LocalX>` and cva tables all
387
+ resolve. `z.infer`/`z.input` aliases resolve too, by reading the zod schema
388
+ syntactically (object/enum/primitive/array/coerce, `.optional`/`.nullable`/
389
+ `.default`/`.partial`/`.extend`; `.transform`, unions, records and dates
390
+ bail). Non-literal unions, `Omit`/`Pick`/`Partial` and anything in
391
+ `node_modules` do not — the prop lands in `unresolvedProps` and the type that
392
+ defeated it in `unresolvedTypes`.
393
+
394
+ Mocks come from a component's **own** imports, so one that renders `<Link>` only
395
+ through a child gets no `next/link` mock in its own spec.
396
+
397
+ Every run writes `flawless/REPORT.md`, which accounts for every component found:
398
+ how many can mount in the browser profiling host, how many cannot and why, and
399
+ which files yielded no component at all. Read it before quoting a coverage
400
+ number — a file count is not coverage.
401
+
402
+ A green generated suite means "nothing obviously broken", not "covered" — the
403
+ value is in the scaffolding you harden, and in the findings.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: flawless-react-rules
3
+ description: Run and act on flawless-react's react-rules task — a curated set of Biome correctness and suspicious rules (render-return, children-prop, hook order, exhaustive deps, iterable keys, duplicate JSX props, `==`, `any`) swept with a scratch config that bypasses the project's biome.json. Use when triaging `reports/react-rules.json`, when deciding whether an index key is a real bug, when the same effect is reported by both biome and `perf-static`, or when a file produces a `parse` diagnostic instead of findings. For a11y rules see flawless-a11y; for the suppression rules shared by both sweeps see flawless-findings.
4
+ ---
5
+
6
+ # flawless react-rules
7
+
8
+ ```bash
9
+ bunx @lariocpt/flawless-react "src/**/*.tsx" --tasks react-rules
10
+ ```
11
+
12
+ `react-rules` is one biome sweep with a hand-picked rule set and nothing else:
13
+ `recommended: false`, then `correctness` and `suspicious` blocks enumerated rule by
14
+ rule. The report's own note says it — *"Curated React correctness rules via Biome.
15
+ Local biome.json is bypassed."* — because the point is to answer "does this React
16
+ code have a known defect", not "does this repo agree with its own lint config".
17
+
18
+ Findings print as `file:line:col lint/<group>/<rule> message` and land in the single
19
+ aggregate `flawless/reports/react-rules.json`. biome must be resolvable (the target
20
+ package's `node_modules`, any ancestor, or `PATH`); when it is not, the task fails
21
+ naming every place it looked.
22
+
23
+ ## What is enabled, and what to do about it
24
+
25
+ Errors unless marked otherwise.
26
+
27
+ **`correctness`** — `noChildrenProp`, `noConstAssign`, `noConstantCondition`,
28
+ `noEmptyPattern`, `noInvalidUseBeforeDeclaration`, `noPrecisionLoss`,
29
+ `noRenderReturnValue`, `noVoidElementsWithChildren`, `noVoidTypeReturn`,
30
+ `useExhaustiveDependencies`, `useHookAtTopLevel`, `useIsNan`, `useJsxKeyInIterable`,
31
+ `useValidForDirection`.
32
+
33
+ **`suspicious`** — `noAssignInExpressions`, `noAsyncPromiseExecutor`,
34
+ `noCompareNegZero`, `noControlCharactersInRegex`, `noDuplicateCase`,
35
+ `noDuplicateClassMembers`, `noDuplicateJsxProps`, `noDuplicateObjectKeys`,
36
+ `noDuplicateParameters`, `noFallthroughSwitchClause`, `noGlobalIsFinite`,
37
+ `noGlobalIsNan`, `noLabelVar`, `noMisleadingCharacterClass`,
38
+ `noMisleadingInstantiator`; and as **warnings**: `noArrayIndexKey`,
39
+ `noConfusingVoidType`, `noEmptyBlockStatements`, `noExplicitAny`,
40
+ `noExtraNonNullAssertion`, `noDoubleEquals`, `noImplicitAnyLet`.
41
+
42
+ The ones that recur on a real codebase, in the order worth spending time on:
43
+
44
+ - **`useHookAtTopLevel`** — a hook behind an early return is a genuine bug: the hook
45
+ order changes between renders and React throws on the state update after. Fix by
46
+ moving the guard below the hooks or returning the placeholder from the same render
47
+ path.
48
+ - **`useExhaustiveDependencies`** — biome's resolution of the same question
49
+ `perf-static` asks as `perf/hook-missing-dependency`. The two engines do not share
50
+ an implementation, so a fix that silences one should be checked against the other
51
+ rather than assumed to cover it.
52
+ - **`noArrayIndexKey`** — real for an editable list: add a client-side id at
53
+ creation, key on it, strip the id at submit. A reasoned suppression is right for a
54
+ server-static list, and for anything where the index *is* the domain identity.
55
+ "It's only a list of labels" is not that reason.
56
+ - **`noRenderReturnValue` / `noChildrenProp` / `noVoidElementsWithChildren`** —
57
+ defects that break at runtime, not style.
58
+ - **`noDuplicateJsxProps`** — the later literal silently wins; the fix is usually
59
+ deleting the first, and reading why both were there.
60
+ - **`noExplicitAny` / `noDoubleEquals` / `noEmptyBlockStatements`** (warnings) — the
61
+ honest options are fix, or suppress with a reason, or leave it and let the ledger
62
+ count stay flat. Quietly adding `// biome-ignore` to reach zero is the one option
63
+ that makes the codebase worse.
64
+
65
+ ## A parse failure is not a clean file
66
+
67
+ Biome reports `parse/*` diagnostics for a file it cannot read, and a category filter
68
+ would drop them — so every sweep would call an unreadable file clean. The runner
69
+ lets a parse diagnostic through the filter on purpose: **a file the analyser could
70
+ not read is never a pass.** Triage one like a finding: usually syntax biome's version
71
+ does not know, sometimes a file that should not have been in the sweep at all.
72
+
73
+ Diagnostics for files outside the swept set are dropped, so a sweep never claims
74
+ findings in code it was not asked about.
75
+
76
+ ## Suppression hygiene
77
+
78
+ `suppressions/*` diagnostics — biome's own "this suppression is unused" complaints —
79
+ are **dropped from both sweeps**, because the sweep runs a scratch config with only
80
+ its own rules and would otherwise punish you for answering a finding belonging to
81
+ your project's config. Your project's `biome ci` does not drop them. How to make the
82
+ two agree is in flawless-findings.
83
+
84
+ ## Under `--expo`
85
+
86
+ `react-rules` runs on React Native — same rules, same aggregate file. Rules that only
87
+ exist for DOM elements (`noVoidElementsWithChildren`) cannot match native JSX, so a
88
+ lower count there is expected and is not a coverage loss. See flawless-expo.
89
+
90
+ ## Never
91
+
92
+ - Never treat "not enabled in my project's biome.json" as a reason to dismiss a
93
+ finding: this sweep bypasses that config by design.
94
+ - Never bulk-suppress `noExplicitAny` to make the number move.
95
+ - Never count findings from the reports directory — use the task summary or
96
+ REPORT.md (flawless-findings).
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: flawless-regen
3
+ description: Regenerate a flawless-react suite safely — the generated/refreshed/kept lifecycle, what a re-run rewrites vs keeps, --force's merge semantics and the colocated-spec provenance stamp, --changed's affected-test narrowing, and proving a run happened via result JSON + REPORT.md rather than stdout. Use before any regeneration over an adopted suite, after upgrading the tool, or when a --changed run appears to have done nothing.
4
+ ---
5
+
6
+ # Regenerating a flawless suite
7
+
8
+ > **Expo mode adds one write-once file.** `<outDir>/jest.setup.ts` is where
9
+ > hand-written module mocks live and nothing can rebuild them from the analysis, so a
10
+ > re-run never overwrites it — delete it to regenerate. `jest.config.cjs` and every
11
+ > native spec under `unit/` ARE regenerated on each run, like the rest of the derived
12
+ > suite.
13
+
14
+ The model is regenerate-by-default: derived files track the tool and your
15
+ sources, and only what the tool cannot rebuild is protected. Know the table
16
+ before touching an adopted suite:
17
+
18
+ | Category | Files | On re-run | Under `--force` |
19
+ | --- | --- | --- | --- |
20
+ | Write-once | `budgets.json`, `host/main.tsx` (wrapper only — the `host/registry.ts` beside it regenerates), `provider.fixtures.ts`, `e2e/global-setup.ts`, `mocks/handlers.ts`, `mock/routes.ts` | kept | **kept** |
21
+ | Merge-kept | `e2e/params.json`, `load/routes.pipes` | kept | **merged** — your edits, rates, commented pipes survive |
22
+ | Colocated specs | `src/**/*.flawless.test.tsx` | kept | **stamp-checked** — untouched refreshed; edited or pre-stamp kept and listed |
23
+ | Everything else | out-dir specs, configs, generated prose, REPORT.md, sidecars | rewritten | rewritten |
24
+
25
+ ## The no-force run over an existing suite
26
+
27
+ **Exits 0.** Every task — `perf-harness` included — refreshes its derived
28
+ files, rewriting them as a fresh scaffold would (the standalone suite's
29
+ package.json is MERGED — your fields win, and in Next.js mode a
30
+ `flawless:*` script you customised is kept unless its value is one of the
31
+ tool's own spellings — kept keys are listed in REPORT.md as
32
+ `> Kept script(s): ...`, and deleting the key takes the tool's current
33
+ command); write-once files are
34
+ untouched (delete one to get a fresh copy). The perf-harness TaskResult and
35
+ REPORT.md's task row carry a status word: `generated` (fresh out dir),
36
+ `refreshed` (this case), or `kept` (`--changed`, below). Emission is
37
+ deterministic, so a same-version refresh usually diffs nothing; after a tool
38
+ upgrade expect the derived files — and the version banner on the suite's
39
+ `playwright.config.ts` first line — to move. An exit 1 means a real
40
+ failure, never the old harness refusal (that was findings FR-028, fixed).
41
+
42
+ A regeneration with a wave-3 tool also BURNS DOWN hand-work: the three
43
+ biggest `TODO(flawless)` clusters are now auto-filled where the source
44
+ supports inference — server-action stubs from the action's own literal
45
+ returns, data-dep fixtures from `Promise<T>` annotations or OpenAPI
46
+ examples, and compound parts mounted through their own file's family.
47
+ REPORT.md's hand-work table shows the drop; the exact count depends on how
48
+ much of the app's surface is inferable, so never promise a number —
49
+ markers that remain are exactly the cases inference honestly refused.
50
+
51
+ ## `--force` and the colocated stamp
52
+
53
+ Every colocated spec opens with a provenance stamp —
54
+ `// @flawless-generated sha256:<hex16> flawless-react@<version>` — hashing
55
+ everything below the line (recomputed after the run's style pass, so a
56
+ formatted spec still reads as untouched). `--force` uses it as the verdict:
57
+
58
+ - stamp matches body → **rewritten** (this is the refresh you asked for);
59
+ - stamp mismatch → you edited or reformatted it: **kept**, listed in the
60
+ summary as hand-edited;
61
+ - no stamp (generated pre-stamp, or the line was deleted) → **kept**, listed
62
+ as pre-stamp.
63
+
64
+ The escape hatch for both kept cases is the same: delete the file and re-run.
65
+ Kept files also drop out of `handWork` tallies — their discarded render's
66
+ TODOs are not your debt. Still commit (or stash) before forcing: the stamp
67
+ protects hand-edited specs, but the rewrite of untouched ones is exactly what
68
+ you're asking for, and a diff you can read beats trusting any tool.
69
+
70
+ Expect new spec files for components the analyser newly detected — their
71
+ browser specs mount through `host/registry.ts`, which the same run refreshed;
72
+ hand registrations live in `main.tsx`'s `ALL` spread and survive (see
73
+ flawless-unit-browser). A pre-split `main.tsx` (no `./registry` import)
74
+ triggers the run's migration warning instead.
75
+
76
+ ## `--changed`
77
+
78
+ `--changed` narrows only the RUN: vitest `related` walks the module graph for
79
+ unit specs, playwright gets an anchored `--grep`, e2e runs when an affected
80
+ component renders on a route. Over an existing suite the perf harness is
81
+ **kept** — the scaffold writes nothing (`status: "kept"`, summary `harness
82
+ kept (generated by flawless-react <X>, tool is <Y>)`, with X read from the
83
+ suite's own banner; pre-banner suites report "unknown version"). The affected
84
+ run is still gated on no task having failed, but a kept harness is a success,
85
+ so any task selection — the full seven included — reaches the affected tests:
86
+
87
+ ```bash
88
+ flawless-react "src/**/*.tsx" --nextjs . --tasks perf-harness,unit-browser,perf-static,a11y,react-rules,e2e,load --changed --no-interactive
89
+ # --tasks matters: bare --no-interactive defaults to perf-harness ALONE, and
90
+ # a harness-only selection has no unit/e2e suites for the affected run to run.
91
+ ```
92
+
93
+ `--changed --force` regenerates: an explicit force outranks the keep. Suite
94
+ failures reach the exit code (a failed affected suite exits 1), so
95
+ `--ci --changed` in a hook is honest.
96
+
97
+ ## Proving a run happened
98
+
99
+ Never take stdout as evidence — write evidence to files and check the disk:
100
+
101
+ - **Programmatic runs** (sandboxes, scripts): call `runFlawless`/`runCore` and
102
+ write `{ exitCode, outDir, results }` to a result file. Pass `nextjs`
103
+ relative to a cwd you set explicitly, or pass an explicit out dir — in
104
+ Next.js mode the default resolves against the target root, and a scratch
105
+ run from the wrong cwd rewrites the target's `package.json` scripts.
106
+ - REPORT.md is rewritten every run — a missing REPORT.md diff means the run
107
+ did not happen or ran somewhere else.
108
+ - `git status --porcelain` in the target must match the expected touch surface
109
+ for the run kind (tiny for no-force; `--force` touches the untouched
110
+ colocated specs, while the ones you hardened stay out of the diff).
111
+ - Another session may be working the same repos: check `git log -1` against
112
+ what you last saw before attributing a surprise diff to the tool.
113
+
114
+ ## Style adoption
115
+
116
+ Generated files are reformatted with the host project's biome or prettier
117
+ (including `.mts`/`.cts` configs), and JSON merges preserve the file's
118
+ indentation — a regeneration should produce a semantic diff, never a reformat.
119
+ If `biome ci` errors on a generated file after a regen, that is a tool bug;
120
+ report it rather than hand-formatting generated output.
121
+
122
+ ## Never
123
+
124
+ - Never `--force` an adopted suite with uncommitted work — the stamp keeps
125
+ hand-edited colocated specs, but e2e route specs and the provider wrapper
126
+ have no such guard, and a committed baseline is what makes any rewrite
127
+ reviewable.
128
+ - Never hand-delete a spec's stamp line to "claim" it — that demotes the file
129
+ to pre-stamp (kept but nagged about); the file is already yours the moment
130
+ you edit the body.
131
+ - Never read a mid-session change to the target app as proof your run
132
+ executed — only your own result file plus the REPORT.md diff counts.
133
+ - Never reach for `--force` on a plain refresh — a no-force re-run already
134
+ rewrites every derived file, and `--force` adds only the untouched-colocated
135
+ /e2e-spec rewrite. Decide whether you want that before asking for it.