Measuring e2e code coverage locally
Reflow can record how much of a Go application, or of a Swift, C, C++ or Objective-C application built with LLVM source-based coverage, your end-to-end flows exercise. You build the application with coverage instrumentation, run it while you apply flows, stop it gracefully, and ingest the profile it writes. Reflow stores one canonical document per run window, tied to the exact commit and tree it was measured against. For Go, that is checked against the build stamp in the binary. For LLVM, it is the worktree you bind, because LLVM binaries carry no commit stamp. Go and LLVM programs produce separate documents; see Swift and other LLVM languages and Coverage from a MacOSApp run. To query stored documents, see Reading coverage as an agent.
This page covers the local, process-level workflow (reflow coverage). It
measures whole processes over a run window. Per-flow numbers and an automatic
counter-reset surface are not available yet.
What you need
Section titled “What you need”- For Go: a Go module whose server or worker runs from your own
mainpackage. For Swift and other LLVM languages, see their section for the requirements. - For Go: Go 1.20 or later (the examples were verified with Go 1.26.1). The
goon yourPATHmust be the same version that built the binary. - A Reflow login and a selected collection for that login: either
reflow collections select <id>or a target selected withreflow target <url> --group <name> --collection <id>. Otherwise pass--collection <id>to each command.
Why package main must be instrumented
Section titled “Why package main must be instrumented”Go only starts its coverage runtime from an instrumented main package. If
main is excluded from -coverpkg, the build still succeeds, but coverage is
silently inactive: no counters are written when the process exits. Always build
the binary whose main you run with -cover, and include main in the
instrumented packages.
Injecting coverage with go build -overlay is not supported. Go’s cover tool
does not apply overlays, which forces main out of the instrumented set.
Set up the build
Section titled “Set up the build”Choose one of the two supported setups. Both must build with
-buildvcs=true (the default inside a Git checkout) so that the binary records
the commit it was built from. reflow coverage bind refuses binaries without
that stamp.
Setup (a): a build-tagged adapter in package main
Section titled “Setup (a): a build-tagged adapter in package main”Commit this file next to your main.go. The build tag keeps it out of normal
builds.
//go:build reflowcoverage
package main
// Reflow coverage startup check (setup a). Runs from init(); `reflow coverage verify` is the backstop.import ( "fmt" "os" "path/filepath" "runtime/coverage")
func init() { dir := os.Getenv("GOCOVERDIR") if dir == "" { fmt.Fprintln(os.Stderr, "reflow coverage: GOCOVERDIR is not set") os.Exit(70) } if err := coverage.WriteMetaDir(dir); err != nil { fmt.Fprintln(os.Stderr, "reflow coverage: coverage is not active:", err) os.Exit(70) } marker := filepath.Join(dir, fmt.Sprintf("reflow-start.%d", os.Getpid())) if err := os.WriteFile(marker, nil, 0o600); err != nil { fmt.Fprintln(os.Stderr, "reflow coverage: marker:", err) os.Exit(70) }}Save it as zz_reflow_coverage.go. At startup it checks that coverage is active,
exits with status 70 if it is not, and writes a reflow-start.<pid> marker that
reflow coverage verify counts.
Build from inside the checkout:
go build -buildvcs=true -tags reflowcoverage -cover -covermode=atomic \ -coverpkg=./... -o /tmp/app-cov/app .Setup (b): a harness module committed inside the app repository
Section titled “Setup (b): a harness module committed inside the app repository”If you cannot add a file to your main package, commit a small nested module,
for example internal/reflowcov/, whose own main calls your application’s
command package and includes the same adapter file:
module example.com/app/internal/reflowcov
go 1.26
require example.com/app v0.0.0
replace example.com/app => ../..package main
import "example.com/app/cmd/server"
func main() { server.Main() }Run go mod tidy in the harness directory and commit its go.sum. Build it
with your application’s packages instrumented; the pattern also covers the
harness’s own main:
cd internal/reflowcovgo build -buildvcs=true -tags reflowcoverage -cover -covermode=atomic \ -coverpkg=example.com/app/... -o /tmp/app-cov/app .The harness must live inside the application repository. Go stamps the VCS
revision from the repository that contains the main module, so a harness kept
elsewhere carries a foreign or missing stamp, which bind refuses. Reflow maps
Go import paths to files through every go.mod in the committed tree, so
nested modules resolve to their real paths.
Make SIGTERM return from main
Section titled “Make SIGTERM return from main”Go writes coverage counters only when main returns or the program calls
os.Exit. A process killed by an unhandled signal writes nothing. Handle
SIGTERM and SIGINT so that main returns after a graceful shutdown:
func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) defer stop()
srv := &http.Server{Addr: ":8080", Handler: routes()} go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } }()
<-ctx.Done() shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutdown) // Returning from main flushes the coverage counters.}Run the built binary directly. go run starts a separate child process and is
not supported.
Keep coverage files outside the worktree
Section titled “Keep coverage files outside the worktree”Put the cover directory and the instrumented binary
outside the repository checkout, for example under /tmp/app-cov/. bind
records whether the worktree is clean with git status; files it can see make
every document dirty, and Go stamps later builds with vcs.modified=true. If
they must live inside the checkout, git-ignore them. bind refuses a cover
directory inside the worktree that is not git-ignored
(cover_dir_in_worktree) and warns about such a binary.
Measure a run window
Section titled “Measure a run window”-
Build the binary with setup (a) or (b), from a committed tree. Go records
vcs.modified=truefor untracked files as well as modified ones, so build from a clean checkout or worktree:git status --porcelainmust print nothing.bindrefuses a binary stampedvcs.modified=truewhen the worktree is clean, and warns when it binds a dirty worktree. -
Bind a fresh, empty cover directory to the binary and the current commit:
Terminal window reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/data \--exclude '**/testenv/**' --exclude '**/*_gen.go' --jsonbindcreates the directory (mode 700) if needed and refuses one that is not empty, or one inside the worktree that is not git-ignored. It records the commit, tree, object format and dirty state, the binary’s SHA-256 and itsgo version -mbuild information, and writesreflow-coverage-binding.json(mode 600) in the cover directory. It refuses a binary that has no VCS stamp, was built from a different commit, was built from a modified tree while the worktree is clean, or was not built with-cover. The repository label comes from theoriginremote ashost/owner/repo; pass--repository <label>to set it explicitly. Optional--excludeglobs are recorded in the binding file, andbindprints how many tracked non-test.gofiles each one matches; see Excluding code from the denominator. TheNext:line namesverifywhen the binary was built with-tags reflowcoverage(the adapter is compiled in), and the exit-only steps otherwise. -
Start every instrumented process (for example a server and a worker running the same binary) with the cover directory in its environment, using your usual process manager:
Terminal window GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app serve &GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app worker & -
Verify that coverage is active in exactly the processes you expect:
Terminal window reflow coverage verify --cover-dir /tmp/app-cov/data --processes 2Within
--timeout-ms(default 10000), exactly onecovmeta.*file and exactly that manyreflow-start.<pid>markers must exist. Reflow then reads the cover mode withgo tool covdata debugdump;go env GOVERSIONmust match the binary’s Go version. The mode and process IDs are recorded in the binding file. A build that leavesmainuninstrumented fails here.When several processes start at the same moment, one may occasionally print
coverage meta-data emit failed ... rename .... The processes share the same meta-data, andverifyconfirms the singlecovmeta.*file. Ifverifypasses, the run is complete; it fails when the meta-data is missing. -
Apply your flows as usual. Keep each apply’s request identity (
request.idempotency_keyinreflow apply --json) if you want the document to name the batches it covers.Terminal window reflow apply --json --result-file /tmp/app-cov/apply.json -
Stop the processes gracefully with SIGTERM, so each one returns from
mainand writes itscovcounters.*file. -
Ingest the cover directory:
Terminal window reflow coverage ingest --cover-dir /tmp/app-cov/data \--batch <idempotency-key> --jsoningestrequires the unmodified binding file and a completedverify; no process still writing (counters_in_progress); exactly onecovmeta.*file, matching the verified one;reflow-start.*markers for exactly the verified processes (process_countif one started afterverify); and exactly one completecovcounters.*file for each verified process and for no other (missing_counters,unverified_counters,duplicate_pid,counter_incomplete). It then derives the profile itself: you no longer rungo tool covdata textfmt.ingestchecks thatgo env GOVERSIONequals the binary’s Go version (go_version_mismatch; every Reflowgocall runs withGOROOTunset, so thegofirst onPATHuses its own toolchain and an inheritedGOROOTof another release cannot breakcovdata), copies exactly the checked files into a private temporary directory, checks the copies again (cover_dir_changedif a file was replaced, grew or became a link after the checks), runsgo tool covdata textfmton that copy only, removes the directory afterwards, and requires the profile’smode:to equal the verified mode. Deriving the profile from a copy of the checked files means the document cannot describe a profile from another run. It maps import paths to repository files through thego.modfiles of the bound commit’s tree (not your current worktree), builds one canonical document, uploads it and prints the document ID, totals, batch counts and how many files each exclude glob matched. Byte-identical canonical documents (same profile, binding, exclude globs, producer and scope, including batch keys) dedupe: an identical document already stored in the collection is reused rather than stored twice. Batch keys are part of the document, so the same profile ingested with different--batchkeys, or once with and once without, is stored as separate documents. Profiles larger than 64 MiB or 2,000,000 blocks are refused astoo_large, never truncated.--batchis repeatable, up to 64 keys.Pass every batch that ran during the window, whatever its outcome: a batch whose runs failed, were skipped or ended unknown still drove traffic that is in the counters, and Reflow accepts any key that has runs in the collection (up to 1,000 runs per batch). An apply refused before admission (its preparation failed, its group was still owned) ran no flow and launched no process: when the server refuses such a key (
unknown_batch), ingest reads each key’s runs, leaves out the keys with no run at all, ingests the batches that ran and names the left-out keys inemptyBatchesand a warning.--processesstays the count you launched. Ingest still refusesunknown_batchwhen no key has runs, or when a refused key’s runs are in another collection. For each key the document reportssameBatchReusedFlows(runs in that batch with at least one step, every one of them reused) andexecutedFlows(the batch’s other runs: at least one step ran rather than being reused, or the run has no steps). A flow that the plan reused whole from an earlier batch creates no run in the new batch, so it is in neither count; pass the earlier batch’s key too if its traffic belongs to the window.reusedFlowsis a deprecated alias ofsameBatchReusedFlows. For the whole document, ingest also printsflows:executions(the batches’executedFlowssummed),distinctFlows(distinct flows among them) andcrossBatchReruns(a flow that ran again in a later batch of the window counts once indistinctFlows);documentsandshowreport the same.You may still pass a profile, as
[--format go-textfmt] <file>, for example one your own tooling already produced. It must be byte-identical to the derived profile, oringestrefuses it asprofile_mismatchand reports both sizes and SHA-256 digests.go tool covdata textfmtsorts packages and blocks, so the output is the same every time for the same cover directory. -
Inspect the stored result:
Terminal window reflow coverage status --latestreflow coverage status --latest --clean --top 20reflow coverage status --document <document-id> --json--latestselects the newest finalized document;--cleanlimits it to documents bound to a clean worktree. The output shows statement coverage first, then line coverage, the instrumented-package denominator and any exclusions, followed by the packages with the most uncovered statements (--top N, default 10, at most 100). The attribution line shows the tier:process (verified)orprocess (exit-only). Ties are ordered by package name. Packages beyond the 2,000-row limit are folded into one row; its uncovered statements are reported separately and never ranked. With--json, the result addspercent(statements,lines; after exclusions, two decimals,nullwhen there is nothing to count),rawPercent(the same figures without exclusions) andtopUncovered(package,uncoveredStmts,stmtsFound,stmtsHit,percent), plusexcludeMatches(see Excluding code from the denominator).
Use a new, empty cover directory and a new bind for each run window.
No app changes (exit-only)
Section titled “No app changes (exit-only)”If you cannot add the startup adapter or a harness module, you can still
measure a run window with a plain -cover build. This path is weaker than the
verified path above (setup (a) or (b)), which remains the recommended setup:
nothing checks the processes when they start, so Reflow checks the run only against
the files Go writes: the covmeta.* file written when an instrumented process
starts, and the covcounters.* file written when it exits. Documents ingested this way are marked exit-only.
-
Build from a clean, committed tree, with no adapter and no build tag. Go records
vcs.modified=truefor untracked files as well as modified ones, so build from a clean checkout or worktree:git status --porcelainmust print nothing.Terminal window go build -buildvcs=true -cover -covermode=atomic -coverpkg=./... \-o /tmp/app-cov/app . -
Bind a fresh, empty cover directory before running anything. Keep it outside the worktree, or git-ignore it:
Terminal window reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/dataWithout
-tags reflowcoveragein the binary’s build information, theNext:line describes this path: run withGOCOVERDIR, stop with SIGTERM, theningest --processes N. -
Start every process with
GOCOVERDIR=/tmp/app-cov/data, then apply your flows. Skipreflow coverage verify; it needs the adapter’s startup markers. -
Stop every process and wait for all of them to exit. A plain Go binary writes its counters only when
mainreturns or the program callsos.Exit. Go’s default SIGTERM handling kills the process without writing anything, so exit-only cannot rescue an application that does not return frommainon SIGTERM. Handle the signal as shown in Make SIGTERM return from main. -
Ingest, giving the number of processes that ran.
ingestderives the profile itself, exactly as on the verified path:Terminal window reflow coverage ingest --cover-dir /tmp/app-cov/data --processes 2 --jsonIf
ingestreportscounter_countwith no counter files at all, the processes may still be running: stop them with SIGTERM (mainmust return), then ingest again.
ingest --processes checks, in order: the binary still has the SHA-256
recorded at bind (binary_changed, binary_missing); no process is still
writing (counters_in_progress); exactly one covmeta.* file exists
(no_covmeta: the binary was not built with -cover, GOCOVERDIR was
wrong or unset, or no process started; multiple_covmeta); it is a valid Go coverage meta-data file
whose hash matches its contents and its name (covmeta_version,
covmeta_corrupt, invalid_mode, covmeta_hash_mismatch); every package it
describes is embedded in the bound binary (covmeta_not_from_binary); there
are exactly that many complete counter files from distinct processes
(counter_hash_mismatch, counter_count, duplicate_pid,
counter_incomplete), each written on the bound binary’s platform
(counter_platform_mismatch). Only then does it derive the profile, with the same
go env GOVERSION check (go_version_mismatch), the same private copy of
exactly the checked files (cover_dir_changed) and the same optional
byte-identical <file> comparison (profile_mismatch) as the verified path,
and require the profile’s mode to match the covmeta (mode_mismatch).
A cover directory that was verified refuses --processes; ingest it without
the flag.
Both tiers derive the profile from a copy of exactly the files they checked,
so neither document can include counters from outside the run window that
ingest checked. The tiers differ in what those checks can prove. What
exit-only cannot prove, compared with the verified path:
- No startup check. Nothing confirms that coverage was active in each
process when it started. A process that never started, crashed or was killed
without returning from
mainis detected only as a missing counter file (counter_count). - Process IDs are not registered in advance with
--processes <n>. Reflow counts the counter files that exist; it cannot tell that they came from the processes you meant to run. Record each start instead (below) to require exactly the processes you launched. - Another build of the same source is not detected when it differs only
in
-covermodeor uses a narrower-coverpkg: its packages are still embedded in the bound binary. Compare the document’s instrumented packages with the packages you expect;reflow coverage statusprints the count for exit-only documents (instrumentedPackagesin--json).
Exact process counts with launch records
Section titled “Exact process counts with launch records”Instead of a number, you can record every instrumented start as it happens and
have ingest require exactly those processes. No app change is needed, and the
process stays a direct child of whatever started it.
-
After
bind, start each process withGOCOVERDIRand record its pid:Terminal window GOCOVERDIR=/tmp/app-cov/data /tmp/app-cov/app &reflow coverage record-start --cover-dir /tmp/app-cov/data --pid $!record-startwrites an owner-onlyreflow-launch.<pid>.<16 hex>file that holds only the format, the pid and the time: never the command line, the environment, the working directory or a binary hash. It makes no network call. It refuses a cover directory without a binding (no_binding) or one that was verified (usage). -
Stop every process as above, then ingest with
--processes launched:Terminal window reflow coverage ingest --cover-dir /tmp/app-cov/data --processes launched --jsonThe pids of the launch records must equal the pids of the counter files:
- a counter file whose pid no record names is
unrecorded_counters(a process you did not record wrote into this directory); - a record without a counter file is
missing_counters(the process crashed, was killed or is still running); - no records at all is
no_launch_records, and a malformed record islaunch_record_invalid; - the same pid recorded twice is
duplicate_launch_record.
Two counter files from one pid stay
duplicate_pid, so pids must be unique within a cover directory. Every other exit-only check runs unchanged, in the same order. Both pid sets are in the refusal details. - a counter file whose pid no record names is
The ingest result reports processes with the source (count,
launched or verified), the expected and found counts and the pids.
The stored document itself does not carry the count yet: cite the ingest
result as the evidence of how many processes ran.
Profiles written somewhere else
Section titled “Profiles written somewhere else”When the processes write their profiles into another directory, for example a
VM guest whose GOCOVERDIR is exported to a new host directory, keep the bound
cover directory for the binding and point exit-only ingest (and check) at
the exported files instead of copying them:
reflow coverage ingest --cover-dir /tmp/app-cov/data --data-dir /exports/run-1/go --processes 2 --json--data-dir reads covmeta.*, covcounters.* and tmp.* from that directory
and ignores its other files; the binding and any launch records stay in
--cover-dir. Every exit-only check above applies unchanged. It requires
--processes <n> or --processes launched (usage otherwise), because
verify watches the cover directory itself, and it refuses a missing
directory or a link (data_dir_invalid). You assert that the directory holds
exactly this window’s files: like a copy into the bound directory, it has no
fresh-directory guarantee.
Check a cover directory before ingest
Section titled “Check a cover directory before ingest”reflow coverage check --cover-dir <dir> is a read-only, offline report. It
makes no API call. It reports:
- the binding, and whether it was verified;
- the
covmetahash and how manycovmetafiles exist; - the counter files, their distinct and duplicate pids, and their GOOS/GOARCH (read from each counter file; the command line Go records there is discarded and never printed);
tmp.*files that are still being written;- the launch records and any unmatched pids.
Without --processes it exits 0 and says the result is not asserted. With
--processes <n> or --processes launched it runs the same cover-directory
checks as an exit-only ingest, except the bound binary’s hash and package
scan, and exits non-zero with ingest’s refusal code (in error.code, beside
the report).
Containers and other platforms
Section titled “Containers and other platforms”The bound binary may be cross-compiled for another OS or architecture, for
example a Linux binary run in Docker from a macOS host. Build it on the host at
the bound commit with CGO_ENABLED=0 -buildvcs=true, bind it there, bind-mount
that exact file read-only into the container, and give the container a cover
directory it can write (a non-root image user that cannot write it produces no
covmeta). Ingest on the host with the same Go toolchain that built the binary.
Every counter file must have been written on the binding’s platform. Its
GOOS/GOARCH must equal the binding’s go version -m GOOS/GOARCH, otherwise
ingest refuses counter_platform_mismatch (a binding without GOOS/GOARCH is
refused too).
Each docker run makes the process pid 1, so a second start in the same cover
directory refuses duplicate_pid. Run the container with --pid=host and
record the pid Docker reports, or use one cover directory per start:
docker run -d --pid=host --name app -v /tmp/app-cov/app:/app:ro \ -v /tmp/app-cov/data:/cov -e GOCOVERDIR=/cov image /appreflow coverage record-start --cover-dir /tmp/app-cov/data \ --pid "$(docker inspect -f '{{.State.Pid}}' app)"docker stop sends SIGTERM, so main must return on it.
Reading the numbers
Section titled “Reading the numbers”- Statement coverage is the primary figure.
stmtsHit / stmtsFounduses Go’s own statement counts and matches the percentagego tool cover -funcreports for the same profile. - Line coverage (Reflow) is shown second. It counts source lines covered by Go’s statement blocks; a line is hit when any block on that line ran. It differs from Go’s statement percentage, so Reflow labels it explicitly.
- Denominator: instrumented packages. Only packages compiled with
coverage appear. Code you excluded from
-coverpkgis absent, not counted as uncovered, and every percentage is shown with that note. - Refused entries. Profile entries with absolute paths, or import paths that do not belong to a module in the bound tree (such as dependencies), are counted as refused and never stored.
Excluding code from the denominator
Section titled “Excluding code from the denominator”Generated code, test environments or vendored helpers can dominate the
denominator without being meaningful targets for end-to-end flows. Pass
--exclude <glob> to bind, once per glob:
reflow coverage bind --binary /tmp/app-cov/app --cover-dir /tmp/app-cov/data \ --exclude '**/testenv/**' --exclude '**/gen/**' --exclude 'internal/k8s/**' \ --exclude '**/*_gen.go'Globs are anchored at the repository root: they match the whole
repository-relative path of a file, with / separators. testenv/** matches
only a top-level testenv/ directory, not server/internal/testenv/env.go; to
match a directory at any depth, start the glob with **/, as in
**/testenv/**.
| Pattern | Matches |
|---|---|
** (a whole segment) | Zero or more whole path segments |
* | Any run of characters within one segment, never / |
? | Exactly one character other than / |
| anything else | Itself, literally (including ., [, { and spaces) |
So **/*_gen.go matches x_gen.go and a/b/x_gen.go, testenv/** matches
everything under testenv/, and *.go matches only files at the repository
root. Globs must be printable ASCII, 1 to 256 characters, without a leading
/, a .. segment or \; at most 64 are allowed. They are recorded in the
binding file and covered by its digest.
Each command reports how many files every glob matched, so a glob that misses is visible at once:
bindcounts tracked repository files: the tracked non-test.gofiles in the bound commit’s tree.ingestcounts instrumented profile files: the files in the canonical document, which only lists files of instrumented packages.statusprints the instrumented profile file counts the server recorded when it stored the document; documents stored before these counts existed show “counts unavailable”. Run inside a checkout that has the bound commit, it also prints the tracked repository file counts.
The two numbers differ whenever the build did not instrument every package a
glob covers (for example generated packages left out of -coverpkg), or an
instrumented package has files with no statements, which go tool covdata textfmt does not list (type-, const- or var-only files are common in generated
code). Files refused as absolute or outside the repository are not counted
either. Each line says which number it is.
A glob that matches nothing produces a warning that suggests the **/ form.
Exclusion is applied only when the numbers are aggregated. The uploaded
document still contains every file and the complete totals, plus the list of
globs. Package rows, the headline percentages and the top uncovered packages
use only files that no glob matches, and the output lists the globs with the
number of excluded files, statements and lines. rawPercent keeps the figures
before exclusion.
Expected sizes
Section titled “Expected sizes”A real Go service measured during design (Gram) had a denominator of 232,404 statements and 366,326 lines across 2,510 files in 463 package directories. Each process wrote about 1.9 MB of counters, a counter flush took about 3 ms on average, and ingest took 0.6 seconds. That is well inside the limits: 20,000 files, 2,000,000 ranges, a 64 MiB profile and a 16 MiB compressed document.
The document also lists the instrumented packages (their import paths). At
most 4,096 are allowed, each 1 to 256 bytes and 60 KiB (61,440 bytes) in total;
beyond that the document is refused as too_large. The list is stored complete,
before any --exclude, in a database field whose size cannot grow without
downtime, so these caps leave room for every pattern plus its JSON quoting. The
byte cap is usually the one reached first: Gram’s 655 packages take about
45 KB, and a repository whose import paths average 68 bytes reaches the cap at
about 850 packages. Import paths may contain only ASCII letters, digits and
-._~+!@, separated by single slashes (Go itself accepts only -._~+ besides letters and digits), so no
pattern grows when it is written as JSON; a path outside that set is refused
as invalid_canonical. Narrow -coverpkg if a very large repository exceeds
the caps.
Deleting a document
Section titled “Deleting a document”reflow coverage delete --document <document-id>delete retires one finalized document. Its uploader can delete it, and so can
a team owner or admin using an admin-scoped token (forbidden otherwise). The
document disappears from status at once, and its stored object is removed in
the background. Repeating the command while the document is still being
deleted succeeds; a repeat after it has been removed reports not_found. A
reservation that was never finalized is refused as not_finalized and expires
on its own. Otherwise
retention removes documents: each collection keeps its latest 50 clean and,
separately, its latest 20 dirty documents.
Swift and other LLVM languages
Section titled “Swift and other LLVM languages”Apps built with LLVM source-based coverage (Swift, C, C++, Objective-C) use
--format llvm-cov. Their documents are separate from Go documents: they report
line and region coverage, have no statement coverage, and are never blended with
a Go helper’s numbers. reflow coverage status summarizes Go documents only: for
an LLVM document it reports unsupported_format and names the reads to use
instead, reflow coverage show and reflow coverage files.
What you need:
-
An
llvm-covthat writes JSON export format 2.0.1 or 3.0.1. Reflow accepts only these two and refuses any other version asunsupported_llvm_export. Thellvm-covin Command Line Tools 27 writes 3.0.1. To check yours, merge the profile from a trial run (not in a bound directory) and read theversionat the end of a summary export:Terminal window xcrun llvm-profdata merge -sparse <trial>.profraw -o <trial>.profdataxcrun llvm-cov export -summary-only -instr-profile <trial>.profdata <binary> | tail -c 40The output ends with
"version":"3.0.1"}or"version":"2.0.1"}. On Linux, run the tools withoutxcrun.
- Build the instrumented binary, for example with SwiftPM:
swift build -c release -Xswiftc -profile-generate -Xswiftc -profile-coverage-mapping -Xswiftc -file-prefix-map -Xswiftc "$PWD=.". In Xcode, pass the same compiler flags (-profile-generate,-profile-coverage-mapping,-file-prefix-map <root>=.) to the target that builds the app. The prefix map keeps recorded source paths relative, but the result depends on the directory you build from (a package in a subdirectory can record a doubled prefix). Check one recorded file name, for example withllvm-cov export -format=lcov ... | grep -m1 ^SF:, and set--path-stripto match, or""when it is already repository-relative. - Bind a fresh, empty directory outside the worktree to the exact binary that
will run and the prefix to strip from recorded paths. For an app bundle that
is the Mach-O executable in
<App>.app/Contents/MacOS/. Bind the copy that actually launches: re-signing or copying from another build changes its bytes, and ingest refusesbinary_changed.reflow coverage bind --format llvm-cov --binary <binary> --cover-dir <dir> --path-strip <prefix>. If the recorded paths are already relative to the repository root (for example a build with-file-prefix-map <root>=.), pass an explicitly empty prefix:--path-strip ""(or--path-strip=). Every stripped path must be a file in the bound commit; other paths are counted as refused, not stored. - Run each instrumented process with
LLVM_PROFILE_FILE=<dir>/<name>-%p.profraw(%pgives one file per process;%malso works; do not use%ccontinuous mode; a fixed file name lets later processes overwrite earlier profiles). Children inherit the variable when the parent passes its environment through. Quit each process normally (for a Cocoa app, its own Quit command). By default the LLVM profile runtime writes the profile from its exit handler, so only a normal exit writes it: SIGKILL and a crash never do, and a signal the program does not handle (for example SIGTERM sent to an ordinary Cocoa app) ends it without writing. An app that handles SIGTERM or SIGINT itself and then exits normally does write its profile; rely on that only for apps whose handler you know. Wait until every process has exited before ingesting. - Ingest with the exact process count:
reflow coverage ingest --format llvm-cov --cover-dir <dir> --processes <n> [--batch <key>]....nis the number of instrumented LLVM processes you launched in the window, counted from what you started, not from the files you find. With%peach of them must leave one.profraw. Ingest refuses (profile_count_mismatch) unless exactlynprofiles exist, and refuses any other file in the directory. A missing profile is missing evidence, not 0%: never lowernto match the files, which would silently accept a process that was killed or crashed; fix the stop and start a new window. Profiles exported elsewhere (a VM guest) can be read in place with--data-dir <dir>; the binding stays in--cover-dirand the same checks apply to that directory.
Ingest merges the raw profiles with llvm-profdata, exports them with
llvm-cov (through xcrun on macOS) and refuses unless the per-line result
equals llvm-cov’s lcov line records and the regions equal its region summary.
It also refuses if the binary or the llvm tool versions changed since bind.
Line counts are LLVM’s per-line view; llvm-cov’s per-file line summary adds up
per-function counts and can exceed it. These documents are always exit-only:
nothing verifies at startup that profiling is active, and the binary carries no
commit stamp, so the source binding is the worktree you bound.
Coverage from a MacOSApp run
Section titled “Coverage from a MacOSApp run”The MacOSApp plugin launches an isolated copy of a macOS app for each step. To measure that app (and any Go helper it starts), set up one run window per producer and give the app the coverage variables through its binding.
-
One directory and one bind per producer. Create a fresh, empty directory outside the worktree for each instrumented program, for example
/tmp/myapp-cov/swiftfor the app and/tmp/myapp-cov/gofor a Go helper. Bind each one to the exact binary that will run:--format llvm-covwith the Mach-O executable inside the isolated copy named by the binding’sapplication, and the Go helper binary with a plainbind. The LLVM document is always exit-only. Go and LLVM documents are separate; never add their counts or quote one blended figure. -
Pass the variables through
environmentFile. In a Reflow run, the binding’s privateenvironmentFileis the only way to give the appGOCOVERDIRorLLVM_PROFILE_FILE: Reflow starts plugins with a fixed minimal environment, so variables exported in your shell or in the device service’s environment do not reach the app. The file is read on the device that runs the step, so use absolute paths on that machine:{"LLVM_PROFILE_FILE": "/tmp/myapp-cov/swift/myapp-%p.profraw","GOCOVERDIR": "/tmp/myapp-cov/go"}Keep it
chmod 600. The app’s children inherit these variables when the app passes its environment through, which is how a Go helper started by the app receivesGOCOVERDIR. Launching through LaunchServices oropeninstead may not propagate them. The step’s state records the file’s SHA-256 (inputs.environmentFileSha256), not its values. -
Apply your flows and keep each apply’s request identity as a batch key:
request.idempotency_keyfromreflow apply --json, or therequest_idyou pass tomanage_executionaction: "apply"over MCP. -
Count actual launches, not flows. Count each instrumented runtime separately, from the processes the window started:
- App (LLVM): every MacOSApp
runandinspectstep that launches the app starts one app process; add any other process of the same bound executable started in the window. Steps reused from earlier evidence and steps refused before launch start nothing. That total is the app’sn. A different instrumented binary, such as a Go helper or another LLVM helper, is a separate document with its own directory, bind and count. - Go helper: every Go process started with that
GOCOVERDIR: each helper the app started, each child that inherited the variable, and any other run of the same binary in the window.
Every counted process must write its file. MacOSApp quits the app through its own Quit command when it can, and the step’s state records how it stopped (
lifecycle.appStop,lifecycle.forced).graceful_quitwithlifecycle.forced: falseis a normal exit through the app’s own Quit, the safe path for a Cocoa app.terminatedmeans SIGTERM: whether a profile was written depends on how the app handles it; for an ordinary Cocoa app expect none.killedmeans SIGKILL, which writes nothing. A Go process writes counters whenmainreturns or it callsos.Exit; a crash or SIGKILL writes nothing, and SIGTERM writes only if the program handles it and returns. If a counted process left no file, the ingest refuses: do not lowern; fix the stop and run a new window. - App (LLVM): every MacOSApp
-
Ingest each directory with the same batch keys, after every process has exited:
Terminal window reflow coverage ingest --format llvm-cov --cover-dir /tmp/myapp-cov/swift --processes <app-n> --batch <key>reflow coverage ingest --cover-dir /tmp/myapp-cov/go --processes <go-n> --batch <key>A missing profile or counter file refuses the ingest. It is missing evidence, never 0%: start a new window rather than editing the count.
Browser JavaScript and TypeScript
Section titled “Browser JavaScript and TypeScript”The Browser plugin can measure the JavaScript a page
runs with V8 precise coverage, mapped through source maps to your TypeScript or
JavaScript files. It stores reflow.code-coverage/3 documents, separate from Go
and LLVM documents; coverage diff judges each changed file by the format that
measures it. It is off by default: the browser compiles page code with coverage
counters, which slows it.
-
Bind a fresh directory outside the worktree (or git-ignored):
Terminal window reflow coverage bind --format browser-js --cover-dir /tmp/web-cov/run1 \--web-root client/dashboard--web-rootis the repository directory the dev server serves URL paths from (Vite’sroot). Without it, each source maps to the one tracked file whose path ends in the URL path. -
Point the Browser binding at it with
"coverageDirectory": "/tmp/web-cov/run1"(Chromium only). After every step and when the browser closes, the provider records coverage for the target origin’s scripts, plus the transformed scripts and source maps. Scripts under/node_modules/and Vite’s own/@vite/modules are skipped. Each browser session writes one profile, marked closed only after its final record. -
Apply your flows, keeping each apply’s request identity as a batch key.
-
Ingest after every browser has closed:
Terminal window reflow coverage ingest --format browser-js --cover-dir /tmp/web-cov/run1 --batch <key>An unclosed profile (a killed browser) refuses
session_unfinished: that is missing evidence, never 0%.--processes <n>asserts the session count.
A line is instrumentable when the source map maps generated code to it; its count
is the innermost V8 block count there, so a branch that never ran is missed. Every
original source’s sourcesContent is checked against the bound tree’s Git blob:
a source the server served from another checkout is left out of the document and
named in a warning (served_source_mismatch when nothing else maps), never
attributed to the bound commit’s lines. A module no session loaded is absent from
the document, so coverage diff reports it as unmeasured. Counts a renderer
accumulated after the last record are lost when a navigation replaces it.
To record only which modules each run’s page loaded, without coverage, set
"loadedModules": true in the Browser binding: every snapshot then retains the
modules the session loaded so far, and reflow evidence diff compares both
runs’ served sources (see reflow evidence --help). A flow needs at least one
snapshot for this record.
Process attribution
Section titled “Process attribution”A document describes everything the instrumented processes executed during the run window: startup, background jobs and requests from anything else that talked to them, as well as your flows. Batch keys record which applies ran in the window and how many flows each executed or reused. They do not mean every covered line was caused by those flows.
Clean and dirty documents
Section titled “Clean and dirty documents”A document is clean only when git status --porcelain=v2 --untracked-files=normal --ignore-submodules=none printed nothing at bind time.
Otherwise it is dirty and records a digest of that status output. Dirty
documents are for local status only. They are never review or pull request
evidence. Build and bind from a committed, clean tree for results you want to
share.
Reading coverage as an agent
Section titled “Reading coverage as an agent”For a commit review with per-hunk verdicts across binary members, see
Changed-line coverage and binary windows.
The commit form is coverage diff --base <commit>; the UUID document-base form below
keeps its existing document-comparison behavior.
Agents and scripts read stored documents with read-only commands. Each
returns one bounded page with --json; pass nextCursor back unchanged with
--cursor to continue. In the local MCP connection (reflow mcp) the
query_code_coverage tool runs the same reads (operation: documents,
show, files, lines, gaps, diff, source, source_selection,
source_pending or source_revisions) and never writes. The remote HTTP MCP
connection does not offer it.
| Read | What it answers |
|---|---|
documents [--limit <n>] | Which finalized documents exist, newest first, with their exact IDs. |
show --document <id> | Source provenance (repository, commit, tree, clean or dirty), producer and tool versions, tier, batch evidence and metrics. |
files --document <id> [--query <substring>] | Files ranked by missed lines. |
gaps --document <id> | Non-excluded files with missed line ranges [start, end], largest gaps first. |
lines --document <id> --path <file> [--with-source [--from <line> --to <line>] [--revision <id>]] | One file’s instrumented line ranges [start, end, hits] and its missed runs, optionally with its source. |
diff --document <after> --base <before> | What changed between two documents. |
source --document <id> --path <file> [--from <line> --to <line>] [--revision <id>] | The text of one covered or selected file from the source snapshot. |
source-selection, source-pending and source-revisions --document <id> (MCP
source_selection, source_pending, source_revisions) are read commands too;
they read source metadata (see Which paths are snapshotted, and keeping source
in sync).
source-selection and source-pending are collection-level and take no
document ID.
To open a document in the dashboard’s coverage explorer, print its link:
reflow dashboard links coverage --document <id> [--base <document-id>] [--path <file>] [--collection <id>]It prints the hosted URL and, when one is running, the local dashboard URL,
after the server confirms the collection, the document, the base document and
that the file is instrumented in the document. --base takes an earlier
document ID, never a commit. A base measured at another commit opens as a
descriptive comparison (tree_changed) with no per-line gain. The MCP tool
get_dashboard_links takes kind: "coverage" with document_id, base_id,
path and collection_id (required on the remote HTTP MCP connection).
Every document read names an exact document ID. Copy it from documents; there
is no mutable “latest” alias in these reads. A document removed by retention or
delete answers not_found.
A metric that a producer does not measure is null and shown as “not
measured”, never as 0%. Go measures statements but not regions or branches;
LLVM (Swift) measures regions but not statements.
Source text
Section titled “Source text”When a document is uploaded, the server can also store a source snapshot: the exact bytes of the document’s files at its commit, plus any selected paths. The uploader reads them from the Git objects of the document’s commit, never from the working tree. Source is shown as source at the document’s commit, as attested by the uploader. The server cannot prove that those bytes were the compiler’s inputs.
source reads one file from that snapshot (serverSource, origin
server-snapshot). lines --with-source adds the same serverSource for the
file it reports.
- Windows: an explicit window (
--from,--to) covers at most 2,000 lines. Without a range, a file of at most 20,000 lines is read whole and a longer one returns its first 2,000 lines.--fromalone reads up to 2,000 lines from there, and--toalone reads from line 1. A range that starts past the end of the file is refused.partialis true exactly when the answer is not the whole file. - Verification: the document’s provenance must be complete and well formed
(repository, commit, tree, object format, and a
dirtyflag that istrueorfalse); otherwise nothing is shown and Git is never read. The client checks every answer against the document, commit, path and revision, including an answer that the file is unavailable: fields such as a document ID, canonical hash, kind or sizes that contradict the request refuse it. The served lines must be exactly the ones the request asks for, within the stored-file limits (1 MiB). A whole file is also verified by its SHA-256 and Git blob ID. A window is identity-checked only, not hash-verified. Forlines --with-source, the source must also belong to the same document, canonical hash and provenance as the lines it is attached to, and the file must have at least as many lines as the coverage measured. A snapshot summary that does not match its document (commit, selected and covered counts) is refused byshow,sourceandlines --with-source. - Revision: by default the read uses the document’s linked revision, which
need not be the newest, and asks the server for exactly that revision. If it
is relinked or removed between reading the document and reading the file, the
read refuses with
revision_changed: run it again.--revisionpicks another one;shownames the linked revision. - Not stored: a path can be unavailable with a reason:
excluded_path(credential files,.envfiles, VCS internals, vendored trees),secret_like,binary_or_non_utf8,too_large,quota,dirty_capture,not_in_document,not_snapshottedorcommit_unavailable. Secret screening is best effort: treat source text as sensitive.
In the app, open Code coverage for a collection:
/<team>/code-coverage?collection=<id>&doc=<document>[&path=<file>&view=source].
Source shows “Source at commit <commit>, as attested by the uploader”. A whole
file is labelled “bytes verified against the attested sha256 and Git blob”; a
window of a larger file is labelled “identity-checked, not hash-verified”.
Selected (non-instrumented) paths open with &kind=selected and have no
coverage marks. Privacy-withheld, quota, dirty or mismatched source is explained
and never offered a local copy. A git show command is offered, in the source
view only, when the coverage service stores no source snapshots or the document
has none, and only for documents captured without uncommitted changes. An
unavailable answer for a document with a snapshot (any reason, including
not_snapshotted and commit_unavailable) is shown as withheld by the server,
with its reason, and no local copy is offered.
When the document has no linked snapshot and the server answers
not_snapshotted or commit_unavailable, or the server does not support source
snapshots (unsupported), the reads fall back to your checkout. When a revision
was read (the linked one or --revision), those reasons describe that
snapshot’s entry for the path: they are reported, and the local text is
withheld (withheld_by_server). reflow coverage source and the MCP source
operation with --revision (revision) never read your checkout, even when
the server does not support source snapshots: the local text is withheld
(withheld_by_server) and Git is not read. lines --with-source keeps its
released fallback there (see below). localSource adds the text only when:
- the local Git repository has the document’s commit;
- that commit’s tree equals the recorded tree;
- the document was built from a clean tree.
If the lines or the document say the document was captured with uncommitted
changes, Git is never read (built_dirty). Such a document never shows source:
text the server sends for it anyway is refused (invalid_response).
The text comes from Git’s object at that commit (not from the working tree) and
is labelled local-checkout with its blob ID. Otherwise localSource names why
it is unavailable (for example built_dirty, commit_missing or
tree_mismatch). The fallback is always the whole file. For
lines --with-source, --from, --to and --revision apply to serverSource
only, including on a server without source snapshots.
reflow coverage source (and the MCP source operation) also gates its local
fallback, because it accepts any path:
- a path excluded by the shared admission rules (credential and key files,
.envfiles, VCS internals, vendored trees) is refused withexcluded_pathbefore any request is sent: no document or source read, and Git is not read; - the path must be a file the document measured, proven by its line coverage
for that document; otherwise
not_in_document, and Git is not read. Line coverage that is not well formed ([start, end, hits]integers, sorted and disjoint) is refused (invalid_response), also before Git; - these Git reads ignore replacement objects (
refs/replace), so the commit, tree and blob are Git’s own objects; - the committed entry must be a regular file (mode 100644 or 100755): a
symlink or other entry is
excluded_path, as at upload; - the blob’s original bytes (at most 1 MiB, read without decoding) must hash to
the blob Git names (
blob_mismatchotherwise), pass the same admission as an upload (too_large,binary_or_non_utf8,secret_like) and be valid UTF-8; a file that fails returns no text. A U+FFFD character that is really in the file is kept.
lines --with-source keeps its released behaviour: its local copy is the whole
measured file at the exact commit, without these extra admission checks.
Every other reason withholds local text too: Reflow never reads your checkout
for a path that the snapshot deliberately left out. dirty_capture is reported
locally as built_dirty. A failed source read never falls back. That
includes forbidden, busy, revision_changed, a transport failure
(transport_error), an answer for another document or commit
(identity_mismatch), and an answer that breaks the checks above
(invalid_response):
sourceexits non-zero;lines --with-sourcereportsreason: "error"inserverSourceand still returns the lines.
When the server serves the file, lines --with-source adds no localSource;
the local copy is only a fallback.
Size fields on an unavailable answer must be consistent: a file with N lines
has at least N bytes, and zero lines means zero bytes. Anything else is refused
(invalid_response) before any fallback.
For the coverage read commands, status and the MCP query_code_coverage tool,
error output names only the error code and HTTP status, never server text. Only
documented error codes are shown; any other value from the server is reported as
http_<status>. On the read commands and the MCP tool, a failed connection,
abort or timeout is transport_error with client wording only; another
unexpected failure in the MCP tool is coverage_failed with a fixed client
message.
Human output is a display, not the verified bytes: it is escaped, and
credential-shaped values (tokens, passwords, bearer values, URL credentials and
parameters) are redacted on a best-effort basis, on each original line before
escaping. Redaction can miss credentials it does not recognize. --json and
the MCP tool give the exact text.
Human output of the read commands (results and errors) is escaped with one
grammar: a backslash is
shown as \\, every control character (everything below U+0020, including tab
and line feed inside a value, DEL and U+0080 to U+009F) as \xHH, and
invisible format characters (bidi overrides and isolates, zero-width marks,
U+2028 and U+2029) as \u{HHHH}, in source text, paths and every server value.
Source text is printed one file line per output line, so a tab or carriage
return inside a line shows as \x09 or \x0d. Escaped values cannot move the
cursor, retitle the terminal, fake an output line or visually reorder a file
name, and a file that literally contains \x1b reads \\x1b, unlike an
escaped ESC. --json and the MCP tool keep the exact values, including the
verified text. Every other reflow coverage error message is escaped the same
way; status and sync results are not (control characters other than tab and
line feed are removed). The repository label in the provenance is not verified
against your remote.
Which paths are snapshotted, and keeping source in sync
Section titled “Which paths are snapshotted, and keeping source in sync”A snapshot always lists every file of the coverage document, excluded files included, each either stored or with a reason. It can also store paths outside the document:
- The collection’s selection set: up to 256 repository paths stored for
every document of the collection.
source-selectionprints the set and its version.source-selection set --source-path <path>... --if-version <n>replaces it (--clearsets the empty set).--if-versionis thecollectionVersionyou last read; if the set changed since, the command is refused withconflict. Changing the set needs a token withrun, or a team owner or admin in the app. It prints the new version and the number of paths. - A document’s explicit paths: the paths you pass with
--source-path. They stay with the document: later syncs keep them, and--source-pathadds to them.--replace-explicit(with--source-path) makes them exactly the paths given, and--clear-explicitdrops them. The set and the explicit paths together hold at most 256 paths; a sync that would exceed that fails withselection_too_largebefore uploading anything.
Every path is read from the document’s own commit, never from your working
tree. A path that does not exist at that commit is reported as
not_in_document.
After ingest. A successful ingest (a new document or an existing one)
then syncs that document’s source in the ingest’s own collection. That sync
has a 60-second bound, counted like --max-seconds below but from the start
of the sync (after the ingest); its elapsedMs counts from there too. The result’s
sync field and the human output report what happened. A failed or incomplete
sync never changes the ingest’s exit status, but it is reported as failed, not
as success. --no-sync skips it; --source-path <path> (repeatable) adds
explicit paths.
Later. A snapshot is a source revision. When the selection set changes,
older documents keep their old revision until they are synced again.
source-pending lists recent clean documents (the latest 10) whose linked
revision does not use the current set, and source-revisions --document <id>
lists a document’s revisions with the linked one marked. In the local MCP
connection, query_code_coverage offers the same reads as operations
source_selection, source_pending (limit up to 20) and source_revisions.
They are metadata only and never write. Of the coverage operations, the
review-reader MCP profile offers just these three, with no source text and no
local Git.
reflow coverage source sync uploads new revisions from your checkout:
--document <id> [--source-path <path>]... [--replace-explicit | --clear-explicit]syncs one document;--pendingsyncs the pending documents.--max-documents <1-3>and--max-seconds <1-60>bound one run. The time bound starts with the command and covers finding the collection and the checkout, Git work, compression and every request. At the bound (or on Ctrl-C) a running Git process is killed, and the command waits for that process itself to end (while it looks up the checkout, at most 2 more seconds) before it reports. Reflow does not signal a process that Git itself started; it closes its own end of Git’s output, so such a process may get an error when it next writes. A request to the Reflow API that is already in flight cannot be cancelled: the command stops waiting for it, and the request ends on its own timeout. The result’selapsedMsis measured from the command’s start. A document it stops before any upload step is reportednot_attempted(budget); Ctrl-C at that point reportsnot_attempted(cancelled). Once an upload step has begun, an interrupted document isunconfirmed, and the documents after it arenot_attempted(stopped).- It needs the document’s commit in the local repository and never fetches,
even in a partial clone, and ignores Git replacement objects. A commit (or a
file of it) that Git reports missing, with no error, is skipped as
commit_unavailablewithout using up--max-documents. An object Git cannot read (for example a corrupt one) and any other Git error fail the document (git_failed). Dirty documents get no source. - Every path stored without source is listed with its reason
(
secret_like,excluded_path,too_large, …), never its content. - It exits non-zero unless every document is
createdorexisting(linked and current) orskipped(commit_unavailableordirty).stale_selection(the set or the linked revision changed meanwhile),conflict,revision_limit,quota,integrity,failed,not_attemptedandunconfirmedall fail the run. - It prints every revision ID it learned and never repeats an uncertain step
blindly. After an
unconfirmedoutcome, run sync again: it reads the revision’s state from the server and continues from there, uploading a reserved revision with a fresh upload grant or linking a finished one. - Its human output (and the post-ingest sync lines) prints each outcome’s reason
and revision IDs as the server sent them: control characters other than tab
and line feed are removed and credential-shaped values are redacted on a
best-effort basis, but the read commands’ documented-code filter and escaping
grammar do not apply. A refusal’s message is escaped, but it may carry a
server-supplied code or value (for example
source-selection set’s error code).--jsongives the exact values. - A document keeps at most 8 revisions. At the limit, sync refuses with
revision_limit; only--evict-unlinkeddeletes the oldest revision that is not linked. The linked revision is never deleted. conflictmeans another revision has the same selection with different contents.--supersede <revisionId>(with--document) stores a distinct revision that supersedes it.
A revision built against a set that has since changed is stored but not linked, and a newer link is never overwritten; the document stays pending. When the set changes in a way that leaves a document’s paths the same, the next sync links the existing revision for the new set without uploading again.
Source sync runs only from the CLI for now: after ingest, or as
reflow coverage source sync. An MCP tool for agents, and background sync, are
not available yet.
Before and after a new test
Section titled “Before and after a new test”Start with coverage-guided testing to choose a useful uncovered behavior and decide what to keep. The Code coverage view’s history shows retained capture totals; a plateau is a prompt to inspect gaps, not proof that the suite cannot improve or that equal totals cover equal code.
Coverage is process-wide: nothing attributes lines to a single flow. To learn
what one new test adds without running the existing suite again, run only the
new test in its own window and compare cumulative documents built from saved
raw files: see cumulative coverage and test gain.
That page also covers how diff classifies a pair (likeForLike or
descriptive) and the refusals to expect.
When to stop adding tests
Section titled “When to stop adding tests”- Zero added coverage is advisory, not proof that a test is redundant. A new assertion, an important boundary value, a regression case or a different oracle on the same path can matter without running new lines. Keep such a test and note why (“zero-delta, kept because …”).
- Stop duplicating equivalent scenarios: the same class of inputs, the same path and the same assertions as an existing test. Coverage alone cannot show that; compare the tests’ stated purposes and oracles too.
- Never skip, disable or delete a test from coverage data alone. Flag it for review with its purpose beside the delta.
- Do not add exclusions to make numbers pass, and do not chase excluded files.
- Prefer gaps that carry risk over raising a percentage.
Command reference
Section titled “Command reference”| Command | Purpose |
|---|---|
reflow coverage bind --binary <path> --cover-dir <dir> [--collection <id>] [--repository <label>] [--exclude <glob>]... | Tie a fresh cover directory to the binary and current commit. |
reflow coverage bind --format llvm-cov --binary <mach-o> --cover-dir <dir> --path-strip <prefix> | Same for an LLVM (Swift, C, C++, Objective-C) binary; --path-strip "" when recorded paths are already relative. |
reflow coverage verify --cover-dir <dir> --processes <n> [--timeout-ms <ms>] | Check that exactly n instrumented processes started with coverage active. |
reflow coverage record-start --cover-dir <dir> --pid <pid> | Record one instrumented start for ingest --processes launched (local, no network). |
reflow coverage check --cover-dir <dir> [--processes <n>|launched] | Offline report of a cover directory; with --processes, apply the exit-only checks. |
reflow coverage ingest --cover-dir <dir> [--processes <n>|launched] [--batch <key>]... [--source-path <path>]... [--no-sync] [[--format go-textfmt] <file>] | Derive the profile and upload one canonical document for the run window (--processes for exit-only), then sync its source. |
reflow coverage ingest --format llvm-cov --cover-dir <dir> --processes <n> [--batch <key>]... | Merge exactly n LLVM profiles and upload one exit-only document. |
reflow coverage status [--document <id> | --latest [--clean]] [--top <n>] [--collection <id>] | Show statement and line coverage and the most uncovered packages. |
reflow coverage delete --document <id> [--collection <id>] | Retire one finalized document. |
reflow coverage documents [--limit <n>] [--cursor <c>] | List finalized documents and their IDs. |
reflow coverage show --document <id> | One document with provenance, evidence and metrics. |
reflow coverage files --document <id> [--query <substring>] [--limit <n>] [--cursor <c>] | Files ranked by missed lines. |
reflow coverage gaps --document <id> [--limit <n>] [--cursor <c>] | Missed line ranges in non-excluded files. |
reflow coverage lines --document <id> --path <file> [--with-source [--from <line> --to <line>] [--revision <id>]] | One file’s line ranges and missed runs, optionally with its source. |
reflow coverage source --document <id> --path <file> [--from <line> --to <line>] [--revision <id>] | One file’s text from the source snapshot, or the allowed local fallback. |
reflow coverage diff --document <after> --base <before> [--limit <n>] [--cursor <c>] | Like-for-like gain or a descriptive change between two documents; across commits, with both commits in the checkout, it leads with the commit’s changed lines and their head hit state, then lists unchanged lines that newly ran or stopped running as secondary. |
reflow coverage source-selection | The collection’s source selection set and its version. |
reflow coverage source-selection set (--source-path <path>... | --clear) --if-version <n> | Replace the collection’s source selection set. |
reflow coverage source-pending [--limit <n>] | Recent clean documents whose source does not use the current set. |
reflow coverage source-revisions --document <id> | A document’s source revisions, the linked one marked. |
reflow coverage source sync (--document <id> [--source-path <path>]... [--replace-explicit | --clear-explicit] [--supersede <revisionId>] | --pending) [--max-documents <n>] [--max-seconds <s>] [--evict-unlinked] | Upload source revisions from your checkout. |
reflow dashboard links coverage --document <id> [--base <document-id>] [--path <file>] [--collection <id>] | Hosted and local coverage explorer URLs for one document. |
Every command accepts --json. Refusals are named (for example
unstamped_binary, vcs_mismatch, process_count, missing_counters,
profile_mismatch or mode_mismatch) and exit non-zero.