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.
- package/README.md +341 -0
- package/bin/flakelab.mjs +3 -0
- package/dist/artifacts/ndjson.d.ts +3 -0
- package/dist/artifacts/ndjson.js +16 -0
- package/dist/artifacts/ndjson.js.map +1 -0
- package/dist/bisect/archive.d.ts +1 -0
- package/dist/bisect/archive.js +14 -0
- package/dist/bisect/archive.js.map +1 -0
- package/dist/bisect/confidence.d.ts +7 -0
- package/dist/bisect/confidence.js +27 -0
- package/dist/bisect/confidence.js.map +1 -0
- package/dist/bisect/engine.d.ts +10 -0
- package/dist/bisect/engine.js +110 -0
- package/dist/bisect/engine.js.map +1 -0
- package/dist/bisect/git.d.ts +7 -0
- package/dist/bisect/git.js +56 -0
- package/dist/bisect/git.js.map +1 -0
- package/dist/bisect/schema.d.ts +82 -0
- package/dist/bisect/schema.js +33 -0
- package/dist/bisect/schema.js.map +1 -0
- package/dist/bisect/solari-command.d.ts +7 -0
- package/dist/bisect/solari-command.js +95 -0
- package/dist/bisect/solari-command.js.map +1 -0
- package/dist/bisect/solari-evaluator.d.ts +20 -0
- package/dist/bisect/solari-evaluator.js +310 -0
- package/dist/bisect/solari-evaluator.js.map +1 -0
- package/dist/ci/changed-tests.d.ts +7 -0
- package/dist/ci/changed-tests.js +104 -0
- package/dist/ci/changed-tests.js.map +1 -0
- package/dist/ci/job-summary.d.ts +11 -0
- package/dist/ci/job-summary.js +45 -0
- package/dist/ci/job-summary.js.map +1 -0
- package/dist/ci/select-command.d.ts +1 -0
- package/dist/ci/select-command.js +49 -0
- package/dist/ci/select-command.js.map +1 -0
- package/dist/ci/summary-command.d.ts +1 -0
- package/dist/ci/summary-command.js +46 -0
- package/dist/ci/summary-command.js.map +1 -0
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.js +47 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +121 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/bisect.d.ts +2 -0
- package/dist/commands/bisect.js +63 -0
- package/dist/commands/bisect.js.map +1 -0
- package/dist/commands/diagnose.d.ts +2 -0
- package/dist/commands/diagnose.js +27 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/discover.d.ts +2 -0
- package/dist/commands/discover.js +46 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +78 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/investigate.d.ts +2 -0
- package/dist/commands/investigate.js +43 -0
- package/dist/commands/investigate.js.map +1 -0
- package/dist/commands/options.d.ts +38 -0
- package/dist/commands/options.js +35 -0
- package/dist/commands/options.js.map +1 -0
- package/dist/commands/prove.d.ts +2 -0
- package/dist/commands/prove.js +21 -0
- package/dist/commands/prove.js.map +1 -0
- package/dist/commands/repair.d.ts +2 -0
- package/dist/commands/repair.js +55 -0
- package/dist/commands/repair.js.map +1 -0
- package/dist/commands/replay.d.ts +2 -0
- package/dist/commands/replay.js +26 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +79 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/scan.d.ts +12 -0
- package/dist/commands/scan.js +86 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/core/plan.d.ts +3 -0
- package/dist/core/plan.js +17 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/discovery/evaluate.d.ts +22 -0
- package/dist/discovery/evaluate.js +81 -0
- package/dist/discovery/evaluate.js.map +1 -0
- package/dist/discovery/minimize.d.ts +24 -0
- package/dist/discovery/minimize.js +73 -0
- package/dist/discovery/minimize.js.map +1 -0
- package/dist/domain/schema.d.ts +155 -0
- package/dist/domain/schema.js +67 -0
- package/dist/domain/schema.js.map +1 -0
- package/dist/faults/install.d.ts +4 -0
- package/dist/faults/install.js +9 -0
- package/dist/faults/install.js.map +1 -0
- package/dist/faults/network-delay.d.ts +5 -0
- package/dist/faults/network-delay.js +34 -0
- package/dist/faults/network-delay.js.map +1 -0
- package/dist/faults/request-failure.d.ts +5 -0
- package/dist/faults/request-failure.js +24 -0
- package/dist/faults/request-failure.js.map +1 -0
- package/dist/investigator/agent.d.ts +26 -0
- package/dist/investigator/agent.js +197 -0
- package/dist/investigator/agent.js.map +1 -0
- package/dist/investigator/budget.d.ts +14 -0
- package/dist/investigator/budget.js +39 -0
- package/dist/investigator/budget.js.map +1 -0
- package/dist/investigator/file.d.ts +2 -0
- package/dist/investigator/file.js +7 -0
- package/dist/investigator/file.js.map +1 -0
- package/dist/investigator/groq.d.ts +5 -0
- package/dist/investigator/groq.js +26 -0
- package/dist/investigator/groq.js.map +1 -0
- package/dist/investigator/ledger.d.ts +17 -0
- package/dist/investigator/ledger.js +121 -0
- package/dist/investigator/ledger.js.map +1 -0
- package/dist/investigator/safe-source.d.ts +6 -0
- package/dist/investigator/safe-source.js +79 -0
- package/dist/investigator/safe-source.js.map +1 -0
- package/dist/investigator/schema.d.ts +110 -0
- package/dist/investigator/schema.js +53 -0
- package/dist/investigator/schema.js.map +1 -0
- package/dist/playwright.d.ts +3 -0
- package/dist/playwright.js +21 -0
- package/dist/playwright.js.map +1 -0
- package/dist/providers/errors.d.ts +21 -0
- package/dist/providers/errors.js +159 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/repair/file.d.ts +2 -0
- package/dist/repair/file.js +6 -0
- package/dist/repair/file.js.map +1 -0
- package/dist/repair/generator.d.ts +12 -0
- package/dist/repair/generator.js +74 -0
- package/dist/repair/generator.js.map +1 -0
- package/dist/repair/policy.d.ts +2 -0
- package/dist/repair/policy.js +70 -0
- package/dist/repair/policy.js.map +1 -0
- package/dist/repair/remote-proof-runner.d.ts +1 -0
- package/dist/repair/remote-proof-runner.js +53 -0
- package/dist/repair/remote-proof-runner.js.map +1 -0
- package/dist/repair/schema.d.ts +78 -0
- package/dist/repair/schema.js +44 -0
- package/dist/repair/schema.js.map +1 -0
- package/dist/repair/solari-validator.d.ts +25 -0
- package/dist/repair/solari-validator.js +231 -0
- package/dist/repair/solari-validator.js.map +1 -0
- package/dist/repair/validator.d.ts +16 -0
- package/dist/repair/validator.js +81 -0
- package/dist/repair/validator.js.map +1 -0
- package/dist/repair/workspace.d.ts +7 -0
- package/dist/repair/workspace.js +40 -0
- package/dist/repair/workspace.js.map +1 -0
- package/dist/report/browser-entry.d.ts +2 -0
- package/dist/report/browser-entry.js +15 -0
- package/dist/report/browser-entry.js.map +1 -0
- package/dist/report/bundle.d.ts +2 -0
- package/dist/report/bundle.js +77 -0
- package/dist/report/bundle.js.map +1 -0
- package/dist/report/classification.d.ts +9 -0
- package/dist/report/classification.js +55 -0
- package/dist/report/classification.js.map +1 -0
- package/dist/report/components/artifacts.d.ts +4 -0
- package/dist/report/components/artifacts.js +6 -0
- package/dist/report/components/artifacts.js.map +1 -0
- package/dist/report/components/experiments.d.ts +4 -0
- package/dist/report/components/experiments.js +7 -0
- package/dist/report/components/experiments.js.map +1 -0
- package/dist/report/components/hypotheses.d.ts +4 -0
- package/dist/report/components/hypotheses.js +26 -0
- package/dist/report/components/hypotheses.js.map +1 -0
- package/dist/report/components/indicators.d.ts +32 -0
- package/dist/report/components/indicators.js +42 -0
- package/dist/report/components/indicators.js.map +1 -0
- package/dist/report/components/layout.d.ts +29 -0
- package/dist/report/components/layout.js +20 -0
- package/dist/report/components/layout.js.map +1 -0
- package/dist/report/components/proof.d.ts +4 -0
- package/dist/report/components/proof.js +34 -0
- package/dist/report/components/proof.js.map +1 -0
- package/dist/report/components/reproduction.d.ts +4 -0
- package/dist/report/components/reproduction.js +19 -0
- package/dist/report/components/reproduction.js.map +1 -0
- package/dist/report/components/summary.d.ts +4 -0
- package/dist/report/components/summary.js +12 -0
- package/dist/report/components/summary.js.map +1 -0
- package/dist/report/components/verdict.d.ts +4 -0
- package/dist/report/components/verdict.js +27 -0
- package/dist/report/components/verdict.js.map +1 -0
- package/dist/report/model.d.ts +19 -0
- package/dist/report/model.js +82 -0
- package/dist/report/model.js.map +1 -0
- package/dist/report/open.d.ts +1 -0
- package/dist/report/open.js +24 -0
- package/dist/report/open.js.map +1 -0
- package/dist/report/publish.d.ts +12 -0
- package/dist/report/publish.js +67 -0
- package/dist/report/publish.js.map +1 -0
- package/dist/report/redaction.d.ts +1 -0
- package/dist/report/redaction.js +58 -0
- package/dist/report/redaction.js.map +1 -0
- package/dist/report/report-app.d.ts +6 -0
- package/dist/report/report-app.js +16 -0
- package/dist/report/report-app.js.map +1 -0
- package/dist/report/report-details.css +182 -0
- package/dist/report/report.css +404 -0
- package/dist/report/schema.d.ts +101 -0
- package/dist/report/schema.js +71 -0
- package/dist/report/schema.js.map +1 -0
- package/dist/reproducer/file.d.ts +3 -0
- package/dist/reproducer/file.js +14 -0
- package/dist/reproducer/file.js.map +1 -0
- package/dist/reproducer/schema.d.ts +17 -0
- package/dist/reproducer/schema.js +14 -0
- package/dist/reproducer/schema.js.map +1 -0
- package/dist/runner/local.d.ts +12 -0
- package/dist/runner/local.js +72 -0
- package/dist/runner/local.js.map +1 -0
- package/dist/runner/playwright-executor.d.ts +10 -0
- package/dist/runner/playwright-executor.js +148 -0
- package/dist/runner/playwright-executor.js.map +1 -0
- package/dist/security/credentials.d.ts +23 -0
- package/dist/security/credentials.js +105 -0
- package/dist/security/credentials.js.map +1 -0
- package/dist/solari/checkout-fixture.d.ts +3 -0
- package/dist/solari/checkout-fixture.js +54 -0
- package/dist/solari/checkout-fixture.js.map +1 -0
- package/dist/solari/executor.d.ts +17 -0
- package/dist/solari/executor.js +280 -0
- package/dist/solari/executor.js.map +1 -0
- package/dist/solari/retry.d.ts +7 -0
- package/dist/solari/retry.js +50 -0
- package/dist/solari/retry.js.map +1 -0
- package/dist/solari/run-demo.d.ts +21 -0
- package/dist/solari/run-demo.js +64 -0
- package/dist/solari/run-demo.js.map +1 -0
- package/dist/solari/transport.d.ts +1 -0
- package/dist/solari/transport.js +21 -0
- package/dist/solari/transport.js.map +1 -0
- package/dist/solari/usage.d.ts +23 -0
- package/dist/solari/usage.js +53 -0
- package/dist/solari/usage.js.map +1 -0
- package/dist/solari/websocket-proxy.d.ts +7 -0
- package/dist/solari/websocket-proxy.js +131 -0
- package/dist/solari/websocket-proxy.js.map +1 -0
- package/dist/ui/progress.d.ts +5 -0
- package/dist/ui/progress.js +16 -0
- package/dist/ui/progress.js.map +1 -0
- 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)
|
package/bin/flakelab.mjs
ADDED
|
@@ -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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"}
|