flakelab 0.1.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 (245) hide show
  1. package/README.md +341 -0
  2. package/bin/flakelab.mjs +3 -0
  3. package/dist/artifacts/ndjson.d.ts +3 -0
  4. package/dist/artifacts/ndjson.js +16 -0
  5. package/dist/artifacts/ndjson.js.map +1 -0
  6. package/dist/bisect/archive.d.ts +1 -0
  7. package/dist/bisect/archive.js +14 -0
  8. package/dist/bisect/archive.js.map +1 -0
  9. package/dist/bisect/confidence.d.ts +7 -0
  10. package/dist/bisect/confidence.js +27 -0
  11. package/dist/bisect/confidence.js.map +1 -0
  12. package/dist/bisect/engine.d.ts +10 -0
  13. package/dist/bisect/engine.js +110 -0
  14. package/dist/bisect/engine.js.map +1 -0
  15. package/dist/bisect/git.d.ts +7 -0
  16. package/dist/bisect/git.js +56 -0
  17. package/dist/bisect/git.js.map +1 -0
  18. package/dist/bisect/schema.d.ts +82 -0
  19. package/dist/bisect/schema.js +33 -0
  20. package/dist/bisect/schema.js.map +1 -0
  21. package/dist/bisect/solari-command.d.ts +7 -0
  22. package/dist/bisect/solari-command.js +95 -0
  23. package/dist/bisect/solari-command.js.map +1 -0
  24. package/dist/bisect/solari-evaluator.d.ts +20 -0
  25. package/dist/bisect/solari-evaluator.js +310 -0
  26. package/dist/bisect/solari-evaluator.js.map +1 -0
  27. package/dist/ci/changed-tests.d.ts +7 -0
  28. package/dist/ci/changed-tests.js +104 -0
  29. package/dist/ci/changed-tests.js.map +1 -0
  30. package/dist/ci/job-summary.d.ts +11 -0
  31. package/dist/ci/job-summary.js +45 -0
  32. package/dist/ci/job-summary.js.map +1 -0
  33. package/dist/ci/select-command.d.ts +1 -0
  34. package/dist/ci/select-command.js +49 -0
  35. package/dist/ci/select-command.js.map +1 -0
  36. package/dist/ci/summary-command.d.ts +1 -0
  37. package/dist/ci/summary-command.js +46 -0
  38. package/dist/ci/summary-command.js.map +1 -0
  39. package/dist/cli-help.d.ts +2 -0
  40. package/dist/cli-help.js +47 -0
  41. package/dist/cli-help.js.map +1 -0
  42. package/dist/cli.d.ts +1 -0
  43. package/dist/cli.js +121 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/commands/bisect.d.ts +2 -0
  46. package/dist/commands/bisect.js +63 -0
  47. package/dist/commands/bisect.js.map +1 -0
  48. package/dist/commands/diagnose.d.ts +2 -0
  49. package/dist/commands/diagnose.js +27 -0
  50. package/dist/commands/diagnose.js.map +1 -0
  51. package/dist/commands/discover.d.ts +2 -0
  52. package/dist/commands/discover.js +46 -0
  53. package/dist/commands/discover.js.map +1 -0
  54. package/dist/commands/doctor.d.ts +8 -0
  55. package/dist/commands/doctor.js +78 -0
  56. package/dist/commands/doctor.js.map +1 -0
  57. package/dist/commands/investigate.d.ts +2 -0
  58. package/dist/commands/investigate.js +43 -0
  59. package/dist/commands/investigate.js.map +1 -0
  60. package/dist/commands/options.d.ts +38 -0
  61. package/dist/commands/options.js +35 -0
  62. package/dist/commands/options.js.map +1 -0
  63. package/dist/commands/prove.d.ts +2 -0
  64. package/dist/commands/prove.js +21 -0
  65. package/dist/commands/prove.js.map +1 -0
  66. package/dist/commands/repair.d.ts +2 -0
  67. package/dist/commands/repair.js +55 -0
  68. package/dist/commands/repair.js.map +1 -0
  69. package/dist/commands/replay.d.ts +2 -0
  70. package/dist/commands/replay.js +26 -0
  71. package/dist/commands/replay.js.map +1 -0
  72. package/dist/commands/report.d.ts +2 -0
  73. package/dist/commands/report.js +79 -0
  74. package/dist/commands/report.js.map +1 -0
  75. package/dist/commands/scan.d.ts +12 -0
  76. package/dist/commands/scan.js +86 -0
  77. package/dist/commands/scan.js.map +1 -0
  78. package/dist/core/plan.d.ts +3 -0
  79. package/dist/core/plan.js +17 -0
  80. package/dist/core/plan.js.map +1 -0
  81. package/dist/discovery/evaluate.d.ts +22 -0
  82. package/dist/discovery/evaluate.js +81 -0
  83. package/dist/discovery/evaluate.js.map +1 -0
  84. package/dist/discovery/minimize.d.ts +24 -0
  85. package/dist/discovery/minimize.js +73 -0
  86. package/dist/discovery/minimize.js.map +1 -0
  87. package/dist/domain/schema.d.ts +155 -0
  88. package/dist/domain/schema.js +67 -0
  89. package/dist/domain/schema.js.map +1 -0
  90. package/dist/faults/install.d.ts +4 -0
  91. package/dist/faults/install.js +9 -0
  92. package/dist/faults/install.js.map +1 -0
  93. package/dist/faults/network-delay.d.ts +5 -0
  94. package/dist/faults/network-delay.js +34 -0
  95. package/dist/faults/network-delay.js.map +1 -0
  96. package/dist/faults/request-failure.d.ts +5 -0
  97. package/dist/faults/request-failure.js +24 -0
  98. package/dist/faults/request-failure.js.map +1 -0
  99. package/dist/investigator/agent.d.ts +26 -0
  100. package/dist/investigator/agent.js +197 -0
  101. package/dist/investigator/agent.js.map +1 -0
  102. package/dist/investigator/budget.d.ts +14 -0
  103. package/dist/investigator/budget.js +39 -0
  104. package/dist/investigator/budget.js.map +1 -0
  105. package/dist/investigator/file.d.ts +2 -0
  106. package/dist/investigator/file.js +7 -0
  107. package/dist/investigator/file.js.map +1 -0
  108. package/dist/investigator/groq.d.ts +5 -0
  109. package/dist/investigator/groq.js +26 -0
  110. package/dist/investigator/groq.js.map +1 -0
  111. package/dist/investigator/ledger.d.ts +17 -0
  112. package/dist/investigator/ledger.js +121 -0
  113. package/dist/investigator/ledger.js.map +1 -0
  114. package/dist/investigator/safe-source.d.ts +6 -0
  115. package/dist/investigator/safe-source.js +79 -0
  116. package/dist/investigator/safe-source.js.map +1 -0
  117. package/dist/investigator/schema.d.ts +110 -0
  118. package/dist/investigator/schema.js +53 -0
  119. package/dist/investigator/schema.js.map +1 -0
  120. package/dist/playwright.d.ts +3 -0
  121. package/dist/playwright.js +21 -0
  122. package/dist/playwright.js.map +1 -0
  123. package/dist/providers/errors.d.ts +21 -0
  124. package/dist/providers/errors.js +159 -0
  125. package/dist/providers/errors.js.map +1 -0
  126. package/dist/repair/file.d.ts +2 -0
  127. package/dist/repair/file.js +6 -0
  128. package/dist/repair/file.js.map +1 -0
  129. package/dist/repair/generator.d.ts +12 -0
  130. package/dist/repair/generator.js +74 -0
  131. package/dist/repair/generator.js.map +1 -0
  132. package/dist/repair/policy.d.ts +2 -0
  133. package/dist/repair/policy.js +70 -0
  134. package/dist/repair/policy.js.map +1 -0
  135. package/dist/repair/remote-proof-runner.d.ts +1 -0
  136. package/dist/repair/remote-proof-runner.js +53 -0
  137. package/dist/repair/remote-proof-runner.js.map +1 -0
  138. package/dist/repair/schema.d.ts +78 -0
  139. package/dist/repair/schema.js +44 -0
  140. package/dist/repair/schema.js.map +1 -0
  141. package/dist/repair/solari-validator.d.ts +25 -0
  142. package/dist/repair/solari-validator.js +231 -0
  143. package/dist/repair/solari-validator.js.map +1 -0
  144. package/dist/repair/validator.d.ts +16 -0
  145. package/dist/repair/validator.js +81 -0
  146. package/dist/repair/validator.js.map +1 -0
  147. package/dist/repair/workspace.d.ts +7 -0
  148. package/dist/repair/workspace.js +40 -0
  149. package/dist/repair/workspace.js.map +1 -0
  150. package/dist/report/browser-entry.d.ts +2 -0
  151. package/dist/report/browser-entry.js +15 -0
  152. package/dist/report/browser-entry.js.map +1 -0
  153. package/dist/report/bundle.d.ts +2 -0
  154. package/dist/report/bundle.js +77 -0
  155. package/dist/report/bundle.js.map +1 -0
  156. package/dist/report/classification.d.ts +9 -0
  157. package/dist/report/classification.js +55 -0
  158. package/dist/report/classification.js.map +1 -0
  159. package/dist/report/components/artifacts.d.ts +4 -0
  160. package/dist/report/components/artifacts.js +6 -0
  161. package/dist/report/components/artifacts.js.map +1 -0
  162. package/dist/report/components/experiments.d.ts +4 -0
  163. package/dist/report/components/experiments.js +7 -0
  164. package/dist/report/components/experiments.js.map +1 -0
  165. package/dist/report/components/hypotheses.d.ts +4 -0
  166. package/dist/report/components/hypotheses.js +26 -0
  167. package/dist/report/components/hypotheses.js.map +1 -0
  168. package/dist/report/components/indicators.d.ts +32 -0
  169. package/dist/report/components/indicators.js +42 -0
  170. package/dist/report/components/indicators.js.map +1 -0
  171. package/dist/report/components/layout.d.ts +29 -0
  172. package/dist/report/components/layout.js +20 -0
  173. package/dist/report/components/layout.js.map +1 -0
  174. package/dist/report/components/proof.d.ts +4 -0
  175. package/dist/report/components/proof.js +34 -0
  176. package/dist/report/components/proof.js.map +1 -0
  177. package/dist/report/components/reproduction.d.ts +4 -0
  178. package/dist/report/components/reproduction.js +19 -0
  179. package/dist/report/components/reproduction.js.map +1 -0
  180. package/dist/report/components/summary.d.ts +4 -0
  181. package/dist/report/components/summary.js +12 -0
  182. package/dist/report/components/summary.js.map +1 -0
  183. package/dist/report/components/verdict.d.ts +4 -0
  184. package/dist/report/components/verdict.js +27 -0
  185. package/dist/report/components/verdict.js.map +1 -0
  186. package/dist/report/model.d.ts +19 -0
  187. package/dist/report/model.js +82 -0
  188. package/dist/report/model.js.map +1 -0
  189. package/dist/report/open.d.ts +1 -0
  190. package/dist/report/open.js +24 -0
  191. package/dist/report/open.js.map +1 -0
  192. package/dist/report/publish.d.ts +12 -0
  193. package/dist/report/publish.js +67 -0
  194. package/dist/report/publish.js.map +1 -0
  195. package/dist/report/redaction.d.ts +1 -0
  196. package/dist/report/redaction.js +58 -0
  197. package/dist/report/redaction.js.map +1 -0
  198. package/dist/report/report-app.d.ts +6 -0
  199. package/dist/report/report-app.js +16 -0
  200. package/dist/report/report-app.js.map +1 -0
  201. package/dist/report/report-details.css +182 -0
  202. package/dist/report/report.css +404 -0
  203. package/dist/report/schema.d.ts +101 -0
  204. package/dist/report/schema.js +71 -0
  205. package/dist/report/schema.js.map +1 -0
  206. package/dist/reproducer/file.d.ts +3 -0
  207. package/dist/reproducer/file.js +14 -0
  208. package/dist/reproducer/file.js.map +1 -0
  209. package/dist/reproducer/schema.d.ts +17 -0
  210. package/dist/reproducer/schema.js +14 -0
  211. package/dist/reproducer/schema.js.map +1 -0
  212. package/dist/runner/local.d.ts +12 -0
  213. package/dist/runner/local.js +72 -0
  214. package/dist/runner/local.js.map +1 -0
  215. package/dist/runner/playwright-executor.d.ts +10 -0
  216. package/dist/runner/playwright-executor.js +148 -0
  217. package/dist/runner/playwright-executor.js.map +1 -0
  218. package/dist/security/credentials.d.ts +23 -0
  219. package/dist/security/credentials.js +105 -0
  220. package/dist/security/credentials.js.map +1 -0
  221. package/dist/solari/checkout-fixture.d.ts +3 -0
  222. package/dist/solari/checkout-fixture.js +54 -0
  223. package/dist/solari/checkout-fixture.js.map +1 -0
  224. package/dist/solari/executor.d.ts +17 -0
  225. package/dist/solari/executor.js +280 -0
  226. package/dist/solari/executor.js.map +1 -0
  227. package/dist/solari/retry.d.ts +7 -0
  228. package/dist/solari/retry.js +50 -0
  229. package/dist/solari/retry.js.map +1 -0
  230. package/dist/solari/run-demo.d.ts +21 -0
  231. package/dist/solari/run-demo.js +64 -0
  232. package/dist/solari/run-demo.js.map +1 -0
  233. package/dist/solari/transport.d.ts +1 -0
  234. package/dist/solari/transport.js +21 -0
  235. package/dist/solari/transport.js.map +1 -0
  236. package/dist/solari/usage.d.ts +23 -0
  237. package/dist/solari/usage.js +53 -0
  238. package/dist/solari/usage.js.map +1 -0
  239. package/dist/solari/websocket-proxy.d.ts +7 -0
  240. package/dist/solari/websocket-proxy.js +131 -0
  241. package/dist/solari/websocket-proxy.js.map +1 -0
  242. package/dist/ui/progress.d.ts +5 -0
  243. package/dist/ui/progress.js +16 -0
  244. package/dist/ui/progress.js.map +1 -0
  245. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # FlakeLab
2
+
3
+ FlakeLab is an AI debugging scientist for flaky Playwright tests. The product architecture,
4
+ scope, and milestone-based development plan live in
5
+ [`../../FLAKELAB.md`](../../FLAKELAB.md).
6
+
7
+ ## Quick start
8
+
9
+ Run a bounded, local, credit-free stability scan from any Playwright project:
10
+
11
+ ```bash
12
+ npx flakelab@latest .
13
+ ```
14
+
15
+ Pass a file when you already know which test is suspicious, and increase repetitions when the
16
+ failure is rare:
17
+
18
+ ```bash
19
+ npx flakelab@latest tests/checkout.spec.ts --runs 20
20
+ npx flakelab@latest tests/checkout.spec.ts --runs 20 --verbose
21
+ ```
22
+
23
+ The default command repeatedly executes the selected target in isolated Playwright processes,
24
+ distinguishes intermittent failures from consistent test failures, prints a concise result, and
25
+ writes machine-readable evidence to `.flakelab/runs/scan.json`. It does not call Groq or Solari.
26
+
27
+ For the complete causal workflow, use FlakeLab's Playwright fixture in tests that should accept
28
+ controlled browser faults:
29
+
30
+ ```ts
31
+ import { expect, test } from "flakelab/playwright"
32
+ ```
33
+
34
+ Then explicitly opt into AI investigation and isolated proof:
35
+
36
+ ```bash
37
+ npx flakelab@latest tests/checkout.spec.ts \
38
+ --prove \
39
+ --pattern "**/api/checkout" \
40
+ --open
41
+ ```
42
+
43
+ `--prove` requires `GROQ_API_KEY` and `SOLARI_API_KEY`. It chains trigger discovery, reproducer
44
+ verification, bounded AI investigation, candidate generation, proof in a disposable Solari
45
+ microVM, and the offline evidence report. The candidate remains a reviewable diff and is never
46
+ applied automatically.
47
+
48
+ See every command and default without running tests:
49
+
50
+ ```bash
51
+ npx flakelab@latest --help
52
+ ```
53
+
54
+ Check whether the current project is ready without running tests or consuming provider credits:
55
+
56
+ ```bash
57
+ npx flakelab@latest doctor
58
+ ```
59
+
60
+ The doctor checks Node.js, Playwright configuration, Chromium, ignored evidence output,
61
+ credential availability, and test-process credential isolation. It reports only whether a
62
+ credential is configured and where it came from; it never prints the value or a fingerprint.
63
+
64
+ The local diagnostic core can already run controlled baseline and fault-injected trials:
65
+
66
+ ```bash
67
+ pnpm flakelab diagnose tests/fixtures/flaky-checkout.spec.ts \
68
+ --runs 4 \
69
+ --seed 42 \
70
+ --delay-ms 250
71
+ ```
72
+
73
+ The command runs alternating control and network-delay trials, prints their failure rates,
74
+ and writes a validated NDJSON event stream under `.flakelab/runs/`. The included checkout
75
+ fixture passes normally and fails reliably when its checkout request is delayed.
76
+
77
+ ## Discover and replay a minimal reproducer
78
+
79
+ Run repeated baseline and fault trials, then minimize the delay that crosses the configured
80
+ failure-rate and confidence thresholds:
81
+
82
+ ```bash
83
+ pnpm flakelab discover tests/fixtures/flaky-checkout.spec.ts \
84
+ --trials 4 \
85
+ --concurrency 4 \
86
+ --seed 42 \
87
+ --max-delay 125 \
88
+ --min-rate 0.7 \
89
+ --output flakelab.repro.yaml
90
+ ```
91
+
92
+ The command writes a strict portable YAML reproducer and a JSON discovery sidecar containing
93
+ the evidence for every candidate. It normalizes Playwright failures into stable signatures,
94
+ uses an 80% Wilson lower confidence bound, and accepts a trigger only when both the observed
95
+ rate and lower bound meet the requested minimum. Replay the result with:
96
+
97
+ ```bash
98
+ pnpm flakelab replay flakelab.repro.yaml --concurrency 4
99
+ ```
100
+
101
+ The current browser-fault matrix supports deterministic network delay and injected HTTP
102
+ failures. Fault routes are removed after every trial, including failed tests.
103
+
104
+ ## AI investigator
105
+
106
+ Run the investigator directly. If `GROQ_API_KEY` is not already configured, an interactive
107
+ terminal requests it through hidden input and keeps it in memory for this run only:
108
+
109
+ ```bash
110
+ pnpm flakelab investigate tests/fixtures/flaky-checkout.spec.ts \
111
+ --trials 4 \
112
+ --concurrency 4 \
113
+ --max-delay 125
114
+ ```
115
+
116
+ The investigator uses the provider-neutral [Vercel AI SDK](https://ai-sdk.dev/docs/agents/overview)
117
+ with Groq's [`qwen/qwen3.8-27b`](https://console.groq.com/docs/model/qwen/qwen3.8-27b)
118
+ model. It makes two bounded model calls: one to propose competing hypotheses and a three-part
119
+ experiment batch, and one to assess the resulting evidence. FlakeLab—not the model—executes
120
+ trials, calculates confidence, enforces budgets, and decides whether a causal claim is valid.
121
+
122
+ The model receives only the selected test and at most eight local imported source files through
123
+ a 64 KiB, path-confined, credential-blocking reader. The resulting evidence-backed report is
124
+ written to `flakelab.investigation.json`. If no model key is configured, deterministic
125
+ `diagnose`, `discover`, and `replay` commands continue to work.
126
+
127
+ ## Bring your own credentials
128
+
129
+ FlakeLab connects directly from the developer's machine or CI runner to Groq and Solari. It does
130
+ not operate a credential relay or store provider keys in evidence. Credential lookup follows this
131
+ order:
132
+
133
+ 1. the current process environment, including protected CI environment secrets;
134
+ 2. a local `.env` file for compatibility with existing workflows;
135
+ 3. hidden interactive input, retained in memory only until the FlakeLab process exits.
136
+
137
+ Do not put credentials in command arguments: shell history and process inspection can expose
138
+ them. FlakeLab deliberately has no `--api-key` option. Local `.env` files and `.flakelab` evidence
139
+ are ignored by this repository, but protected CI secrets or the hidden run-once prompt are the
140
+ preferred choices.
141
+
142
+ Groq and Solari credentials are removed from the environment passed to Playwright subprocesses.
143
+ Application-specific environment variables remain available so existing test suites continue to
144
+ work. Provider keys are used only by the FlakeLab process at the API boundary and are never
145
+ injected into disposable proof sandboxes.
146
+
147
+ To ignore an outdated shell or `.env` credential and enter replacements securely for one run:
148
+
149
+ ```bash
150
+ npx flakelab@latest . --prove --prompt-credentials
151
+ ```
152
+
153
+ FlakeLab requests each required key once, hides terminal input, and reuses it only inside that
154
+ process. It does not modify the shell, `.env`, or CI configuration.
155
+
156
+ Provider failures are normalized from typed SDK errors, HTTP status, and machine-readable error
157
+ codes. Authentication, permissions, billing, rate limits, concurrency caps, capacity, network,
158
+ timeouts, and invalid requests receive separate next actions. FlakeLab does not branch on provider
159
+ prose or print raw response bodies. Groq rate and capacity failures may be retried within the
160
+ configured bound; Solari concurrency `429` responses are not retried because a session must be
161
+ released before another can start.
162
+
163
+ ## Isolated candidate repair
164
+
165
+ After reviewing an investigation, generate and prove a candidate without changing the working
166
+ tree:
167
+
168
+ ```bash
169
+ pnpm flakelab repair flakelab.investigation.json \
170
+ --reproducer flakelab.repro.yaml \
171
+ --patch candidate.diff \
172
+ --proof flakelab.proof.json
173
+ ```
174
+
175
+ This command requires both `GROQ_API_KEY` and `SOLARI_API_KEY`. The model can propose only exact,
176
+ bounded edits to application source it previously received. FlakeLab rejects test changes,
177
+ assertion weakening, lint suppressions, credential-like additions, path escapes, and numeric-only
178
+ timeout increases before execution.
179
+
180
+ The candidate is copied into a disposable Solari microVM. Typecheck, lint, hostile trials, clean
181
+ controls, and nearby regression tests all run there. The machine is destroyed afterward, and the
182
+ candidate is returned as a reviewable diff; FlakeLab never applies it to the local checkout.
183
+ Cold validation installs the pinned Node/pnpm toolchain and Chromium, so it is intentionally
184
+ slower than the future snapshot-backed warm path.
185
+
186
+ ## Evidence report
187
+
188
+ Turn the validated investigation, reproducer, candidate, and proof into one portable report:
189
+
190
+ ```bash
191
+ pnpm flakelab report flakelab.investigation.json \
192
+ --reproducer flakelab.repro.yaml \
193
+ --proof flakelab.proof.json \
194
+ --patch candidate.diff \
195
+ --html flakelab.report.html \
196
+ --open
197
+ ```
198
+
199
+ The React/Recharts interface is bundled by Vite into a single HTML file. It explains the root
200
+ cause, deterministic ownership classification, experiment timeline, competing hypotheses,
201
+ minimal trigger, before/after proof matrix, static checks, model usage, and reviewable artifacts.
202
+ The evidence is schema-validated and credential-redacted before rendering. A restrictive content
203
+ security policy blocks runtime network access, so the report remains usable offline.
204
+
205
+ `--open` launches only the local file. `--publish` is optional and always asks for interactive
206
+ confirmation immediately before creating a public Solari preview. Published reports expire and
207
+ their hosting sandbox is automatically killed after at most 60 minutes.
208
+
209
+ ## Statistical Git bisect
210
+
211
+ After producing a deterministic reproducer, locate the introducing commit without executing
212
+ historical code on the developer machine:
213
+
214
+ ```bash
215
+ pnpm flakelab bisect \
216
+ --good v1.4.0 \
217
+ --bad HEAD \
218
+ --reproducer flakelab.repro.yaml \
219
+ --bisect-report flakelab.bisect.json
220
+ ```
221
+
222
+ The selected good revision must be an ancestor of the bad revision, and both endpoints are
223
+ measured rather than trusted by name. FlakeLab archives each candidate without Git credentials,
224
+ prepares up to two midpoint revisions concurrently in disposable Solari sandboxes, and runs each
225
+ probabilistic trial in an independent fork of that revision's snapshot.
226
+
227
+ Classification uses both sides of an 80% Wilson interval. A revision is bad only when the lower
228
+ bound reaches `--min-rate`; it is good only when the upper bound stays below that threshold.
229
+ Ambiguous evidence receives another trial batch up to `--max-trials`. Dependency installation or
230
+ test-discovery failures are recorded as incompatible instead of being counted as test failures.
231
+
232
+ The JSON report contains every evaluated commit, pass/fail/error counts, confidence bounds,
233
+ snapshot reuse, duration, and decision reason. `firstFailingCommit` is populated only when the
234
+ good/bad boundary is exact. If an incompatible or inconclusive commit hides the boundary, the CLI
235
+ reports `earliestKnownBadCommit`, exits with code 2, and does not overclaim an exact answer.
236
+
237
+ Run the explicit credit-consuming end-to-end demonstration with:
238
+
239
+ ```bash
240
+ pnpm verify:bisect
241
+ ```
242
+
243
+ It creates a temporary four-commit repository with one intentional hydration regression, bisects
244
+ it in Solari, writes `.flakelab/bisect-demo.json`, and removes its sandboxes, snapshots, and local
245
+ temporary history. It is separate from the default test suite.
246
+
247
+ ## GitHub Actions
248
+
249
+ The repository includes a reusable composite action and a pull-request workflow:
250
+
251
+ - `../../.github/actions/flakelab/action.yml` installs the pinned Node/pnpm/Chromium toolchain,
252
+ selects changed tests, diagnoses one bounded target, uploads all evidence, and writes the job
253
+ summary.
254
+ - `../../.github/workflows/flakelab.yml` runs the credit-free quality gate on every matching pull
255
+ request and gates provider-backed diagnosis behind the protected `flakelab` environment.
256
+
257
+ Configure `GROQ_API_KEY` and `SOLARI_API_KEY` as GitHub environment secrets, then add required
258
+ reviewers to the `flakelab` environment. Fork pull requests never receive those secrets, checkout
259
+ credentials are not persisted, and the workflow deliberately avoids `pull_request_target`.
260
+
261
+ Changed-test selection prefers directly modified Playwright specs. When application, support,
262
+ Playwright configuration, manifest, or lockfile behavior changes, it falls back to the bounded
263
+ `tests/e2e` and `tests/fixtures` suite. The current action deeply diagnoses the first selected test
264
+ to keep time and cost predictable; `.flakelab/changed-tests.json` preserves the complete selection.
265
+
266
+ The uploaded artifact is retained for seven days and includes the offline HTML report,
267
+ investigation, reproducer, discovery evidence, proof, candidate diff, and selection manifest. A
268
+ rejected repair still uploads its evidence before the job reports failure.
269
+
270
+ ## Parallel Solari runner
271
+
272
+ Run the live eight-worker verification explicitly so routine tests remain local and
273
+ credit-free:
274
+
275
+ ```bash
276
+ pnpm verify:solari-parallel
277
+ ```
278
+
279
+ For a sequential comparison:
280
+
281
+ ```bash
282
+ pnpm verify:solari-parallel -- --concurrency 1 --runs 8
283
+ ```
284
+
285
+ The runner prepares and snapshots the application once, reuses snapshots by a cache key
286
+ derived from the Git commit, pnpm lockfile, and fixture configuration, and shares one
287
+ application sandbox for browser-only faults. Each trial receives an independent Solari
288
+ browser session. Metrics include wall time, cumulative trial time, peak concurrency,
289
+ infrastructure retries, cache status, and created/released resource counts.
290
+
291
+ ## Live Solari verification
292
+
293
+ The Solari verification script proves the remote execution path:
294
+
295
+ 1. create a Solari sandbox;
296
+ 2. write and start a tiny test application;
297
+ 3. expose it through a preview URL;
298
+ 4. snapshot the running sandbox and fork a second sandbox from it;
299
+ 5. launch a recorded Solari browser against the fork;
300
+ 6. inject network latency and verify the application still reaches its ready state;
301
+ 7. save a Playwright trace as the guaranteed diagnostic artifact;
302
+ 8. retrieve the optional Solari rrweb replay when available;
303
+ 9. destroy both sandboxes, even when a step fails.
304
+
305
+ ## Verify Solari
306
+
307
+ Set `SOLARI_API_KEY` in your shell, or add it to this directory's `.env` file:
308
+
309
+ ```text
310
+ SOLARI_API_KEY=...
311
+ ```
312
+
313
+ Then run the explicit live check:
314
+
315
+ ```bash
316
+ pnpm install
317
+ pnpm verify:solari
318
+ ```
319
+
320
+ The command exits non-zero if any required capability is not proven. It never prints the API key.
321
+
322
+ ## Development quality gates
323
+
324
+ ```bash
325
+ pnpm typecheck
326
+ pnpm lint
327
+ pnpm test
328
+ ```
329
+
330
+ Run `pnpm quality` before handing back a completed behavior change. The default Playwright suite
331
+ is local and credit-free; `repair`, `pnpm verify:solari`, and `pnpm verify:solari-parallel` are
332
+ explicit live integrations that can consume Solari credits. `pnpm verify:bisect` is also an
333
+ explicit live integration.
334
+
335
+ ## Solari documentation
336
+
337
+ - [Sandboxes](https://docs.getsolari.com/sandboxes)
338
+ - [Snapshots](https://docs.getsolari.com/snapshots)
339
+ - [Browser sessions](https://docs.getsolari.com/sessions)
340
+ - [Session recording](https://docs.getsolari.com/recording)
341
+ - [Browser API reference](https://docs.getsolari.com/api-reference/browser)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ await import("../dist/cli.js")
@@ -0,0 +1,3 @@
1
+ import type { RunEvent } from "../domain/schema.js";
2
+ export type EventWriter = (event: RunEvent) => Promise<void>;
3
+ export declare function createNdjsonWriter(filePath: string): EventWriter;
@@ -0,0 +1,16 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { runEventSchema } from "../domain/schema.js";
4
+ export function createNdjsonWriter(filePath) {
5
+ let pendingWrite = Promise.resolve();
6
+ return (event) => {
7
+ const write = pendingWrite.then(async () => {
8
+ const validatedEvent = runEventSchema.parse(event);
9
+ await mkdir(dirname(filePath), { recursive: true });
10
+ await appendFile(filePath, `${JSON.stringify(validatedEvent)}\n`, { encoding: "utf8" });
11
+ });
12
+ pendingWrite = write.catch(() => undefined);
13
+ return write;
14
+ };
15
+ }
16
+ //# sourceMappingURL=ndjson.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ndjson.js","sourceRoot":"","sources":["../../src/artifacts/ndjson.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAKpD,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAA;IACpC,OAAO,CAAC,KAAK,EAAE,EAAE;QACf,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACzC,MAAM,cAAc,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClD,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACnD,MAAM,UAAU,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;QACzF,CAAC,CAAC,CAAA;QACF,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC3C,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function createRevisionArchive(repositoryRoot: string, revision: string): Promise<Buffer>;
@@ -0,0 +1,14 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
4
+ const execFileAsync = promisify(execFile);
5
+ export async function createRevisionArchive(repositoryRoot, revision) {
6
+ const result = await execFileAsync("git", ["archive", "--format=tar", revision], {
7
+ cwd: repositoryRoot,
8
+ encoding: "buffer",
9
+ maxBuffer: MAX_ARCHIVE_BYTES,
10
+ windowsHide: true,
11
+ });
12
+ return result.stdout;
13
+ }
14
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../../src/bisect/archive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAC1C,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;AAEzC,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,cAAsB,EACtB,QAAgB;IAEhB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE;QAC/E,GAAG,EAAE,cAAc;QACnB,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,iBAAiB;QAC5B,WAAW,EAAE,IAAI;KAClB,CAAC,CAAA;IACF,OAAO,MAAM,CAAC,MAAM,CAAA;AACtB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface ConfidenceInterval {
2
+ lower: number;
3
+ upper: number;
4
+ }
5
+ export declare function wilsonInterval80(failures: number, trials: number): ConfidenceInterval;
6
+ export type ConfidenceClassification = "bad" | "good" | "inconclusive";
7
+ export declare function classifyFailureProbability(failures: number, trials: number, errors: number, minimumFailureRate: number): ConfidenceClassification;
@@ -0,0 +1,27 @@
1
+ const WILSON_Z_80 = 1.281_551_565_545;
2
+ export function wilsonInterval80(failures, trials) {
3
+ if (trials === 0) {
4
+ return { lower: 0, upper: 1 };
5
+ }
6
+ const probability = failures / trials;
7
+ const squaredZ = WILSON_Z_80 ** 2;
8
+ const denominator = 1 + squaredZ / trials;
9
+ const center = probability + squaredZ / (2 * trials);
10
+ const margin = WILSON_Z_80 * Math.sqrt((probability * (1 - probability) + squaredZ / (4 * trials)) / trials);
11
+ return {
12
+ lower: Math.max(0, (center - margin) / denominator),
13
+ upper: Math.min(1, (center + margin) / denominator),
14
+ };
15
+ }
16
+ export function classifyFailureProbability(failures, trials, errors, minimumFailureRate) {
17
+ const validTrials = trials - errors;
18
+ if (validTrials <= 0 || failures > validTrials) {
19
+ return "inconclusive";
20
+ }
21
+ const interval = wilsonInterval80(failures, validTrials);
22
+ if (interval.lower >= minimumFailureRate) {
23
+ return "bad";
24
+ }
25
+ return interval.upper < minimumFailureRate ? "good" : "inconclusive";
26
+ }
27
+ //# sourceMappingURL=confidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confidence.js","sourceRoot":"","sources":["../../src/bisect/confidence.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,iBAAiB,CAAA;AAOrC,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,MAAc;IAC/D,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;IAC/B,CAAC;IACD,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAA;IACrC,MAAM,QAAQ,GAAG,WAAW,IAAI,CAAC,CAAA;IACjC,MAAM,WAAW,GAAG,CAAC,GAAG,QAAQ,GAAG,MAAM,CAAA;IACzC,MAAM,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAA;IACpD,MAAM,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC,IAAI,CACpC,CAAC,WAAW,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CACrE,CAAA;IACD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,WAAW,CAAC;QACnD,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,WAAW,CAAC;KACpD,CAAA;AACH,CAAC;AAID,MAAM,UAAU,0BAA0B,CACxC,QAAgB,EAChB,MAAc,EACd,MAAc,EACd,kBAA0B;IAE1B,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAA;IACnC,IAAI,WAAW,IAAI,CAAC,IAAI,QAAQ,GAAG,WAAW,EAAE,CAAC;QAC/C,OAAO,cAAc,CAAA;IACvB,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;IACxD,IAAI,QAAQ,CAAC,KAAK,IAAI,kBAAkB,EAAE,CAAC;QACzC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,QAAQ,CAAC,KAAK,GAAG,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAA;AACtE,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { BisectReport, Revision, RevisionEvidence } from "./schema.js";
2
+ export type RevisionEvaluator = (revision: Revision) => Promise<RevisionEvidence>;
3
+ interface BisectOptions {
4
+ evaluate: RevisionEvaluator;
5
+ minimumFailureRate: number;
6
+ parallelism?: number;
7
+ revisions: Revision[];
8
+ }
9
+ export declare function statisticalBisect(options: BisectOptions): Promise<BisectReport>;
10
+ export {};
@@ -0,0 +1,110 @@
1
+ import { bisectReportSchema } from "./schema.js";
2
+ function selectCandidates(lower, upper, evaluated, parallelism) {
3
+ const available = Array.from({ length: Math.max(0, upper - lower - 1) }, (_, offset) => lower + offset + 1).filter((index) => !evaluated.has(index));
4
+ if (available.length <= parallelism) {
5
+ return available;
6
+ }
7
+ return Array.from({ length: parallelism }, (_, slot) => {
8
+ const position = Math.floor(((slot + 1) * available.length) / (parallelism + 1));
9
+ return available[Math.min(position, available.length - 1)];
10
+ }).filter((value, index, values) => values.indexOf(value) === index);
11
+ }
12
+ function assertMonotonic(evaluated) {
13
+ const goodIndexes = [...evaluated.entries()]
14
+ .filter((entry) => entry[1].classification === "good")
15
+ .map((entry) => entry[0]);
16
+ const badIndexes = [...evaluated.entries()]
17
+ .filter((entry) => entry[1].classification === "bad")
18
+ .map((entry) => entry[0]);
19
+ if (goodIndexes.length > 0 && badIndexes.length > 0
20
+ && Math.max(...goodIndexes) >= Math.min(...badIndexes)) {
21
+ throw new Error("revision evidence is non-monotonic; a good revision follows a bad revision");
22
+ }
23
+ }
24
+ function boundaries(evaluated) {
25
+ const good = [...evaluated.entries()]
26
+ .filter((entry) => entry[1].classification === "good")
27
+ .map((entry) => entry[0]);
28
+ const bad = [...evaluated.entries()]
29
+ .filter((entry) => entry[1].classification === "bad")
30
+ .map((entry) => entry[0]);
31
+ return [Math.max(...good), Math.min(...bad)];
32
+ }
33
+ async function evaluateIndexes(indexes, revisions, evaluate, evaluated) {
34
+ const results = await Promise.all(indexes.map(async (index) => {
35
+ try {
36
+ return {
37
+ status: "fulfilled",
38
+ index,
39
+ evidence: await evaluate(revisions[index]),
40
+ };
41
+ }
42
+ catch (error) {
43
+ return {
44
+ status: "rejected",
45
+ error: error instanceof Error ? error : new Error("revision evaluation failed"),
46
+ };
47
+ }
48
+ }));
49
+ const failure = results.find((result) => result.status === "rejected");
50
+ if (failure?.status === "rejected") {
51
+ throw failure.error;
52
+ }
53
+ for (const result of results) {
54
+ if (result.status === "fulfilled") {
55
+ evaluated.set(result.index, result.evidence);
56
+ }
57
+ }
58
+ }
59
+ function validateOptions(options) {
60
+ if (options.revisions.length < 2) {
61
+ throw new Error("bisect requires at least two revisions");
62
+ }
63
+ if (options.minimumFailureRate <= 0 || options.minimumFailureRate > 1) {
64
+ throw new Error("minimum failure rate must be greater than 0 and at most 1");
65
+ }
66
+ const parallelism = options.parallelism ?? 2;
67
+ if (!Number.isInteger(parallelism) || parallelism < 1 || parallelism > 4) {
68
+ throw new Error("bisect parallelism must be an integer between 1 and 4");
69
+ }
70
+ return parallelism;
71
+ }
72
+ export async function statisticalBisect(options) {
73
+ const parallelism = validateOptions(options);
74
+ const evaluated = new Map();
75
+ const lastIndex = options.revisions.length - 1;
76
+ await evaluateIndexes([0, lastIndex], options.revisions, options.evaluate, evaluated);
77
+ if (evaluated.get(0)?.classification !== "good") {
78
+ throw new Error("the selected good revision was not classified as good");
79
+ }
80
+ if (evaluated.get(lastIndex)?.classification !== "bad") {
81
+ throw new Error("the selected bad revision was not classified as bad");
82
+ }
83
+ let [goodIndex, badIndex] = boundaries(evaluated);
84
+ while (badIndex - goodIndex > 1) {
85
+ const candidates = selectCandidates(goodIndex, badIndex, evaluated, parallelism);
86
+ if (candidates.length === 0) {
87
+ break;
88
+ }
89
+ await evaluateIndexes(candidates, options.revisions, options.evaluate, evaluated);
90
+ assertMonotonic(evaluated);
91
+ [goodIndex, badIndex] = boundaries(evaluated);
92
+ }
93
+ const exact = badIndex - goodIndex === 1;
94
+ const evidence = [...evaluated.entries()]
95
+ .sort((left, right) => left[0] - right[0])
96
+ .map((entry) => entry[1]);
97
+ return bisectReportSchema.parse({
98
+ version: 1,
99
+ goodRevision: options.revisions[0],
100
+ badRevision: options.revisions[lastIndex],
101
+ firstFailingCommit: exact ? options.revisions[badIndex] : null,
102
+ earliestKnownBadCommit: options.revisions[badIndex],
103
+ exact,
104
+ minimumFailureRate: options.minimumFailureRate,
105
+ evaluatedRevisionCount: evaluated.size,
106
+ totalRevisionCount: options.revisions.length,
107
+ evidence,
108
+ });
109
+ }
110
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/bisect/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Revision } from "./schema.js";
2
+ export interface GitHistory {
3
+ projectPath: string;
4
+ repositoryRoot: string;
5
+ revisions: Revision[];
6
+ }
7
+ export declare function resolveGitHistory(projectRoot: string, goodReference: string, badReference: string): Promise<GitHistory>;
@@ -0,0 +1,56 @@
1
+ import { execFile } from "node:child_process";
2
+ import { relative, resolve } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { revisionSchema } from "./schema.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const MAX_REVISIONS = 1_000;
7
+ async function git(cwd, args) {
8
+ const result = await execFileAsync("git", args, {
9
+ cwd,
10
+ encoding: "utf8",
11
+ maxBuffer: 4 * 1024 * 1024,
12
+ windowsHide: true,
13
+ });
14
+ return result.stdout.trim();
15
+ }
16
+ async function resolveCommit(repositoryRoot, reference) {
17
+ if (reference.length > 200 || /[\r\n\0]/u.test(reference)) {
18
+ throw new Error("Git revision must be a single value no longer than 200 characters");
19
+ }
20
+ return git(repositoryRoot, ["rev-parse", "--verify", `${reference}^{commit}`]);
21
+ }
22
+ async function revisionDetails(repositoryRoot, hash) {
23
+ const subject = await git(repositoryRoot, ["show", "-s", "--format=%s", hash]);
24
+ return revisionSchema.parse({ hash, shortHash: hash.slice(0, 12), subject });
25
+ }
26
+ export async function resolveGitHistory(projectRoot, goodReference, badReference) {
27
+ const repositoryRoot = resolve(await git(projectRoot, ["rev-parse", "--show-toplevel"]));
28
+ const goodHash = await resolveCommit(repositoryRoot, goodReference);
29
+ const badHash = await resolveCommit(repositoryRoot, badReference);
30
+ try {
31
+ await git(repositoryRoot, ["merge-base", "--is-ancestor", goodHash, badHash]);
32
+ }
33
+ catch {
34
+ throw new Error("the selected good revision must be an ancestor of the bad revision");
35
+ }
36
+ const descendants = await git(repositoryRoot, [
37
+ "rev-list",
38
+ "--ancestry-path",
39
+ "--reverse",
40
+ `${goodHash}..${badHash}`,
41
+ ]);
42
+ const hashes = [goodHash, ...descendants.split(/\r?\n/u).filter(Boolean)];
43
+ if (hashes.length > MAX_REVISIONS) {
44
+ throw new Error(`bisect history exceeds the ${MAX_REVISIONS}-revision safety limit`);
45
+ }
46
+ const projectPath = relative(repositoryRoot, resolve(projectRoot));
47
+ if (projectPath.startsWith("..")) {
48
+ throw new Error("the project must be inside its Git repository");
49
+ }
50
+ return {
51
+ projectPath,
52
+ repositoryRoot,
53
+ revisions: await Promise.all(hashes.map((hash) => revisionDetails(repositoryRoot, hash))),
54
+ };
55
+ }
56
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/bisect/git.ts"],"names":[],"mappings":"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"}