@lariocpt/flawless-react 0.0.0-stage → 0.3.0

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 +608 -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,113 @@
1
+ ---
2
+ name: flawless-a11y
3
+ description: Run and act on flawless-react's a11y task — biome's a11y rule group over a web tree, or the tool's own React Native analyser (six `a11y/*` rules with a denominator) under `--expo`, both writing one aggregate `reports/a11y.json`. Use when triaging accessibility findings, when the native run reports zero and you need to know whether it looked, when biome is not found, when a `biome-ignore` comment is flagged as unused, or when deciding what an icon-only control needs. For the suppression rules across tasks see flawless-findings.
4
+ ---
5
+
6
+ # flawless a11y
7
+
8
+ ```bash
9
+ bunx @lariocpt/flawless-react "src/**/*.tsx" --tasks a11y
10
+ ```
11
+
12
+ One task, two engines, one aggregate report (`flawless/reports/a11y.json`). Which
13
+ engine runs is decided by `--expo`, and the two are not interchangeable: biome's
14
+ rules are HTML/ARIA, the native rules are `accessibilityLabel`/`accessibilityRole`.
15
+
16
+ ## Web: a biome sweep
17
+
18
+ The task runs biome with a **scratch config** — `recommended: false` plus only the
19
+ `a11y` group — so your project's `biome.json` is bypassed and a finding here is not
20
+ an invitation to reconfigure your lint. Findings print as
21
+ `file:line:col lint/a11y/<rule> message`.
22
+
23
+ Severities are mixed and mean what they say: `useAltText`, `useButtonType`,
24
+ `useHtmlLang`, `useAnchorContent`, `useHeadingContent`, `noRedundantRoles` are
25
+ errors; `noAutofocus`, `noLabelWithoutControl`, `noNoninteractiveTabindex`,
26
+ `noPositiveTabindex`, `noRedundantAlt`, `useFocusableInteractive` are warnings.
27
+ Errors are defects; warnings on a component that composes its label elsewhere (a
28
+ generic `Field` wrapper) are usually correct code.
29
+
30
+ **biome is a prerequisite on this path only.** The resolver looks in the target
31
+ package's `node_modules`, every ancestor (so a hoisted workspace root counts) and
32
+ `PATH`; when it finds nothing the task fails naming all of them. From the compiled
33
+ binary the message is explicit: install `@biomejs/biome` in the project you are
34
+ scanning, or put biome on `PATH`.
35
+
36
+ **And it has to be biome 2.** The sweep writes a biome 2 linter config and parses
37
+ its JSON reporter, so a candidate whose `--version` names another major is walked
38
+ past in favour of one that can read it — which is how a repo that hoists a 1.x
39
+ still gets a sweep from a 2 on `PATH` — and if none can, the task fails naming the
40
+ version and path it refused. A 1.x biome does answer, and what it prints is a
41
+ different document (`location.path` is an object, `message` an array of markup
42
+ runs), so its output cannot be read as findings. A source or npm install answers
43
+ with its own pinned 2 first and never sees the repo's biome: only the compiled
44
+ binary resolves into the scanned project.
45
+
46
+ ## Native: the analyser, and its denominator
47
+
48
+ Biome's a11y rules are all HTML/ARIA. Over 31 React Native files carrying 82
49
+ unlabelled `<Pressable>`s they returned **zero**, while the same config finds 3/3 on
50
+ this repo's own DOM offender fixture — so the native path is the tool's own engine,
51
+ six rules:
52
+
53
+ | rule | severity | what it catches |
54
+ | --- | --- | --- |
55
+ | `a11y/unnamed-control` | error | a pressable whose children are provably textless with no `accessibilityLabel` |
56
+ | `a11y/missing-role` | error | a pressable with no `accessibilityRole` — RN has no implicit roles |
57
+ | `a11y/textinput-unlabelled` | error | a `TextInput` with no label; a `placeholder` alone does not count |
58
+ | `a11y/icon-unhidden` | warn | an icon from a known vector family exposed to the a11y tree with nothing to announce |
59
+ | `a11y/label-text-mismatch` | error | an `accessibilityLabel` that does not contain the visible text |
60
+ | `a11y/invalid-role` | error | an `accessibilityRole` value RN does not know |
61
+
62
+ Two things to read off every native run:
63
+
64
+ - **The summary always carries a denominator:** `N press target(s), N text input(s),
65
+ N icon(s) examined, 6 native rules`. **A native zero without that denominator is a
66
+ bug, not a clean bill.**
67
+ - **Files in DOM dialect are not swept.** A component whose host dialect is not
68
+ native (a `react-native-web` surface, a web-only screen inside an Expo tree) is
69
+ counted, not judged: the summary appends `N dom-dialect file(s) NOT swept — run
70
+ without --expo for those`, and the report carries them under
71
+ `domDialectFilesNotSwept`.
72
+
73
+ The engine is restrained on purpose, and that restraint is why its errors are
74
+ trustworthy: `unnamed-control` fires only on the **provably** nameless case —
75
+ children that are absent or all textless. A pressable rendering `{building.name}`
76
+ announces at runtime, so label-absence there is not a finding. The same for
77
+ `dynamicLabel` / `dynamicRole`: a label built by code is not a finding.
78
+
79
+ One freebie worth knowing: RN's own `<Button>` is exempt from `missing-role`,
80
+ because it has exactly one implicit role and the analyser injects it.
81
+
82
+ ## What neither engine sees
83
+
84
+ Static a11y is name-and-role analysis. It does not check contrast, focus order,
85
+ reading order, motion, or whether a `role` is semantically right for the widget; the
86
+ web engine cannot see a label supplied by a wrapper it does not analyse, and the
87
+ native engine cannot see what a screen reader assembles at run time. Zero findings
88
+ means "no provably silent control", not "accessible".
89
+
90
+ ## Suppressing
91
+
92
+ ```tsx
93
+ // biome-ignore lint/a11y/noLabelWithoutControl: generic wrapper — every call site associates the control
94
+ ```
95
+
96
+ Biome attaches the comment to the **next line's** diagnostic; for a multi-line JSX
97
+ element whose finding anchors on an attribute, put the comment inside the tag
98
+ directly above that attribute. Native findings have **no suppression mechanism** —
99
+ deliberately: each message names the one prop that retires it
100
+ (`accessibilityLabel`, `accessibilityRole`, `aria-hidden`), so either add it or the
101
+ finding is wrong and belongs in an issue.
102
+
103
+ The cross-config trap that makes a biome suppression report as `unused`, and the fix
104
+ for it, is in flawless-findings.
105
+
106
+ ## Never
107
+
108
+ - Never read a native zero as a pass without its denominator.
109
+ - Never enable one of these rules in a project's biome config purely to silence the
110
+ project's `biome ci` about the suppression — enable it because the suppressed site
111
+ was the only finding, which is usually true, and say so in the commit.
112
+ - Never add `accessibilityRole` or `aria-*` to satisfy a sweep where the message's
113
+ actual ask (`accessibilityLabel`, `aria-hidden`) is the fix.
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: flawless-api
3
+ description: Use flawless-react programmatically — runFlawless/runCore options and results, exit-code and error contracts, the analyse()/ComponentTree surface for custom reporting, the tasks registry, and the --ci JSON contract. Use when scripting suite generation (CI, sandboxes, agents), building custom reports on the analysis, or embedding the tool in another workflow.
4
+ ---
5
+
6
+ # flawless programmatic API
7
+
8
+ > **`expo` is a `CoreOptions` field beside `nextjs`**, and the two are mutually
9
+ > exclusive — `runCore` throws a `UsageError` for the pair, because it is a public API a
10
+ > programmatic caller reaches without `parseArgs`. `RunContext` carries
11
+ > `expo?: ExpoContext`; ask `platformOf(ctx)` rather than testing `ctx.expo` yourself, so
12
+ > widening the rule later happens in one place. An unported task returns a **failed
13
+ > `TaskResult`**, not a throw, so a refusal is readable from `results[]` and the `--ci`
14
+ > blob.
15
+
16
+ ```ts
17
+ import { runFlawless } from "flawless-react";
18
+
19
+ const { exitCode, outDir, results, trees } = await runFlawless({
20
+ entry: "src/components/Widget.tsx", // path or glob; optional in Next.js mode
21
+ nextjs: ".", // enables Next.js mode, "." = cwd
22
+ tasks: ["unit-browser", "perf-static"],
23
+ colocate: true,
24
+ });
25
+ ```
26
+
27
+ `runFlawless` is the friendly wrapper (`entry`/`tasks`/`out`); `runCore` from
28
+ `core.ts` is the same engine with CLI-shaped names (`pattern`/`taskIds`/
29
+ `outDir`) plus `changed` and `heaps`. Everything else — `runTests`, `force`,
30
+ `report`, `log`, `quiet` — is on **both**.
31
+
32
+ Two output flags, and they answer different questions:
33
+
34
+ - **`log`** — may this run write to **stdout**? Off by default programmatically.
35
+ It gates the progress narration *and* the per-file findings listing. `--json`
36
+ turns it off because stdout must parse whole.
37
+ - **`quiet`** — should `runCore`'s own diagnostics on **stderr** be suppressed?
38
+ Off by default, so the warnings **do** speak, even when `log` is off. Pass
39
+ `quiet: true` only if you route them yourself.
40
+
41
+ One flag for both is how `--ci` became the mode that lost its warnings.
42
+ `quiet` is deliberately narrow: it silences `runCore`'s `warn()` lines, not
43
+ stderr as a whole — the suite runner's output is routed by a different flag,
44
+ and task-level `console.warn`s have no gate. Redirect fd 2 if you need silence.
45
+
46
+ ## Contracts
47
+
48
+ - **Never calls `process.exit`.** Bad input throws `UsageError` (exported from
49
+ the package and from `types.ts`); everything else is a plain error.
50
+ - **CLI exit codes:** 0 clean · 1 a task failed, a `--run-tests`/`--changed`
51
+ suite run failed (suite outcomes fold into the exit code), or a non-task
52
+ failure such as a broken Next.js emit · 2 usage error. The code alone does not
53
+ say which — `results`, `testRun` and `errors` do.
54
+ - **`--ci`** = `--no-interactive --json`: stdout is exactly one JSON blob
55
+ `{ exitCode, outDir, results, keptScripts, testRun, errors? }` and nothing
56
+ else. Progress goes away entirely; parse the whole stdout, not a line of it.
57
+ **stderr still carries diagnostics** — skipped files, an unparseable spec,
58
+ script collisions, a failed Next.js emit. Capture it: those lines are the only
59
+ account of work the run quietly dropped.
60
+ - **Never read exit 0 as "the suites passed."** Read `testRun`. The invariant:
61
+ `mode: "none"` **if and only if** zero suites executed, so `green` never
62
+ appears on a phase that ran nothing.
63
+ - `{ mode: "generated" | "affected", green }` → suites really RAN; `green` is
64
+ what folded into `exitCode`;
65
+ - `{ mode: "none", reason }` → **nothing ran**, and `reason` says which of the
66
+ three causes it was: no selected task generates a suite `--run-tests`
67
+ executes (only `perf-harness`, `unit-browser` and `e2e` do — `load` emits
68
+ pipes you run with `bun run flawless:load`), nothing was affected, or the
69
+ selected tasks plan no run for the work that was;
70
+ - absent → the run never REACHED the test-run phase. That is NOT "no run was
71
+ asked for": a request blocked by a failed task is also absent, exits 1, and
72
+ is explained by `results`. Read absent as "look elsewhere" (FR-046).
73
+ - **`errors`** carries failures belonging to no task, so `results` cannot show
74
+ them — a Next.js emit that threw sets `exitCode: 1` with every task `ok: true`.
75
+ Omitted on a clean run, so branch on its presence.
76
+ - `results` is `TaskResult[]`: `{ id, ok, outputs, findings, summary, error?,
77
+ status? }` — `ok: false` with `error` set is a real task failure.
78
+ `status` ("generated" | "refreshed" | "kept", perf-harness only for now)
79
+ says where the run sat in the suite lifecycle: a re-run over an existing
80
+ suite is ok:true + "refreshed", and a `--changed` run keeps the harness
81
+ (ok:true, "kept", outputs empty) rather than regenerating it.
82
+ - `reproCommand` semantics: REPORT.md's repro line is rebuilt from options,
83
+ relative paths only, defaults omitted — two identical runs produce the same
84
+ line byte-for-byte, so run from the project root with relative inputs if you
85
+ care about a stable REPORT.md.
86
+
87
+ ## Running from a checkout
88
+
89
+ `bun src/bin.ts <args>` and `bun src/cli.ts <args>` both work (cli.ts has an
90
+ `import.meta.main` guard), as does the compiled `bin/flawless-react`. In
91
+ sandboxed or scripted contexts still prefer the programmatic API with evidence
92
+ written to files — stdout may be captured, truncated, or lost, and the
93
+ flawless-regen skill's run-proof discipline applies.
94
+
95
+ Cwd matters in Next.js mode: `nextjs: "."` resolves against the process cwd,
96
+ and the run merges `flawless:*` scripts into THAT root's `package.json`.
97
+ `process.chdir(appRoot)` first, or pass absolute paths deliberately.
98
+ The merge never clobbers a user-customised `flawless:*` value: only the
99
+ tool's own recognised spellings upgrade; kept keys surface in
100
+ `CoreResult.keptScripts`, the `--ci` blob, and REPORT.md.
101
+
102
+ ## The analysis surface
103
+
104
+ ```ts
105
+ import { analyse } from "flawless-react";
106
+
107
+ const tree = await analyse({ entry: "src/Nav.tsx", depth: 3, includeChildren: true });
108
+ // tree: ComponentTree — entry: ComponentInfo (propsShape, variants, hooks,
109
+ // interactions, dataDeps, unresolvedProps/unresolvedTypes), children: ComponentTree[]
110
+ // A dataDep whose usage inference came up empty may carry inferredReturn
111
+ // (a fixture value from its Promise<T> annotation, provenance "annotation")
112
+ // or wrapsFetch (method+path for the emit-time OpenAPI join); a server-action
113
+ // moduleImport may carry serverActionReturns (module-side name -> literal).
114
+ ```
115
+
116
+ `analyse()` is pure static analysis — no files written — and is the right base
117
+ for custom reporting. The honesty contract worth preserving in anything you
118
+ build on it: an extraction that fails is RECORDED (`unresolvedProps`,
119
+ `unresolvedTypes`, `uncertain` on data deps), never silently empty; treat an
120
+ empty shape with empty unresolved records as a bug to report, not a component
121
+ with no props.
122
+
123
+ Tasks are a registry: `findTask(id).run(ctx)` with
124
+ `ctx: { trees, outDir, force, … }` returning a `TaskResult`. A custom task is
125
+ a new entry in `src/tasks/index.ts` honouring one rule — write findings and
126
+ outputs, throw only for genuine failure, and make `ok` mean what it says.
127
+
128
+ ## The MCP server — the same contracts for a host
129
+
130
+ `flawless-react mcp` serves this tool over the Model Context Protocol on stdio:
131
+ eight tools, and **no new API surface to learn**. `flawless_run` answers with the
132
+ child's exit line plus the same `--ci` blob documented above, so every contract in
133
+ this file — `testRun` as the only proof suites ran, `errors` belonging to no task,
134
+ `absent` meaning "look elsewhere" (FR-046) — is how to read what an agent gets
135
+ back. It spawns a `flawless-react` child per run call (a suite runner inherits
136
+ stdout; in-process would write vitest's report into a JSON-RPC frame), so nothing
137
+ is held between calls. The six `flawless_*` read tools parse in-process and write
138
+ nothing; unknown arguments are rejected, not ignored; `flawless_findings` is the
139
+ static engine only, so zero findings is not "accessible and lint-clean". List the
140
+ tools from the binary rather than from memory:
141
+
142
+ ```bash
143
+ flawless-react mcp
144
+ flawless-react mcp --help
145
+ ```
146
+
147
+ The two planes, the arguments and the honest limits: `docs/mcp.md`.
148
+
149
+ ## Never
150
+
151
+ - Never scrape the human console output — use `--ci` or the API's `results`.
152
+ - Never run Next.js mode from an arbitrary cwd without pinning it: the
153
+ package.json merge follows the resolved root, wherever that is.
154
+ - Never treat `exitCode: 1` as "the tool crashed". Two distinct causes: a
155
+ task failed (`results[].error` names it) or a `--run-tests`/`--changed`
156
+ suite run went red — in the suite case every `results[]` entry is `ok:true`
157
+ and the outcome lives in `exitCode` and `CoreResult.testRun` ({mode:
158
+ "affected"|"generated", green}), not in any error field. A re-run over an
159
+ existing suite is not a failure: it refreshes the harness and exits 0.
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: flawless-api-fixtures
3
+ description: Give flawless-react real API data — `--api` with an OpenAPI/Swagger file, `--swagger-url` against a live docs endpoint (including the automatic `--nextjs` probe and `--swagger-url=off`), the MSW handlers and `swagger.json` it writes and which of them survive a re-run, how a body is synthesised when the spec has no example, and the four fetch cases every generated spec carries. Use when generated fixtures are empty `{}`, when a component's fetch is not matched by a handler, when a spec should assert loading or empty-state copy, or when deciding between mocking in specs and running a real backend. For the programmatic API (`runFlawless`, `analyse`) see flawless-api.
4
+ ---
5
+
6
+ # flawless API fixtures
7
+
8
+ Two different things live in this repo's vocabulary and mixing them up wastes an
9
+ hour: **MSW handlers** (`flawless/mocks/`), which intercept `fetch` inside the unit
10
+ and browser planes, and the **mock server** (`flawless/mock/`), a Bun server you can
11
+ point an app at. Both are derived from the same source: your OpenAPI spec, when the
12
+ tool can get one.
13
+
14
+ ## Getting a spec
15
+
16
+ ```bash
17
+ bunx @lariocpt/flawless-react "src/app/dashboard/page.tsx" --nextjs . --api openapi.json --swagger-url=off
18
+ ```
19
+
20
+ - **`--api <file>`** — a spec on disk. The value is required and must be a path.
21
+ - **`--swagger-url <url>`** — fetch it from a running app (the docs help names
22
+ `http://localhost:3000/api/docs` from `next-swagger-doc`). A spec obtained this way
23
+ is **preferred over static AST inference**.
24
+ - In `--nextjs` mode that URL is **probed automatically**, with a 1.5 s timeout, so a
25
+ docs-enabled app needs no flag at all. The probe never throws: a non-2xx, a
26
+ non-JSON content type, or a dead port is reported and the run continues on inferred
27
+ routes. `--swagger-url` forces a different URL; `--swagger-url=off` skips the probe
28
+ — do that when the app under test is slow to boot, or when a *different* app's docs
29
+ endpoint is listening on 3000.
30
+ - **With no spec at all**, the tool walks `app/**/api/**/route.ts` and derives routes
31
+ from it — useless for a catch-all API (Hono, tRPC), which is exactly the case
32
+ `--api` exists for.
33
+
34
+ A spec obtained by either route is written **verbatim** to `flawless/swagger.json`;
35
+ without one, that file is **derived from the inferred routes and rewritten every
36
+ run**.
37
+
38
+ ## What is written, and what survives a re-run
39
+
40
+ | file | refresh behaviour |
41
+ | --- | --- |
42
+ | `flawless/swagger.json` | rewritten every run |
43
+ | `flawless/mocks/handlers.ts` | **written once, never overwritten — not by `--force` either** |
44
+ | `mocks/browser.ts`, `mocks/node.ts`, `mocks/README.md` | written once, refreshed by `--force` |
45
+ | `flawless/mock/server.ts` | only with `--api`: rewritten each run |
46
+ | `flawless/mock/routes.ts` | only with `--api`: written once (hand-tuned bodies can't be rebuilt from the spec) |
47
+
48
+ The handlers file is the one you edit. Its header says so, and it is the reason a
49
+ spec with a `{}` body can be fixed once and stay fixed.
50
+
51
+ How the three MSW entry points wire up:
52
+
53
+ - **Vitest** — `setupServer` from `mocks/node.ts`, started in `flawless/setup.ts`, so
54
+ a component's `fetch` hits your mock responses in jsdom.
55
+ - **Browser specs and the perf host** — `host/main.tsx` calls `startMockWorker()`
56
+ from `mocks/browser.ts` before mounting; a service worker intercepts page `fetch`.
57
+ - **Server components, server actions, SSR** — not covered by either. When you run
58
+ against a real Next dev server, wire your own instrumentation to import
59
+ `mocks/node.ts`. The honest reason `e2e` runs against the real app: MSW cannot
60
+ intercept a server component's direct database access, and mocking that would be a
61
+ lie (flawless-e2e).
62
+
63
+ ## Where a fixture body comes from
64
+
65
+ Per response media type, in order: `example`, then the first `examples[*].value`,
66
+ then a body **synthesised from the schema**. Synthesis is literal and worth knowing,
67
+ because it explains most empty-looking specs:
68
+
69
+ - `object` → every property, recursively; `array` → **exactly one item**;
70
+ - `string` → `""`, except `format: date-time` → `1970-01-01T00:00:00.000Z`;
71
+ - `integer`/`number` → `0`; `boolean` → `false`; `enum` → its **first** member;
72
+ - `allOf` → the branches merged into one object;
73
+ - `$ref` → resolved **only for local `#/…` refs**; a cycle or an external ref yields
74
+ `null` for that property.
75
+ - Response selection prefers a coded status; a `default` response is used last and
76
+ counted as 200.
77
+
78
+ So `{ id: 0, createdAt: "1970-01-01T00:00:00.000Z", items: [ … ] }` is not a bug — it
79
+ is a spec with no examples. The fix is `examples` in the spec, or hand-editing
80
+ `mocks/handlers.ts` (which nothing will overwrite), or both.
81
+
82
+ When a component's endpoint has no derivable body, the generated spec says so in
83
+ place:
84
+
85
+ ```
86
+ // TODO(flawless): no example found in the API spec — replace {} with a realistic fixture.
87
+ ```
88
+
89
+ ## What every fetch generates
90
+
91
+ One `describe("<Component> GET /api/items")` per detected fetch, with four cases:
92
+
93
+ 1. **success** — `server.use(http.get(URL, …))` returning `FIXTURE`, plus a call
94
+ counter so the spec can prove the request happened;
95
+ 2. **loading** — the same handler behind `delay(150)`, ending in
96
+ `TODO(flawless): assert the loading indicator while the response is pending.`;
97
+ 3. **error** — `new HttpResponse(null, { status: 500 })`, asserting the component
98
+ survives;
99
+ 4. **empty** — `FIXTURE` with **the envelope preserved and every array member
100
+ emptied** (`{ items: [] }`, not `[]`), ending in `TODO(flawless): assert the
101
+ empty-state copy.`
102
+
103
+ The loading and empty assertions are the intended hardening work: the harness proves
104
+ the state is reachable, you say what the state looks like.
105
+
106
+ Path parameters are handled before any of that. A URL hole that corresponds to a
107
+ **prop** is filled with `test-<hole>` at render time, because the prop's synthesised
108
+ default is `""` and `/api/x//y` never matches a `:param` handler — the request would
109
+ silently miss every handler in the file.
110
+
111
+ Handler matching is **param-name-agnostic**: `/api/x/{id}/y`, `/api/x/:id/y` and
112
+ `/api/x/:buildingId/y` are the same shape. So a spec that asks for
113
+ `:buildingId` is satisfied by a route declared `{id}`.
114
+
115
+ ## The mock server
116
+
117
+ With `--api` (a file, not a live probe — `--swagger-url` enriches routes and
118
+ `swagger.json` but scaffolds no server), `flawless/mock/` is scaffolded too: `bun
119
+ mock/server.ts` serves on `127.0.0.1:${MOCK_PORT:-5174}` (default 5174), compiling
120
+ OpenAPI templates (`/users/{id}`) into matchers, and answering anything it does not
121
+ know with
122
+
123
+ ```json
124
+ { "error": "not mocked", "method": "POST", "path": "/api/orders" }
125
+ ```
126
+
127
+ That 404 body is the diagnostic: a run that behaves oddly against the mock and
128
+ correctly against the real API is missing routes in `mock/routes.ts`, not broken
129
+ components. Two limits to keep in view: **`routes.ts` is never rewritten** (your
130
+ hand-tuned bodies are yours), and **the response body is static** — a path parameter
131
+ is captured but does not change the answer, so `/users/1` and `/users/2` return the
132
+ same document. Anything where the id must change the response belongs in a handler
133
+ you write, or in a real backend.
134
+
135
+ ## Never
136
+
137
+ - Never read a `{}` fixture as "the tool generated nothing": it means the spec had no
138
+ example. Add `examples`, or fill the handler.
139
+ - Never rely on MSW in the unit plane to prove what a server component renders —
140
+ server-side data access is not interceptable there.
141
+ - Never let the loading and empty-state TODOs stand while claiming the fetch path is
142
+ covered: the states are reached, not asserted.
143
+ - Never treat the mock server as contract validation. For violation reporting, put
144
+ crust's validating proxy in front of the real app (flawless-e2e).
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: flawless-e2e
3
+ description: Run and maintain flawless-react's route e2e specs against a real dev server — seeded-DB prerequisites, the global-setup login and storageState, params.json ids and `landing` overrides, FLAWLESS_E2E_BASEURL, the auth wall that 404s or serves the login page at a real path, and the landing assertion that decides whether a green e2e run tested a route. Use right before running `flawless:e2e`, when e2e fails at global setup, when every route 404s, or when a suite passes suspiciously cheaply. For load pipes and their baselines see flawless-load.
4
+ ---
5
+
6
+ # flawless route e2e
7
+
8
+ Everything here runs against the **real app**: MSW cannot intercept a server
9
+ component's direct database access, so mocking it would be a lie. That realism is
10
+ also where every vacuous pass hides — a green route spec is only worth what its
11
+ landing assertion is worth.
12
+
13
+ ```bash
14
+ bunx @lariocpt/flawless-react "src/app/**/page.tsx" --nextjs . --tasks e2e
15
+ ```
16
+
17
+ ## Prerequisites, in order
18
+
19
+ ```bash
20
+ bun run db:up && bun run db:migrate && bun run db:seed # Postgres + seed users
21
+ bun run dev:web # app on :3001
22
+ bun run flawless:e2e
23
+ ```
24
+
25
+ From the suite dir, with the plane's own config and its own copy of Playwright:
26
+
27
+ ```bash
28
+ node node_modules/@playwright/test/cli.js test --config e2e/playwright.config.ts
29
+ ```
30
+
31
+ Running it from the app root instead is how `test.describe() called here` /
32
+ `No tests found` appear: two `@playwright/test` copies (app root + suite) and the
33
+ run picks up the wrong one.
34
+
35
+ ## Signing in
36
+
37
+ `flawless/e2e/global-setup.ts` is where you sign in as the seeded user and persist
38
+ `storageState` to `e2e/.auth.json`. **What the tool emits is a stub** that writes an
39
+ empty state (so the config's `storageState` always resolves) plus a worked example.
40
+ It is write-once — never overwritten, `--force` included, because it is yours the
41
+ moment you touch it.
42
+
43
+ A filled setup should **throw if login lands back on /login**. That throw is an
44
+ anti-vacuous guard, not a bug: when it fires the seed drifted (wrong user, wrong
45
+ password, unseeded DB). Fix the seed, never the guard.
46
+
47
+ You are not relying on remembering that, though. **Every emitted route spec asserts
48
+ it landed on the route it asked for** — both the render test and the interaction
49
+ test — so an unauthenticated suite fails naming its escapes instead of quietly
50
+ testing the login screen. Until the hook is filled, REPORT.md's hand-work table
51
+ says so; the check is byte-identity against the stub, so it clears the moment you
52
+ edit the file.
53
+
54
+ **What that assertion does and does not catch**, because an overclaim here is the
55
+ same bug wearing a different hat. It catches redirects that CHANGE THE URL and have
56
+ landed by the time the spec looks — a server 3xx, and a client-side navigation that
57
+ completes before `networkidle` settles. It does **not** catch a middleware *rewrite*
58
+ that serves the login page at the requested path, a 401 body, or a redirect that
59
+ fires later than the spec waited. A 401 usually still reds the spec, but through the
60
+ console-error check, not this one.
61
+
62
+ ## The auth wall, in its three shapes
63
+
64
+ The same cause — the app's own middleware (`auth.protect()`, a Clerk/next-auth
65
+ guard) with placeholder or missing credentials — surfaces one stage earlier each
66
+ time you chase it:
67
+
68
+ 1. **`Timed out waiting …ms from config.webServer`, no spec ran.** Playwright calls
69
+ a server up only on `200 ≤ status < 404` from the readiness URL, which is the app
70
+ root; behind the wall the root 404s and Playwright retries the whole timeout
71
+ (`[WebServer] GET / 404` repeating in the log).
72
+ 2. **Every route 404s while `/` loads in a browser.** The guard is per-route and the
73
+ root is public.
74
+ 3. **Specs pass on a login page.** A rewrite keeps the path and returns 200.
75
+
76
+ There is no tool-side fix for placeholder keys. Either put real credentials in
77
+ `e2e/global-setup.ts`, or set `FLAWLESS_E2E_BASEURL` to a server you started
78
+ yourself (the emitted config drops its own `webServer` block for that — useful when
79
+ the app is already running, or when you want the wall up and provably authenticated
80
+ past), or do not claim those routes.
81
+
82
+ ## ids and landings: `flawless/e2e/params.json`
83
+
84
+ Write-once and merge-kept, so `--force` never touches what you fill in. An
85
+ unresolvable id never fabricates a URL: the spec emits a `test.skip` naming the file
86
+ to fill (`no id fixture for /posts/[id] — fill flawless/e2e/params.json`), so an
87
+ unfilled route is a visible skip, not a 404 measured as a pass. On the load side the
88
+ same hole is `routes.setup.ts`, which refuses the run instead of measuring the 404
89
+ handler — see flawless-load.
90
+
91
+ The resolver (`e2e/params.ts`) tries four things, in order:
92
+
93
+ 1. `FLAWLESS_E2E_<PARAM>` — per-parameter environment override, uppercased;
94
+ 2. `routes[<route>][<param>]` in params.json — for a route that needs a
95
+ **consistent set** (a building and one of its floors);
96
+ 3. `defaults[<param>]` — the usual case, one seeded record;
97
+ 4. `FLAWLESS_E2E_ID` — the legacy single-id escape hatch.
98
+
99
+ `landing` records a route that redirects **on purpose** (a default sub-tab, a locale
100
+ landing) so its spec asserts the real destination instead of failing forever:
101
+
102
+ ```json
103
+ { "landing": { "/dash": "/dash/overview" } }
104
+ ```
105
+
106
+ Both belong in `params.json`, not in the spec: `--force` regenerates specs and
107
+ leaves this file alone.
108
+
109
+ ## Contract-checked runs (optional)
110
+
111
+ Put crust's validating proxy in front and assert no response violated the OpenAPI
112
+ spec:
113
+
114
+ ```bash
115
+ crust -c 'mock-server --swagger openapi.json --port 4747 --proxy http://localhost:3001' &
116
+ FLAWLESS_E2E_BASEURL=http://localhost:4747 bun run flawless:e2e
117
+ crust -c 'GET :4747/__crust/violations | (v => v.violations.filter(x => x.direction === "response")) | assert (a => a.length === 0)'
118
+ ```
119
+
120
+ The filter to `direction === "response"` matters: request-direction violations are
121
+ usually the app talking to itself with fields the spec never described, and they
122
+ will drown the ones that mean the app's own output is off-contract.
123
+
124
+ ## Under `--expo`
125
+
126
+ `e2e` RUNS under `--expo` since wave 6 — expo-router routes against
127
+ `expo start --web` (Metro's webPort, default 8081), authenticated against the API
128
+ origin with a **cookie** via the setup stub's request-context login + storageState
129
+ (the app short-circuits `expo-secure-store` on web and sends `credentials:
130
+ "include"`). Three differences to know:
131
+
132
+ - Set `EXPO_PUBLIC_API_URL` before running. Metro bakes it into the bundle; unset,
133
+ the app falls back to whatever its source hardcodes — on one dogfooded LAN, a
134
+ production container.
135
+ - Metro's statuses are REAL (unknown routes 404, per-route HTML — measured in
136
+ `docs/archive/2026-09-expo-probes.md`), so a 404 is a routing bug, not harness
137
+ noise.
138
+ - Each spec carries a third test, the **copy witness** (`getByText` on the screen's
139
+ longest analysis-proven literal), because an auth wall rendered in place keeps the
140
+ URL and a clean-ish console. The witness is the claim that cannot be true of a
141
+ wall.
142
+
143
+ A cold start that always lands on the login screen is a `landing` override in
144
+ params.json, not a spec edit. See flawless-expo for the rest of the native plane.
145
+
146
+ ## Never
147
+
148
+ - Never weaken or delete global-setup's throw-on-/login guard.
149
+ - Never "fix" a params.json syntax error by deleting the file: an invalid file fails
150
+ the run with `is not valid JSON — fix or delete it and re-run` precisely because a
151
+ hand-edited file's contents are yours; repair the JSON.
152
+ - Never read "N routes pass" as meaning anything until the spec's landing assertion
153
+ was real (a filled `params.json`, a storageState that is not the empty stub).
154
+ - Never add a `landing` override to make a red spec green without checking what it
155
+ lands on: that is how a wall becomes permanent coverage of the login page.