Record fs.lstatSync/statSync kind as payload-free stat records#66
Merged
Conversation
…ords lstatSync/statSync (and their async forms under --fs=async) were serve-only: a path that was ONLY stat'd -- never read, readdir'd, or imported -- was not recorded, so on a bundle=load run its stat fell through to the real fs and threw ENOENT once the path was gone from disk. Capture now records the observed KIND as a payload-free stat record: formats[file] = 'stat:file' | 'stat:directory' -- no bytes, no hash, no module-files entry. That is exactly what the load-side shim serves (the type getters isFile()/isDirectory(); every other Stats member stays real-if-on-disk / benign-synthetic-once-gone), so only type getters are supported, and the existence probes (existsSync/accessSync/realpathSync) answer from the record too. The caller always receives the real Stats (and real errors) at capture; symlinks/sockets/FIFOs aren't modelled and record nothing; escaping-symlink and sidecar-attested paths are skipped like the byte readers; options forms (bigint, throwIfNoEntry) pass through uncaptured. Stat records are deliberately WEAK: real content recorded for the same path supersedes them -- in-place (addFile/addFsDir drop the stale tag, so check-then-read never conflicts), across lock=add/bundle=add re-runs (the format-flip guard and #mergedFormats upgrade a stat baseline), and at bundle serialization (a record a write-mode sidecar upgraded is dropped so the bundle keeps cross-checking against the unified lockfile). addFile refuses an explicit 'stat:*' format, and a forked child's stat records ride the shard channel: shardSnapshot forwards the formats entry and mergeShard replays it by re-deriving the kind from disk. Since getFsStat now answers 'file' for byte-less records, callers that mean "the bundle carries this file's bytes" -- the parentless CJS re-resolution shim and attestedBySidecar -- switch to the new hasFsFileContent. Implied ancestor directories are derived from stat records too, stasis build allowlists the new formats, and the loader fails closed with a dedicated message on importing a stat-only path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017z3Eb4Udqc5BkdQtt2YA72
There was a problem hiding this comment.
Pull request overview
Extends Stasis’s --fs capture/load system to persist payload-free stat observations (stat:file / stat:directory) so fs.lstatSync/statSync kind checks (and related existence probes) can be served from the bundle even when the path is later removed from disk.
Changes:
- Introduces new
KNOWN_FORMATSentries for payload-free stat records and plumbs them through capture, merge/upgrade logic, and load-time stat serving. - Adds
State.addFsStat()plus helpers (getFsStatstat-format branch,hasFsFileContent, merged format upgrade rules, shard replay) to keep stat records weak and safely superseded by real content. - Adds comprehensive tests/fixtures and documentation updates covering stat-only behavior, upgrades, async variants, symlink security, and shard-channel propagation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/fixtures/cli-run-fork-shard/src/worker.js | Adds a child-process-only lstatSync probe to validate shard propagation of stat-only records. |
| tests/fixtures/cli-run-fork-shard/src/statonly.dat | Fixture file for the shard stat-only probe. |
| tests/cli-fs.test.js | Adds test coverage for stat-only records, upgrades to content records, async stat capture, symlink escape behavior, and shard forwarding. |
| stasis/src/cmd/build.js | Allows stasis build to accept stat:* formats in artifacts. |
| stasis-core/src/util.js | Adds stat:* to KNOWN_FORMATS and introduces isStatFormat(). |
| stasis-core/src/state.js | Implements recording/serving of stat-only records, weak-upgrade merge logic, sidecar skip correctness (hasFsFileContent), implied-dir inference for stat records, and shard replay. |
| stasis-core/src/hooks.js | Improves error messaging when attempting to import a stat:* format, and adjusts CJS resolution to require byte content. |
| stasis-core/src/fs.js | Captures stat kind during lstat/stat in write mode, while serving kind from bundle in load mode (sync + async). |
| doc/file-formats.md | Documents stat-only record semantics and their role in --fs capture/load. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mergeShard replayed a child's payload-free stat records with lstatSync, which reports the link itself for a path that is a symlink on disk -- but a child's statSync capture follows links and records the TARGET's kind keyed at the requested path (pnpm's public node_modules/<pkg> links are exactly this shape), so every such record was silently dropped at the merge and a later bundle=load run ENOENT'd on it. Replay with statSync instead: it reproduces the recorded kind for every key a legitimate shard can carry (an lstat of a symlink was never recorded at all, and for non-links lstat and stat agree). Because statSync follows links, re-assert the capture side's containment invariant (fs.js captureStat's realContained) before attesting: the real location must stay inside the root's real path, so a shard entry keyed at an in-root symlink pointing out of the tree is skipped, never recorded. A dangling link now falls into the existing gone-from-disk skip. Reported by the Copilot review on #66. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017z3Eb4Udqc5BkdQtt2YA72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends the
--fsfilesystem capture feature to record the kind (file vs directory) of paths that are stat'd but never read or imported. These are stored as payload-free stat records (stat:file/stat:directoryin formats) that allow the bundle to serveisFile()/isDirectory()type checks at load time even after the file is deleted from disk.Key Changes
New stat record format: Added
stat:fileandstat:directoryas payload-free formats inKNOWN_FORMATS. These records attest only to a path's existence and kind, with no bytes or hash entries.Capture-side recording:
state.addFsStat(url, kind)to record stat observations as weak, payload-free entriescaptureStat()helper infs.jsto capture the kind from realStatsobjectsfs.lstatSync()andfs.statSync()to record observed kinds during capture while passing through real Stats to the callerLoad-side serving:
getFsStat()to return the recorded kind forstat:*format entrieshasFsFileContent()to distinguish between actual file content vs payload-free stat records (used by resolution shims and sidecar skip logic)Format upgrade logic:
addFile/addFsDirdropping stale stat entries)mergeFormat()in#mergedFormats()to handle stat-to-content upgrades across lockfile baselines and sidecar mergesSecurity & consistency:
shardSnapshotandmergeShardDocumentation: Updated file-formats.md and fs.js comments to explain the new stat record semantics and their role in serving type checks at load time.
Notable Implementation Details
addFsStat()method guards against recording beside existing content (hashes, sources, or resources) to maintain the invariant that stat records carry no payloadstat:fileatnode_modules/dep/index.jsimpliesnode_modulesandnode_modules/depare directorieslstatSync(path, options)with options is out of scope and passes through untouchedhttps://claude.ai/code/session_017z3Eb4Udqc5BkdQtt2YA72