@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,283 @@
1
+ ---
2
+ name: flawless-expo
3
+ description: Work with flawless-react's Expo / React Native mode — what --expo detects, which tasks run and which refuse, the jest-expo + @testing-library/react-native unit plane, renderRouter vs render, the native selector ladder and why a skip is a skip, and the version pairing that has to be exact. Use when generating or fixing native specs, when a native suite is red, or when deciding whether a refusal is a bug or the design.
4
+ ---
5
+
6
+ # flawless-expo
7
+
8
+ React Native is a **second element vocabulary**, not a Next.js variant. `<View>` is
9
+ capitalised and imported; there is no `<form>`, no implicit ARIA role, and
10
+ `className` is compiled away by babel before anything can observe it. Most of what
11
+ this skill records is the consequences of those four facts.
12
+
13
+ ## What `--expo` detects
14
+
15
+ `detectExpo(root)` reads, off disk only:
16
+
17
+ | field | from | why it matters |
18
+ |---|---|---|
19
+ | `sdkMajor` | the project's own `expo` range, else what is installed | jest-expo's major must **match** it |
20
+ | `routerRoot` | `app/` or `src/app/`, **only if** `expo-router` is a dependency | decides which files get the route template |
21
+ | `reactCompiler` | `app.json` `experiments.reactCompiler`, or `babel-plugin-react-compiler` | the three `perf-static` memo rules are suppressed, with the config that proved it named in REPORT.md |
22
+ | `nativewind` | `babel.config.js` mentioning nativewind | decides whether an unstyled host is honest to ship |
23
+ | `webPort` | the `web` then `start` script, default **8081** | Metro's port, not 3000 |
24
+
25
+ `sdkMajor` is `number | undefined` and **never guessed**. `latest`, `*`, a git url and
26
+ a `file:` specifier all produce *unknown*, and the run says so rather than substituting
27
+ a number — a jest-expo major that does not match the SDK fails deep inside
28
+ `expo-modules-core` with a message that names nothing.
29
+
30
+ ## The version pairing, which has to be exact
31
+
32
+ ```
33
+ jest-expo ~<the project's OWN sdk major>.0.0
34
+ jest ^29.7.0
35
+ @testing-library/react-native ^13.3.3 <- 13, NOT 14
36
+ react-test-renderer 19.1.0
37
+ ```
38
+
39
+ Each wrong answer fails somewhere unhelpful:
40
+
41
+ - **A bare `jest`** resolves to 30 while jest-expo 54's tooling is all `^29.2.1`, and the
42
+ mismatch surfaces as `this._moduleMocker.clearMocksOnScope is not a function` from
43
+ inside jest-runtime.
44
+ - **RNTL 14** replaced `react-test-renderer` with the `test-renderer` package, and
45
+ expo-router 6's own testing-library is built against the former.
46
+
47
+ If you are tempted to "upgrade" any of these, run the suite. That is what caught all of it.
48
+
49
+ ## Three spec shapes, chosen from the file
50
+
51
+ **Route screen** → `renderRouter` from `expo-router/testing-library`, at its own URL.
52
+ Not a preference: `useFocusEffect` reaches `useNavigation()`, which throws
53
+ `Couldn't find a navigation object` outside a navigator, and `useLocalSearchParams`
54
+ is empty. Inside a real navigator the screen is **focused**, so focus effects run and
55
+ the spec can assert loaded state instead of the loading skeleton.
56
+
57
+ **`_layout`** → its own navigator with one synthesised child. `<Tabs>` and `<Stack>`
58
+ call `useContextKey` and throw `No filename found. This is likely a bug in expo-router`
59
+ when rendered alone.
60
+
61
+ **Everything else** → RNTL `render()`.
62
+
63
+ ### The context is the OBJECT form, always
64
+
65
+ ```tsx
66
+ const CONTEXT = {
67
+ "_layout": Layout0,
68
+ "login": { default: LoginScreen },
69
+ "signup": { default: () => null }, // a stub for every literal push target
70
+ };
71
+ ```
72
+
73
+ expo-router resolves the string and `{ appDir }` forms through
74
+ `path.resolve(process.cwd(), …)`, so a spec using either breaks the moment jest runs
75
+ from the monorepo root instead of the app directory. The stub leaves are not
76
+ decoration: a push to a route the context does not know lands on expo-router's
77
+ injected `+not-found`, and the pathname assertion then fails for the wrong reason.
78
+
79
+ ### fireEvent in route specs, userEvent in component specs
80
+
81
+ `renderRouter` calls `jest.useFakeTimers()` internally. `userEvent`'s press timing
82
+ under fake timers is not something the emitter vouches for, so route specs use
83
+ `fireEvent`, which has no timers in it. Do not "upgrade" one to the other.
84
+
85
+ ## The selector ladder
86
+
87
+ ```
88
+ testID → accessibilityLabel → accessibilityRole (+name) → placeholder
89
+ → displayValue → text → text fragment → undefined
90
+ ```
91
+
92
+ `placeholder` sits **above** text deliberately: a placeholder names *the element*,
93
+ text names a *child*.
94
+
95
+ **There is no last-resort rung, and that is a decision.** The DOM ladder bottoms out at
96
+ `container.querySelector("button")`; the two native candidates are both unsound.
97
+ `UNSAFE_getByType(Pressable)` compares component identity, and under NativeWind's
98
+ `jsxImportSource` the `Pressable` the source rendered went through `cssInterop`'s
99
+ wrapper while the one a spec imports is the bare component — so it matches zero
100
+ elements, and `fireEvent.press` on nothing is a green test that touched nothing.
101
+ "The first pressable in the tree" is wrong for the same reason the DOM ladder refuses
102
+ an ambiguous bare tag, only more often, because every screen is a stack of Pressables.
103
+
104
+ ### A skip is not a gap in the tool
105
+
106
+ `getByRole("button")` matches **nothing** on a bare `<Pressable>`: RNTL's `getRole()`
107
+ reads the explicit `accessibilityRole` prop, else `"text"` for `Text`, else `"none"`.
108
+ There is no implicit-role table to lean on.
109
+
110
+ So a control with no `testID`, no `accessibilityLabel`, no `accessibilityRole` and no
111
+ text under it is a `test.skip` carrying the reason **and the fix**. The icon-only
112
+ wording wins over the ambiguous one when both apply, because one `accessibilityLabel`
113
+ resolves both — and the same absence is what makes the control silent to a screen
114
+ reader. **Answer a skip by adding the label to the app, not by loosening the ladder.**
115
+
116
+ ## What a native spec does NOT assert
117
+
118
+ Three DOM assertions have no honest native equivalent and are dropped rather than faked:
119
+
120
+ - the root-tag `querySelector` — `rootTag` is a capitalised element name, and the only
121
+ RNTL equivalents couple the spec to RN internals;
122
+ - the `.flawless-cls` className-flow check — NativeWind compiles `className` into a
123
+ `style` through **babel**, so a sentinel class is unobservable in the jest-expo tree
124
+ *and* in the react-native-web DOM;
125
+ - anything asserting a specific style token, for the reason the DOM emitter refuses to
126
+ assert a Tailwind class.
127
+
128
+ ## No emitted spec touches the network
129
+
130
+ A module that resolves project-locally and performs `fetch()` is `jest.mock`ed. Before
131
+ this existed the fixture suite made live HTTP calls and crashed a jest worker with
132
+ `api_error 405` from a real server.
133
+
134
+ Each stub's shape comes from the mocked function's own `Promise<T>` annotation, resolved
135
+ through workspace barrels and zod schemas. `Promise<void>` mocks to `undefined` with no
136
+ marker — that IS its realistic return value. Only an unresolvable shape falls back to
137
+ `{}`, and only that one keeps a `TODO(flawless)`. The fallback matters: `const { items }
138
+ = await listBuildings()` against `{}` binds `items` to undefined, and the next
139
+ `buildings.length` throws into the app's own ErrorBoundary, so the screen renders
140
+ "Something went wrong" and the failure looks like the component.
141
+
142
+ Collections are emptied for the default mock, so assertions derived from the initial
143
+ render hold. The loaded state gets its own test — see below.
144
+
145
+ The mock specifier is relativised to the **spec**, not the component — `app/login.tsx`
146
+ writes `"../lib/api"`, which from `flawless/unit/` names `flawless/lib/api`.
147
+
148
+ ## The loaded state gets its own test, where that is fair
149
+
150
+ A separate test re-mocks ONE function to return a single row and asserts a synthesised
151
+ witness appears. Emitted only where three things hold, each checked against the AST:
152
+
153
+ 1. **Mounting calls it** — reachable from a `useEffect`/`useFocusEffect` callback or the
154
+ component body, and not inside a branch, a `catch`, or a JSX event handler. A mutation
155
+ called from a submit handler is not a loader; a call behind `if (isTrustee)` does not run.
156
+ 2. **What it returns is rendered as a list** — the result reaches a `useState` setter whose
157
+ state variable is mapped in JSX or handed to a list as `data`. A collection read to decide
158
+ a permission flag changes a boolean, not the screen.
159
+ 3. **The row has a witness** — a string field THIS list's row renders as a JSX child. Not an
160
+ attribute value (`key={r.id}` is not displayed), and not a field rendered elsewhere in the
161
+ file over different data.
162
+
163
+ Where any fails, the emitter writes a `test.skip` naming which. On the dogfood app 14 of 31
164
+ screens qualify and all 14 pass.
165
+
166
+ An earlier version shipped this as a blanket skip, blaming "an external store the harness
167
+ does not reach". That was a guess and it was wrong — every cause was visible in the AST.
168
+
169
+ ## `router.back()` needs a history entry
170
+
171
+ `renderRouter` mounts one screen at one URL with nothing behind it, so `GO_BACK` into an
172
+ empty stack fails with *The action 'GO_BACK' was not handled by any navigator*. A route
173
+ spec pushes its own URL once before the press. A component spec has no router to push into,
174
+ so the handler is a skip that says so.
175
+
176
+ ## Colocation is impossible under `app/`
177
+
178
+ expo-router's `require.context` matches every `.[tj]sx?` under the router root, so a
179
+ colocated `app/login.flawless.test.tsx` would **register as a route**. Native specs for
180
+ route files always go to the out dir.
181
+
182
+ Related: check the app's own `test` script. A bare runner with no path filter — the
183
+ dogfood app's was `bun test --preload ./tests/setup.ts` — sweeps the generated specs
184
+ into the project suite and runs them under a runner they were not written for. The tool
185
+ warns; scope the script (`bun test tests`).
186
+
187
+ ## The native a11y analyser (wave 5)
188
+
189
+ `a11y` runs under `--expo` since wave 5, through the tool's OWN analyser
190
+ (`src/tasks/a11yNative.ts`) rather than biome — biome structurally cannot judge RN:
191
+ all 29 of its a11y rules are HTML/ARIA, and over 31 real RN files with 82 unlabelled
192
+ `<Pressable>`s they returned zero while scoring 3/3 on the DOM `A11yOffender.tsx`.
193
+
194
+ Six rules: `a11y/unnamed-control` (provably-textless press target with no label —
195
+ fires only on icon-only or childless elements, never on dynamic children, which DO
196
+ announce at runtime), `a11y/missing-role`, `a11y/textinput-unlabelled` (a placeholder
197
+ is not a label), `a11y/icon-unhidden` (warn; a render-prop icon such as `tabBarIcon`
198
+ stands down — its owner is invisible to the file, and React Navigation's tab button
199
+ announces the title over it), `a11y/label-text-mismatch` (WCAG 2.5.3),
200
+ `a11y/invalid-role` (each role attr validated against ITS OWN union — "header" is an
201
+ accessibilityRole, "heading" is a role).
202
+
203
+ Three contracts to rely on:
204
+
205
+ - **The denominator.** The summary and `reports/a11y.json` always say how many press
206
+ targets / text inputs / icons were examined and how many rules ran. A zero without a
207
+ denominator is a bug, not a clean bill.
208
+ - **Presence-not-value stands rules down.** `accessibilityLabel={t("back")}` is recorded
209
+ as dynamic and never flagged. A missing finding on a dynamic attr is the design.
210
+ - **Skip ⇔ finding agreement.** An icon-only control with no handle is BOTH the unit
211
+ plane's `test.skip` and an `a11y/unnamed-control` finding — same retained element,
212
+ one `accessibilityLabel` retires both. Deliberate asymmetries: a `testID`-only
213
+ control is addressable to a spec but still flagged (a testID is not a name), and a
214
+ dynamic-text control is a ladder skip but NOT flagged.
215
+
216
+ ## Route e2e against Expo web (wave 6)
217
+
218
+ `e2e` runs under `--expo` since wave 6: one Playwright spec per expo-router route,
219
+ against `expo start --web`. The Next.js flavour's machinery carries over
220
+ (params.json ids, write-once global-setup with the hand-work tally, `landing`
221
+ overrides, `GET <route>` describe titles for `--changed` narrowing); the Expo facts
222
+ baked in, each measured first (`docs/archive/2026-09-expo-probes.md`):
223
+
224
+ - Metro's statuses are REAL — unknown routes 404, per-route rendered HTML — so the
225
+ status assertion keeps teeth on a dev server.
226
+ - Auth is a **cookie on the API origin** (the app stores no token on web); the setup
227
+ stub's request-context login + storageState example is the whole job. Set
228
+ `EXPO_PUBLIC_API_URL` first — Metro inlines it, and the unset fallback is whatever
229
+ the source hardcodes.
230
+ - **Two auth-wall faces, three assertions.** A client redirect fails the landing
231
+ assertion; a wall rendered IN PLACE fails the console assertion (the API's 401s)
232
+ and the **copy witness** — a third test asserting the screen's longest
233
+ analysis-proven literal renders, the claim that cannot be true of a wall.
234
+ - An app whose cold start always lands on `/login` (no session hydration) is a
235
+ `landing` override in params.json — recorded truth, never a spec edit.
236
+
237
+ ## API-only load pipes (wave 7)
238
+
239
+ `load` runs under `--expo` since wave 7, API-only — an Expo app has no
240
+ server-rendered page routes, so the pipes are the GET endpoints the screens hit.
241
+ The harvest is the new part: a React Native screen never has a bare `fetch()`
242
+ (`extractFetchCalls` finds 0 on the whole app), so the endpoint is read ONE hop
243
+ through the app's own fetch wrapper — screen calls `listBuildings()` →
244
+ `apiFetch("/api/buildings")`. The wrapper is detected structurally (its single
245
+ `fetch()` addresses one of its own parameters), never by name. A store-mediated
246
+ call, a non-literal URL, or a wrapper two modules deep is dropped — a missing
247
+ endpoint costs a pipe, a wrong one measures a URL the app never sends. Retained
248
+ at analysis time on `ComponentInfo.apiCalls` (native-only), judged at task time.
249
+
250
+ The pipe/params/env/merge machinery is the Next flavour's, reused verbatim. The
251
+ one difference: `routes.setup.ts` probes a parameterless GET endpoint and fails
252
+ on a **401**, because an API endpoint answers 401 at the same URL rather than
253
+ redirecting to a login page — so the Next flavour's redirect probe would pass on
254
+ a dead session. Mutations are excluded (a POST load-test writes junk into the DB).
255
+
256
+ ## Tasks that refuse, and why that is the design
257
+
258
+ `perf-harness`
259
+ refuses as **measured-and-declined, not deferred**: on an expo-router app the
260
+ population is route screens, which the browser host cannot mount (navigator hooks;
261
+ renderRouter is jest-bound), so the measured ceiling on the dogfood app was ~0 of
262
+ 32 components — the refusal points at `docs/archive/backlog-completed-2026-10.md` B-16 and the probes
263
+ ledger, and the revisit condition is an adopter with a real `components/` directory
264
+ of non-route components. And one runner reads differently rather than refusing:
265
+
266
+ - **`perf-static` suppresses its memo rules under the React Compiler.** On a project
267
+ with `experiments.reactCompiler: true`, 136 of 168 findings were
268
+ `perf/unstable-child-prop`, `perf/missing-memo-leaf` and
269
+ `perf/no-memoisation-with-children` — work the compiler already performs, so the
270
+ three are suppressed and REPORT.md names `app.json experiments.reactCompiler` as
271
+ the evidence. The hook-dependency, effect and `set-state-in-render` rules are
272
+ unaffected: the compiler relies on correct dependency arrays rather than
273
+ repairing wrong ones.
274
+
275
+ ## Never
276
+
277
+ - Never drop `--expo` to make a task run. It will emit DOM artefacts for a React Native
278
+ project: a suite that runs, passes and tests nothing.
279
+ - Never mock `react-native`. That is exactly why an app whose own setup does it cannot
280
+ test screens; jest-expo exists so you do not have to.
281
+ - Never add a row to `NATIVE_TAG_TO_ROLE` unless the element's own React Native source
282
+ sets the role unconditionally. A fuller table means queries that match nothing.
283
+ - Never satisfy a skip by relaxing the selector ladder. Add the `accessibilityLabel`.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: flawless-findings
3
+ description: Triage the findings flawless-react reports — REPORT.md's accounting, the reports/ JSON and sidecars, which findings are worth a commit versus a suppression, the cross-config trap that makes a `biome-ignore` read as unused, and the counts ledger across runs. Use after a run when working through a findings list, when a suppression shows up as "unused" in the project's own biome, or before quoting any coverage number from REPORT.md. For the rules themselves see flawless-perf-static, flawless-a11y and flawless-react-rules; for runtime numbers see flawless-perf-harness.
4
+ ---
5
+
6
+ # triaging flawless findings
7
+
8
+ ```bash
9
+ bunx @lariocpt/flawless-react "src/**/*.tsx" --tasks perf-static,a11y,react-rules
10
+ ```
11
+
12
+ Three tasks scaffold nothing to run and only report: `perf-static` (the tool's own
13
+ rules), `a11y` (a biome sweep on web, the tool's native analyser under `--expo`) and
14
+ `react-rules` (a biome sweep with a curated rule set). Each has its own skill for the
15
+ rule-level detail — flawless-perf-static, flawless-a11y, flawless-react-rules. This
16
+ one is for reading a run and spending effort in the right order.
17
+
18
+ ## Where a finding lives
19
+
20
+ Findings print as `file:line:col rule message` and land in `flawless/reports/*.json`:
21
+ the two biome sweeps write **one aggregate each** (`react-rules.json`,
22
+ `a11y.json`), while `perf-static` writes **one file per component that owns
23
+ findings** (`<Component>.perf-static.json`). A `<Component>.flawless.json` sidecar
24
+ lands beside each analysed source unless `--no-inline-summary`; its paths are
25
+ relative to the sidecar, and its `a11y` and `reactRules` entries name the run's
26
+ single aggregate rather than a per-component file.
27
+
28
+ Per-component files only appear for components with findings, so **stale
29
+ per-component reports from earlier runs linger in `reports/`.** Count from the task
30
+ summary or REPORT.md, never from a glob over the directory.
31
+
32
+ ## Reading REPORT.md
33
+
34
+ Regenerated every run: it describes the **last run**, not the suite on disk.
35
+
36
+ - The components table accounts for every component in exactly one render-mode row
37
+ (client / rsc / compound-part / document-root / null), split by mountable or not.
38
+ - "Not covered, and why" names each gap. **A file count is not coverage.**
39
+ - "Imports that will not resolve" appears only on a run that wrote specs, and only
40
+ when an analysed import names a package nothing on disk answers for. It is why
41
+ those files fail at collect: **a file that never collected reports no tests.**
42
+ - The task table records what that run wrote, with a Status column saying where
43
+ perf-harness sat in the suite lifecycle (generated / refreshed / kept).
44
+ - Its hand-work table names the files only you can finish — `e2e/global-setup.ts`
45
+ while still the emitted stub, `e2e/params.json` while an id is blank.
46
+
47
+ ## Triage order
48
+
49
+ Ordered by how much the tool trusts its own finding, not by count.
50
+
51
+ 1. **`perf/set-state-in-render`** (error) — always real, fix first.
52
+ 2. **`useHookAtTopLevel`** — a hook behind an early return breaks hook order at
53
+ runtime. Same class of defect.
54
+ 3. **Hook-dependency findings** — see flawless-perf-static: `hook-extraneous-dependency`
55
+ usually means the dep is never read (remove it, or make the read real);
56
+ `hook-missing-dependency` wants `useCallback` plus the dep, not a suppression.
57
+ 4. **a11y errors** — a control that is silent to a screen reader. `useAltText`,
58
+ `useButtonType`, `missing-role`, `unnamed-control`, `textinput-unlabelled`: each
59
+ message names the one attribute that retires it.
60
+ 5. **`perf/unstable-child-prop`** — a real win when the receiving child is memoised
61
+ or heavy; leave inline arrows to plain DOM elements alone.
62
+ 6. **`noArrayIndexKey`** — real for editable lists (a client-side id at creation,
63
+ stripped at submit); a reasoned suppression for server-static lists or where the
64
+ index *is* the domain identity.
65
+ 7. **The advisory tail** — `no-memoisation-with-children`, `missing-memo-leaf`,
66
+ `noExplicitAny`, `noDoubleEquals`. Fix where profiling agrees
67
+ (`flawless-perf-harness`), or where the value is obvious. Do not chase to zero.
68
+ 8. **`perf/unresolved-prop` is not on this ladder at all** — it is not a finding and
69
+ nothing here suppresses it. It is bucketed per component under REPORT.md's
70
+ `## Spec quality`, and its fix is `--props '{"name":1}'` or that spec's `DEFAULTS`,
71
+ never a suppression.
72
+
73
+ On a project with the React Compiler on, item 5 and both memo rules in item 7 are
74
+ suppressed by `perf-static` itself: the compiler memoises props and JSX at the call
75
+ site, and REPORT.md names the config that proved the flag. That ladder then starts at
76
+ item 6 for those rules — and a zero from them is the compiler's doing, not a clean
77
+ bill. See flawless-perf-static for the three states and what each prints.
78
+
79
+ ## Suppressing honestly
80
+
81
+ A suppression is an **answer**, so it carries its reason:
82
+
83
+ ```tsx
84
+ // biome-ignore lint/a11y/noLabelWithoutControl: generic wrapper — every call site associates the control
85
+ ```
86
+
87
+ Placement matters: biome attaches the comment to the next line's diagnostic. For a
88
+ multi-line JSX element whose finding anchors on the `key=` attribute, put the comment
89
+ inside the tag directly above that attribute, not above the element.
90
+
91
+ **The cross-config trap.** The tool's sweeps run a scratch config containing only
92
+ their own rules, and your project's biome has a different set. A rule enabled in only
93
+ one of the two makes the suppression "unused" in the other. The tool's side is
94
+ handled — its sweeps drop `suppressions/*` diagnostics entirely, since reporting them
95
+ would punish you for answering another config's finding. **Your side is not:** if the
96
+ project config doesn't enable the rule you're suppressing, the project's own `biome
97
+ ci` flags the comment. The clean resolution is to enable the rule in the project
98
+ config — usually free, because the suppressed site was the only finding — so both
99
+ runs agree the suppression is legitimate, and to say that in the commit.
100
+
101
+ ## Counts, not colors
102
+
103
+ Keep a ledger across runs (e.g. perf-static 246 / a11y 1 / react-rules 10) and
104
+ explain every delta. A burn-down commit should be able to say "a11y 1→0, react-rules
105
+ 10→0" with one cause per line. **New findings after a regen are usually the analyser
106
+ seeing new components, not regressions** — check the components count in REPORT.md
107
+ before treating a rising total as a fall.
108
+
109
+ ## Never
110
+
111
+ - Never suppress without a reason clause, and never to make a batch green.
112
+ - Never chase the advisory clusters to zero — that is churn, not performance.
113
+ - Never diff the reports/ directory to count findings; stale per-component files from
114
+ earlier runs survive. Use the task summaries or REPORT.md.
115
+ - Never quote REPORT.md's file counts as coverage of a suite you have not run since
116
+ the last change — the report is the last run's.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: flawless-load
3
+ description: Run and maintain the crust load pipes flawless-react generates — routes.pipes, LOAD_BASE pointing at a production build, LOAD_AUTH as a header line, routes.setup.ts seeding LOAD_<PARAM> ids and proving the session, last-/baseline-<name>.json gating, and the anti-vacuous probes that decide whether a p95 describes a route or a login page. Use right before running `flawless:load`, when pipes report suspiciously fast p95s, when a run exits on the auth probe, or when a baseline needs arming. For route specs and the e2e login see flawless-e2e.
4
+ ---
5
+
6
+ # flawless load pipes
7
+
8
+ ```bash
9
+ bunx @lariocpt/flawless-react "src/app/**/page.tsx" --nextjs . --tasks load
10
+ ```
11
+
12
+ `load` writes `flawless/load/routes.pipes` — crust pipelines for the analysed page
13
+ routes plus the GET endpoints the page's components actually fetch (mutations are
14
+ excluded on purpose). Crust runs them; nothing in this repo does.
15
+
16
+ Measuring anything but the real, production app is fiction, and every vacuous pass
17
+ in this plane comes from forgetting that: an unauthenticated dashboard request 302s
18
+ to a login page that answers **200**, so every pipe passes.
19
+
20
+ ## Run it
21
+
22
+ ```bash
23
+ bun run build:web && PORT=3001 bunx next start & # PROD build on :3001
24
+ SESSION=$(jq -r '.cookies[] | select(.name=="session").value' flawless/e2e/.auth.json)
25
+ export LOAD_BASE=http://localhost:3001
26
+ export LOAD_AUTH="cookie: session=$SESSION"
27
+ bun run flawless:load # routes.setup.ts seeds the ids and proves the auth
28
+ ```
29
+
30
+ - **`LOAD_BASE` must be a production build.** `next dev` compiles routes on demand,
31
+ so the first request to each route measures the compiler. Check the server banner
32
+ says `next start`. If the app's `start` script has no port flag, the default :3000
33
+ may collide with something else on the box — `PORT=3001` wins.
34
+ - **`LOAD_AUTH` is a header line** — for a cookie session, `cookie:
35
+ session=<value>` taken from the e2e run's `flawless/e2e/.auth.json`, not a Bearer
36
+ token. The e2e run is the cheapest way to get a real one: see flawless-e2e.
37
+
38
+ ## `routes.setup.ts`: the module that refuses
39
+
40
+ You no longer source `env.sh`. `flawless/load/routes.setup.ts` runs first (crust
41
+ awaits a pipes file's sibling setup module), seeds every `LOAD_<PARAM>` from
42
+ `flawless/e2e/params.json`, and throws if it seeded none. The shell version needed
43
+ `jq` and, without it, exported nothing *silently* while 2/3 of the pipes measured
44
+ the 404 handler. The module has one failure mode and it is loud.
45
+
46
+ The same module probes every static route and **throws if one lands somewhere
47
+ else** — an expired cookie redirects to a login page that answers 200, so `expect
48
+ 200` passes and the whole file reports healthy percentiles for a login form.
49
+ Measured, before the probe existed: 20/20 status 200, p50 19.7ms, exit 0, every
50
+ request served /login. A by-design redirect goes under `landing` in params.json.
51
+
52
+ The module is **write-once** — replace it with a check that knows your app and
53
+ nothing will overwrite yours.
54
+
55
+ ## The probes, before you trust a number
56
+
57
+ A load pipe carries no landing assertion of its own (an e2e spec does) and crust
58
+ will happily report a fast, green p95 for 500 redirects.
59
+
60
+ ```bash
61
+ echo "$LOAD_ID" # must be non-empty
62
+ curl -s -H "$LOAD_AUTH" "$LOAD_BASE/dashboard/buildings/$LOAD_ID" | grep -ci "login"
63
+ ```
64
+
65
+ The curl must return real page HTML (tens of KB) and the grep must find no login
66
+ form. The same trap wears a second face **after** the run: a pipe whose p95 is
67
+ suspiciously fast is measuring the 404 handler or a redirect, not the route. Real
68
+ SSR routes on a dev box sit around 10–15 ms p95, not 2 ms.
69
+
70
+ ## Baselines
71
+
72
+ Each pipe gates p95 against `baseline-<name>.json`; a missing baseline is a pass by
73
+ design — the first run arms it. Arm one by copying a good `last-<name>.json`; after
74
+ that, a run fails when p95 doubles.
75
+
76
+ Refresh a baseline only when you can explain the shift in the commit body, and never
77
+ refresh a batch of them to make a red run green.
78
+
79
+ ## Regenerating
80
+
81
+ `--force` **merges** `flawless/load/routes.pipes` rather than overwriting: edited
82
+ rates, hand-added flows, and pipes you commented out yourself all survive. A pipe
83
+ whose ids are unfilled is emitted **commented out**, above a line naming which key
84
+ in `flawless/e2e/params.json` to fill — a blank segment measures the app's 404
85
+ handler. The task summary reports live versus commented counts, so REPORT.md cannot
86
+ call a gated pipe covered.
87
+
88
+ ## Under `--expo`
89
+
90
+ `load` RUNS under `--expo` since wave 7 — **API pipes only**. An Expo app has no
91
+ server-rendered page routes, so the pipes are the GET endpoints the screens hit,
92
+ harvested ONE hop through the app's fetch wrapper (screen → `listBuildings()` →
93
+ `apiFetch("/api/buildings")`; a bare `fetch()` never exists in a screen, so the
94
+ direct harvest finds 0).
95
+
96
+ - `LOAD_BASE` points at the **API**, not Metro — Metro serves the UI, not the
97
+ endpoints.
98
+ - `LOAD_AUTH` is a bearer or cookie header for that API.
99
+ - The write-once `routes.setup.ts` here probes a parameterless GET endpoint and
100
+ fails on a **401**, not on a redirect: an API 401 keeps the URL, so the
101
+ page-route redirect probe would pass vacuously on a dead session.
102
+
103
+ Mutations are excluded by default here too. See flawless-expo for the native plane's
104
+ other differences.
105
+
106
+ ## Never
107
+
108
+ - Never run pipes against `next dev`, or without the curl probe.
109
+ - Never read "N pipes pass" as meaning anything until `$LOAD_ID` was proven
110
+ non-empty and the probe returned authed content.
111
+ - Never refresh a baseline without explaining the shift where the next reader will
112
+ find it.