ID

R925

Status

Backlog

Bucket

correctness

Priority

2

Theme

dev-loop

Created

2026-09-06

Updated

2026-09-06

A java_file row’s stamp and its declarations come from one read of the file

Goal

A row in the java_ family describes one read of one file, so the content hash it carries and the declarations beside it can never come from two different versions of that file. The java_ family is the store’s record of what a consumer’s .java sources declare, refreshed on the source-save cadence by the dev loop; a stamp is the content hash a row is retained against, and a row is skipped whenever the file still hashes to it. Today the two halves of a row are read at two different moments, so a file saved in the gap between them produces a row whose stamp says "current" and whose declarations are one version behind, permanently: every later refresh hashes the file, finds the stamp matching, and skips it.

The gap, in the order it happens

SourceWalker.walkFiles parses the file and returns a ParsedFile. JavaSourceFacts.refresh then calls ClasspathSources.hash(file.file()), which opens the file again. Between those two reads the file can move, and on the save cadence that is not a rare interleaving: the walk is what a save triggered, and an editor writing a second time (or a formatter, or a build regenerating sources) is ordinary.

When it happens the row is written with declarations from version one and the hash of version two. Nothing afterwards notices: the next refresh hashes version two, compares it against the stamp, finds them equal, and retains a row describing declarations nobody wrote. The symptom is goto-definition and hover answering from positions one edit behind, for one file, until the session restarts.

SourceWalker’s own cache has the same shape one layer down. It records `mtimeOf(f) after parsing, so a save landing between the parse and that stat writes the new modification time beside the old declarations, and the walker never re-parses the file either.

Not what the observation changed

Worth stating because the two look alike and the item that introduced the observation should not be blamed for this one. The observation only ever lets a pass skip a hash, and the file above is skipped for a different reason: its stamp genuinely matches the bytes on disk. The behaviour is identical before and after that item, and hashing every walked file (which is what the loop did before) never caught it either.

What did change is that the defect is now easier to see, because the writer states in its own javadoc that a stamp vouches for the rows beside it.

Sketch

The fix is to make one read produce both halves, rather than to add a second check. The walker already opens the file to parse it, so the shape to reach for is a ParsedFile that carries the content identity of the bytes it parsed, computed from those bytes rather than from a later reopen. Then JavaSourceFacts stamps what it was handed and never opens the file at all, read_at dates exactly that read, and the walker’s own cache keys on the same identity instead of on a modification time it stats afterwards.

Spec should also decide what the walker does about a file it cannot hash, and whether the identity belongs on ParsedFile or on a small record beside it, since the walker is also the LSP’s.

Provenance

Found in the adversarial review of the item that added the observation and read_at, while checking whether the comparison could make an existing staleness permanent. It cannot, and this can, by a route that predates it.