Defer bundle writes until all validations pass - #162
Open
exo-nikita wants to merge 3 commits into
Open
Conversation
`stasis add <dir>` already globbed a directory into every file under it, but then packed whatever it found: a `types.d.ts` went in as `module-typescript` code (types only, erased at runtime -- the resolvers refuse to land on one, so no loader can ever serve it), a `web.env` as `env` code carrying secrets, and `add .` attested the very bundle it was writing, so the next run conflicted with the recorded copy. None of those are caught by the `resources` check -- they all classify as code. `add` now runs in four ordered steps: - GLOB: expand each directory (recursive, sorted so nothing depends on readdir order), tagging each path as swept or named by the caller, and carrying the stat the walk already took into validation instead of re-probing the inode. - FILTER: drop the auto-excluded set from what the sweep FOUND -- type declarations, the `.env` family, stasis's own outputs (by name, plus the configured bundle targets), and `*.map`/`*.js.flow` sidecars unless their extension is declared in `resources` (matching --fs's `map` opt-in). A path named explicitly is never filtered, so `add .env` still captures. Dropped files are counted in the summary line, never skipped silently. - VALIDATE: everything that survives still has to be recognized source or a declared resource, now checked across the whole set before any target is touched -- one error names every offender instead of one per re-run (a lone offender keeps its exact message). - ADD: unchanged merge semantics (add-if-missing, divergent bytes for an attested path are a conflict), except every target's merge is computed before the first write, so a refused run leaves no half-updated project behind. isStasisArtifactName moves to util.js (shared with --fs, which had the only copy) and gains basename handling for the path-shaped callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KrgpYdYarjgv8ur9kpBpia
exo-nikita
force-pushed
the
claude/stasis-core-add-directories-niat1q
branch
from
July 28, 2026 19:13
8494fc6 to
606fb01
Compare
…e set Directories a sweep no longer descends into, matched by segment at any depth: `.git`, `.github`, `.settings` (VCS/CI/IDE metadata), `example`/`examples` (sample apps import the package, they aren't it), `__tests__`/`__mocks__`/ `jest` (test scaffolding), and Apple prebuilt slice dirs (`ios-arm64...`, compiled output). Files: the auto-excluded set now also covers everything the native capture skips as non-build-input noise -- NATIVE_EXCLUDE_EXTS and NATIVE_EXCLUDE_NAMES, i.e. docs/legal, editor/lint/CI config, logs, and the `*.map`/`*.js.flow` sidecars -- on top of type declarations, the `.env` family and stasis's own outputs. Exclusions apply to what a GLOB found, never to what the caller named, so any of these is still addable by naming it: `add .github/workflows`, `add src/ examples`, `add README.md`. Two consequences of that rule: - Directory rules are checked against the match path relative to the NAMED root, not the project root, so `add src/examples` sweeps the dir it points at while `add src` skips the one it merely found. - `resources` no longer un-excludes a swept `*.map`/`*.js.flow`. It decides what a file IS once the sweep offers it, not whether the sweep offers it; naming the file is the way in, and the declaration then makes it a resource. A path a run reaches more than once keeps its most specific provenance -- named beats swept, nearer root beats farther -- so `add src src/examples` includes the subtree it names regardless of the order the two entries are listed in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KrgpYdYarjgv8ur9kpBpia
The sweep exclusions added a third copy of a directory list `stasis-plugins`
and `stasis` already carried: metro.js's NATIVE_WALK_SKIP_DIRS and
bundle.js's NATIVE_SKIP_DIRS. The two existing ones had already drifted --
bundle.js was missing `.github`/`example`/`examples`, metro.js was missing
`DerivedData` -- which is what three hand-typed copies of one policy do.
util.js's `isAutoExcludedDir` is now the single "not the code being shipped"
name rule (metadata, sample apps, test scaffolding, Apple slice dirs), and both
walks compose it with their own build-output dirs (`build`, `.gradle`, `.cxx`,
`Pods`, `DerivedData`, `node_modules`). That folds in each walk's separate
`isAppleSliceDir` call, and fixes metro's `#hasPodspec`, which checked the set
without the slice-dir rule its sibling call site had. Converging the lists
means the static native walk now also skips `.github`/`example`/`examples` and
metro's also skips `DerivedData`.
Also from the same review pass:
- `toPosix` is exported from util.js instead of the `split(/[\\/]/u).join('/')`
idiom being spelled out in util.js, bundle-util.js and add.js.
- `add` serializes each target inside its write thunk rather than up front, so
peak memory is one target's bytes instead of every target's at once. The
merges still all happen before the first write -- that's what makes a
conflict abort leave the project untouched.
- validationError's three copies of the singular/plural skeleton collapse into
one `phrase()` helper; `validateFiles`'s `hash` flag is `withIntegrity`, the
thing it actually means.
- tests: the auto-excluded fixture set is one list the count derives from
instead of a hand-maintained `11`, and the repeated
`Object.keys(decode(...).modules.get('.').files).toSorted()` is one
`packedFiles()` helper (12 call sites).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrgpYdYarjgv8ur9kpBpia
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
Refactor the
stasis addcommand to defer all file writes until after validating the entire input set. This ensures that if validation fails, the project remains untouched rather than left in a partially-updated state. Additionally, implement auto-exclusion filtering for directory sweeps to prevent unintended capture of type declarations, secrets, and stasis's own outputs.Key Changes
Deferred writes: Rename
addToBundleFile→prepareBundleFileandupdateLockfile→prepareLockfileto return write thunks instead of executing immediately. All merges are computed first, then writes execute only after all validations pass.Four-step pipeline: Restructure
addCommandinto distinct phases:fs.globSync, tracking which paths were swept vs. explicitly namedAuto-exclusion rules: New
isAutoExcludedFile()function filters out:.d.ts,.d.mts,.d.cts).env,.env.*,*.env)stasis.lock.json,*.stasis.*.br)*.map,*.js.flow) unless opted into viaresourcesconfigImproved error reporting: Collect all validation failures (missing files, undeclared types, non-UTF-8 content) into a single error message rather than failing on the first offender. Directory sweeps that match only auto-excluded files report what was filtered.
Explicit override: Files named explicitly on the command line bypass the auto-exclusion filter, allowing
stasis add .envto work even though.envis auto-excluded fromstasis add .Repeatable operations:
stasis add .now never attests the bundle or lockfile it's writing, making the operation idempotent across runs.Notable Implementation Details
expandDirectories()now returns aMap<rel, { swept, stats }>to distinguish between explicitly named paths and those found by glob expansionvalidateFiles()collects errors across the entire set before throwing, enabling comprehensive error messagesSetto prevent self-reference during sweepshttps://claude.ai/code/session_01KrgpYdYarjgv8ur9kpBpia