The key fuzzer: a differential probe for stale hits (2026-10-02)
Status: a tool, run by hand. scripts/fuzz-keys.ts is not a gate
task: a run is minutes of vx run spawns, and a seed that finds nothing
proves nothing about the next. Run it after a change to how a key reads
the worktree (src/cache/git-inputs.ts, inputs.ts, file-hashes.ts,
key-fold.ts, the hit path).
cd packages/vxbun scripts/fuzz-keys.ts <scratch-dir> 300 <seed> # a few seeds, in parallelbun scripts/fuzz-keys.ts <scratch-dir> 300 <seed> --policies # random --force / --no-cache / --cache-policy=The method
Section titled “The method”The workspace’s tasks print what they read: each listed src file’s
name, mode bit and bytes or link target, a constant from an imported
conf.mjs, a cached producer’s output read by content (tasks: []), a
workspaceFiles input, an uncached producer’s output (it writes a
passed-through env value and a file outside its project), and the
upstream’s output. A symlink prints the bytes behind it, not only its
target string. After each random
edit, one vx run build --all, then every output is compared with what
the current inputs produce. A mismatch is a hit that replayed bytes its
key does not describe, the worst failure class; a seed replays it
exactly.
The edits are the ones a key can miss: content, a same-size rewrite with
the times put back (forge), chmod, symlinks, renames, git add,
commit, stash and pop (conflicts included), checkout, reset, add -N,
index flags (assume-unchanged, skip-worktree, --chmod),
.gitignore, .gitattributes, core.autocrlf, setting or unsetting the
passed-through env value, and tampering with declared outputs. Symlinks
point at files and directories inside and outside the project; a run that
refuses an undeclared link target is correct, and a run that accepts one
is reported (UNREFUSED).
What it found
Section titled “What it found”Three stale hits in its first afternoon, each a fix with a row:
- A-59:
git statuspaired a deleted file with a similar unmerged path as its rename source and printed onlyUU <path>(--no-renames). - A-60: a blob a removed clean filter wrote stood for the CRLF bytes on disk (the blob-size check).
- A-61: a gitlink whose directory lost its
.gitlisted none of its files (walkFiles).
Two more once it read through links and gained an uncached producer
(2026-10-16): a link whose target was no input (now keyed behind the
link, linked-inputs.ts), and an uncached producer’s passed-through env
value missing from its dependents’ keys. Its link targets reach a file
or directory in the project, elsewhere in the workspace and outside it,
with edits behind each.
The one probe of the script itself: with the file-hash memo’s ctime and inode checks deleted, seeds 1 to 3 each report a stale hit by step 23.
What it does not cover
Section titled “What it does not cover”The oracle reads git’s view of src, so an embedded repository there is
out of its set (A-61 was found by a find-based variant). Two clones
sharing a remote layer, and two runs sharing a --cache-dir at once,
were fuzzed the same way (about 5,600 steps, clean) by variants not kept here.