Selectivity 0.2 Removes Words and Characters and Checks Candidates in Parallel
The first release worked in lines. That’s a good unit for logs, CSV exports and config files, and a poor one for the input that is one enormous line: minified JSON, a generated query, a log record someone glued together. Selectivity 0.1 would hand that line back untouched, because removing it lost the bug and there was nothing smaller to try.
Version 0.2 adds two finer units. --units tokens goes on from lines to words, where a word carries the space or newline after it, so alpha omega stays readable while it shrinks. --units chars goes all the way to single characters. When more than one level runs, Selectivity repeats the whole sequence until a round removes nothing, so at the end no single line, word or character can go without losing the bug.
Here’s the released 0.2.0 Linux binary on a generated 21,311-byte line of fieldN=value pairs, one of which hides ERR42. The predicate is a plain grep -q ERR42, one observation per check:
--units lines 21311 -> 21311 bytes 3 runs
--units tokens 21311 -> 16 bytes 21 runs value=zzERR42zz
--units chars --jobs 4 21311 -> 5 bytes 58 runs ERR42
Lines is still the default. That was a review call, not the plan we started with. Finer passes end by trying every surviving unit on its own, and on a big input that keeps most of its text that can take thousands of extra runs. A script that finished under 0.1 could suddenly hit its budget and exit with code 2. So the finer units are something you ask for when a line is too coarse, and the README spells out what they cost.
Several checks at once
Most of a reduction’s time goes into running your predicate, not into Selectivity. --jobs N runs up to N candidates of the same step side by side, each in its own temporary copy. When the group finishes, Selectivity keeps the earliest candidate that still showed the bug, which is exactly the one a one-at-a-time run would have kept. So a predicate that always answers the same way gives the same result with any --jobs.
We tried it on 64 lines where the two needed lines sit at opposite ends, with a predicate that sleeps 0.2 seconds before it greps. Three runs each on a two-core container: about 7.0 seconds with one job, 3.7 with four, and 3.7 with eight. The result was the same 12 bytes every time. Parallel runs did 49 checks against 34, because a group finishes all its checks even after an earlier one wins. Those extra checks are in the attempt log, marked speculative. They never change the result, and an unresolved one among them doesn’t cost you the locally-minimal claim.
Two caveats. Speculative checks spend budget, so a tight --max-tests can stop a parallel run sooner than a sequential one. And --jobs is only safe when the predicate keeps its state inside its own copy and working folder; one that writes a fixed temp file or binds a fixed port will give wrong answers. The default stays at one.
The independent review before release went through the parallel window hardest. It fuzzed random inputs at one, two, five and 64 jobs and got identical results and identical acceptance paths each time. It also caught the default problem above, a report that showed a units setting a directory run never used, and a timing test that was always going to fail on a slower Windows runner. Which it then did, once, before the fix.
Under the hood, 0.2 also moves to a supported Go release, and CI now runs the tests on Windows as well as Linux and macOS. The quick start has the new options, the reference explains them, and the download page has the 0.2.0 archives.