Skip to content

fix(state): drop a stale stat:file from the lockfile once content upgrades it - #148

Merged
ChALkeR merged 1 commit into
mainfrom
claude/shasums-stat-file-e0y4ri
Jul 28, 2026
Merged

fix(state): drop a stale stat:file from the lockfile once content upgrades it#148
ChALkeR merged 1 commit into
mainfrom
claude/shasums-stat-file-e0y4ri

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Summary

A path only stat'd under --fs records a payload-free stat:file (a formats tag, no hash/bytes). When a separate --lock=add run absorbs that lockfile and then fs-reads the path, the lockfile ended up attesting both a content hash and a payload-free stat:file for the same path — breaking the "stat records are payload-free" invariant and disagreeing with the bundle.

Root cause

  • addFile deletes the stat:* tag from the live this.formats when content arrives, but only writes a replacement format when one is known. A loader-authoritative .js/.ts (module vs commonjs is the loader's call) produces no format.
  • #mergedFormats rebuilds the lockfile's formats by seeding from the absorbed #lockFormats (which still holds stat:file) and overlaying this.formats. With no entry in this.formats to override it, the stale stat:file survived next to the new hash.
  • The bundle is built from this.formats directly, so it correctly had no format — hence the lockfile/bundle disagreement.
  • A later frozen run that imports the file tripped #assertAttestedFormat's strict equality ('module' !== 'stat:file').

Only genuinely-ambiguous .js/.ts were affected; extension-determinable formats (.mjs/.json/resource/…) already win the merge and never desynced. directory/stat:directory are also unaffected — a real directory overrides stat:directory via reconcileFormat.

Fix

Enforce the invariant at the merge boundary in #mergedFormats: a stat:* never survives on a file that carries a content hash. The genuinely-ambiguous .js/.ts falls through to no format — matching the bundle, and served at load by content presence (getFsStat), not a format tag.

Tests

  • tests/cli-fs.test.js — a cross-run stat → fs-read upgrade of a .js leaves no stale stat:file beside its hash; .mjs/.json keep their real format; lstatSync().isFile() still works at load with the file gone from disk.
  • tests/fs.test.jsgetFsStat answers file on content with no format; lockData keeps a pure stat-only record but drops one superseded by content.

Verification

  • New tests pass; oxlint clean.
  • Ran the lockfile/format/stat/sidecar/bundler/command suites — no regressions (5 pre-existing CommonJS/CLI spawn failures reproduce on a clean tree, unrelated to this change).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JHHftnppTgJEkEsUP7hqTg


Generated by Claude Code

…rades it

A path only stat'd under --fs records a payload-free `stat:file` (a formats tag,
no hash/bytes). When a SEPARATE --lock=add run absorbs that lockfile and then
fs-reads the path, addFile deletes the tag from the live `this.formats` and
records real content -- but for a loader-authoritative .js/.ts (module vs
commonjs is the loader's call) no replacement format is produced. #mergedFormats
then re-seeded the `stat:file` from the absorbed #lockFormats with nothing to
override it, so the lockfile attested BOTH a content hash and a payload-free
stat:file for the same path: it broke the "stat records are payload-free"
invariant and disagreed with the bundle (whose formats come from `this.formats`,
which had the tag deleted). A later frozen run that imported the file tripped
#assertAttestedFormat's strict equality ('module' !== 'stat:file').

Enforce the invariant at the merge boundary: a stat:* never survives on a file
that carries a content hash. Extension-determinable formats (.mjs/.json/
resource/...) already win the upsert, so only the genuinely-ambiguous .js/.ts
falls through to no format -- matching the bundle, and served at load by content
presence (getFsStat), not a format tag.

Tests: a cross-run stat->fs-read upgrade of a .js leaves no stale stat:file
beside its hash (and .mjs/.json keep their real format); getFsStat answers
isFile on content with no format; lockData keeps a pure stat-only record but
drops one superseded by content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHHftnppTgJEkEsUP7hqTg
@ChALkeR
ChALkeR merged commit 802ab55 into main Jul 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants