PROP-022 — Materialization modes
01Status: IMPLEMENTED (specified 2026-06-24 in an owner-requested design
session; verified against the tree 2026-07-25 by the spec-actualization
campaign). The Materialization enum ships with Copy as the default (wire copy; named Snapshot/snapshot until the 2026-08-13 terminology ruling),
InPlace beside it and a doctested is_in_place (vibe-core package.rs); the
hardlink / in-place machinery runs through vibe-install (plan.rs /
fetched.rs / apply.rs, the materialise_in_place seam); submodule
snapshot-embedding lands in git_package_registry/fetch.rs; and the destructive
guard sits in commands/uninstall.rs. R1 successor 6d606ef2/1cf4f189
reconciles copy/hardlink refresh through the strict slot record without
touching unrecorded outputs. One of four orthogonal specs from the
bridge-packages design (siblings:
PROP-020 install hooks,
PROP-021 submodule sources,
PROP-023 bridge packages).
Materialization mode is a property of any package; a huge git package wants
in-place with no bridge in sight.
02Related: PROP-009 (the materialise step into
vibedeps/), PROP-007 (vibedeps/ layout),
PROP-010 (the live-git
cache + .git-stripped content tree the copy modes draw from),
PROP-019 §2.15 (the VVM
placer diff-copy/hardlink — direct prior art for hardlink),
PROP-019 §2.16 (VVM
"sources by reference" — prior art for in-place),
PROP-020 §2.1 (hook-edit reset rides on the
mode).
1. Motivation
1.1 The problem — one materialisation policy does not fit every package
- 03Today every package is materialised the same way: clone into the live-git
cache, strip
.gitinto a content tree, then full recursive copy of that tree into thevibedeps/<group>.<name>/<version>/slot (identity-keyed — owner ruling 2026-08-13; the slot carried<kind>-<name>before that ruling, which collided same-named packages of different groups and moved a package on a kind change). - This is right for ordinary
packages and gives the lockfile a stable
content_hashand a committable, offline-reproducible vendored slot.
04It fails at two different scales of "big":
- 05Big in bytes — a package with a few large binary assets pays a full byte copy per install/update of data that did not change.
- Big in file count — a package with millions of small files (think the
Chromium source tree) is killed not by bytes but by per-file syscalls:
copying — or even hardlinking — five million files takes hours, barely
faster than re-fetching over the network. The full-tree walk (
content_hashreads every file too) is itself the cost.
06The fix is to make materialisation a declared mode on the package, and to borrow the two cost-avoidance primitives VVM already proved (PROP-019 §2.15/§2.16).
2. Decisions
2.1 Three modes, declared in the descriptor
07req r1
08[package].materialization selects how the package lands on disk:
09[package]
materialization = "copy" # default | "hardlink" | "in-place"
- 10
copyis the default and the only mode an ordinary package needs. (It was namedsnapshotuntil the owner's 2026-08-13 terminology ruling reserved that word for the unfrozen version — PROP-044 §2b; the legacy spelling is refused with the rename recipe, never aliased.) - The mode is published in the descriptor so a consumer sees, before installing, how a package will be placed.
2.2 copy — the vendored full copy (default)
11req r1
- 12Initial placement is live-git cache →
.git-stripped shippable tree → full recursive copy into the slot. Refresh is record-aware: the incoming tree is reconciled against.vibe-slot.tomlrather than replacing the directory. - PROP-024 §2.2
extends the
.gitstrip to build output (.vibe/,target/,node_modules/,.vibeignoreglobs) for code-bearing packages, so such a package vendors its source, never its build artifacts. - The slot is a self-contained tree, identified by
content_hash(§2.5), vendored into the project's git (§2.7). - Submodule content is embedded into the copied tree (PROP-021 §2.3).
- For
copyandhardlinkslots, update/reinstall restores the materialiser-owned payload by diffing the incoming shippable tree against.vibe-slot.toml: changed owned files are replaced, stale owned files are removed, equal files stay untouched, and unrecorded build output is preserved. Hooks rerun exactly when that materialisation diff is nonempty, per PROP-020 §2.1.
2.3 hardlink — per-file hardlink, copy on change
13req r1
- 14For packages big in bytes but modest in file count. Initial placement may hardlink source/cache files into the slot, falling back to copy when linking is unavailable. Refresh uses the common
.vibe-slot.tomlpath→SHA-256 footprint: equal files are left in place and a changed destination is unlinked before its replacement is staged, so writing a slot can never mutate the source/cache inode through a hardlink. - A hardlink that fails (cross-volume / unsupported filesystem) falls back to copy.
- The slot still presents a full shippable tree and its source identity remains
content_hash. The slot record is reconciliation and integrity metadata, not a second source identity; unrecorded paths such astarget/remain outside the materialiser-owned footprint. - This mode does not help the file-count case — the per-file syscall remains — so it is not the giant-repo answer (§2.4 is).
2.4 in-place — git-native, project-local, no copy
15req r1
16For packages big in file count (and incidentally bytes), where even one full tree walk is unacceptable. vibevm never walks the tree:
- 17
git clone --recurse-submoduleslands directly in the slot, bypassing both the cache clone and the snapshot copy — one physical copy on the machine, not three (decisive when disk cannot hold several copies of a giant). - git manages it in place: update is
git fetch+ checkout (incremental, touches only changed objects/files); a hook's edits are reset withgit clean -dfxin the slot. - Project-local, never shared. Each project gets its own clone in its own
vibedeps/; there is deliberately no cross-project sharing, which removes the concurrent-mutation problem a shared global clone would create. - The slot path is not version-qualified. An
in-placeslot isvibedeps/<group>.<name>/(no/<version>/): one working clone whose version is the current git ref. Versioning the path would mean two on-disk copies of the giant — the opposite of the goal. - Requires a git source. Incremental update and
git cleanreset both need git; a non-git source has noin-placestory (§4).
2.5 Identity follows the mode
18req r1
- 19
copy/hardlink— source identity remainscontent_hash;.vibe-slot.tomlindependently records the owned destination footprint and exact per-file SHA-256 values used for reconciliation and verification. in-place—resolved_commit, notcontent_hash. The slot is a mutable git working tree (hooks edit it), so a content hash is neither stable nor affordable to compute; the git commit is the identity, known in O(1). The lockfile already recordsresolved_commit, so no new field is needed.
2.6 Destructive operations on an in-place slot need confirmation
20req r1
- 21An
in-placeslot may be a multi-hour download. Any destructive operation on it —uninstall,reinstall --force, a version switch that requires a re-clone, or slot removal — must be confirmed: interactively ay/n, and in a non-interactive run it requires an explicit flag (--force) or it aborts rather than silently deleting an expensive resource. - Hooks and their reset
(
git clean -dfx) are exempt — they are routine and trusted (the hook author is assumed competent, PROP-020). - The guard protects against accidental loss, not against the package's own lifecycle.
2.7 Vendoring differs by mode
22req r1
- 23
copy/hardlinkare vendored — the slot is committed into the project's git and is offline-reproducible from it (ahardlinkslot's bytes are materialised into git ongit addlike any file); acopyslot trivially so. in-placeis not vendored — the slot (a nested.gitplus possibly millions of files) is.gitignored in the project; restoration is a re-clone at the lockfile'sresolved_commit. The honest trade:in-placepackages need the network to restore, wheresnapshotpackages do not.
3. Rejected alternatives
- 24Content-hashing an
in-placepackage — rejected for the same reason VVM rejected hashing distributions (PROP-019 §9.2): hashing millions of files / gigabytes per operation is prohibitive, and the tree is mutable anyway. Identity is the commit (§2.5). - A shared global
in-placeclone (like the registry cache) — rejected: two projects mutating one giant working tree via hooks would collide; project-local clones make the problem disappear (§2.4). - Hardlink as the giant-repo answer — rejected: per-file syscalls still
cost hours at millions of files; only
in-place(no per-file work) solves the file-count axis (§2.4). - reflink / CoW placement — deferred (§4): not portable; hardlink is the portable byte-sharing primitive, matching VVM's choice.
4. Out of scope
- 25reflink / CoW placement where the filesystem supports it — far-backlog, as in PROP-019 §6.
in-placefor a non-git source — needs git for incremental update andgit cleanreset; a binary/path source has noin-placemode.- Automatic cache garbage collection for the live-git cache — owned by PROP-010.
5. Acceptance
- 26
[package].materializationparses tocopy(default) /hardlink/in-place; an unknown value is a manifest error, and the legacysnapshotis refused with the rename recipe. copyandhardlinkreconcile their recorded footprint without touching unrecorded paths;hardlinkmay share initial files with copy fallback and replaces changed destinations without mutating source inodes.in-placeclones once into an unversioned,.gitignored slot managed by git.in-placeidentity isresolved_commit; no full-tree hash is computed.- A destructive op on an
in-placeslot confirms interactively / requires--forcenon-interactively; hooks andgit cleanare exempt. - Full
self-check.shgreen; conform 0/0/0; specmap clean.