VibeVM
Contents
On this page
en
Publisher
org.vibevm.core
Version
1.0.0latest
Audiences
user, author
Reading time
14 min
Rendered
Read aloud
never

PROP-011: Incremental install — skip resolution when fresh, materialise only the diff

01Milestone: M1.21 (ROADMAP.md) — shipped 2026-05-22. Refines the install machinery of PROP-009 (M1.18); no dependency on PROP-008 or PROP-010.

02Status: SHIPPED 2026-05-22; record-aware R1 successor accepted 2026-09-08. The original four phases remain, while the strict slot record, owned per-file reconciliation, mutable-source hash gate and exact nonempty-diff hook schedule now define materialisation freshness and repair (see §8).

03Related: PROP-009 (the loading model — apply_resolution, regenerate_boot, vibedeps::materialise, the vibe install orchestration this PROP refines; §2.10 vibe reinstall); PROP-007 (workspaces — unified resolution, the matryoshka); PROP-010 (the local cache — skip-when-fresh makes the common path offline-clean for free, §2.6 there).

04Owner sanction: this PROP changes vibe install's observable contract (it becomes lockfile-respecting — §2.2) and so edits VIBEVM-SPEC.md §9.1. The spec edit requires explicit owner sanction — granted 2026-05-22; it lands in Phase 4 (§7).

1. Motivation

  • 05PROP-009 made vibe install correct: run anywhere in a workspace, it re-resolves the whole graph, re-materialises every vibedeps/ slot, and regenerates every node's boot artifacts. Correctness-first — "regenerate everything deterministically" is obviously right and self-healing.
  • It is also whole-tree, unconditionally. Every vibe install — regardless of what changed, or whether anything dependency-relevant changed at all — re-runs the depsolver (a registry walk, network), and vibedeps::materialise does a remove_dir_all followed by a full recursive copy of every package tree.
  • For a large workspace that is a heavy operation paid on every invocation.
  • 06A developer — or, increasingly, an agent — iterating fast inside a large project is blocked by this.
  • Most edits do not need vibe install at all: PROP-009's boot artifacts are path manifests, not content copies, so editing spec content never changes them — authoring is already decoupled from installing.
  • But when vibe install is needed (a dependency declaration changed), it must be cheap, and today it is not: it pays whole-tree cost for a one-subtree change.
  • 07The fix is standard package-manager practice.
  • cargo build and npm install treat the lockfile as a freshness oracle: work the lockfile proves unchanged is skipped; only the diff is touched.
  • PROP-011 brings that discipline to vibe install.

2. Decisions

2.1 Separate resolution from application

08Decision. vibe install is understood as two phases, optimised independently — the current code conflates them.

  • 09Resolution — the depsolver: read every node's [requires], pick one version per package. It must stay unified (one vibe.lock, one version per package across the workspace — the diamond problem; PROP-007 §2.4). It cannot be computed per-subtree. But it can be skipped entirely when its inputs are unchanged (§2.2).
  • Application — materialise the resolution into vibedeps/, then regenerate boot artifacts. This does not have to replace whole slots. It is an ownership diff: skip identity-current slots and reconcile only changed materialiser-owned files in slots that require refresh (§2.3); boot regeneration is cheap and stays whole-tree (§2.4).

10Resolution being unified does not force application to be whole-tree. PROP-011 keeps unified resolution and makes everything around it incremental.

2.2 Skip resolution when the lockfile is fresh

11Decision. Before running the depsolver, vibe install performs a freshness check: it compares the resolution inputs — the union of every workspace node's [requires] (registry, git, path, and resolved var packages) — against what the current vibe.lock was generated from.

  • 12If they are unchanged, the depsolver is not run: the resolution is exactly what vibe.lock already records, and the run proceeds straight to application (§2.3) against the locked versions.
  • This makes a vibe install where no dependency declaration changed cost only: discover the workspace, run the freshness check, apply. No network, no version re-selection — milliseconds even on a large workspace.
  • 13It also fixes an observable wart. Today vibe install always re-resolves, so it silently bumps a package within its constraint on every run (a ^0.3 pin drifts to the newest 0.3.x available).
  • With the freshness check, vibe install becomes lockfile-respecting: unchanged [requires] ⇒ the locked versions are honoured verbatim.
  • vibe update remains the explicit "re-resolve and pick newer" command.
  • This aligns vibe install with the cargo build / npm install contract — install respects the lock; update moves it — and makes a build reproducible.
  • 14When [requires] has changed, resolution runs, but holds the lock for every dependency the change did not touch (§5.3): each registry-resolved root the lock still satisfies is pinned to its locked version, so the re-resolve never drifts an untouched dependency — only the changed one and its subtree move.
  • A held pin that conflicts with the change is detected as a depsolver error and falls back to a full, free re-resolve.
  • The freshness check itself adds no vibe.lock field: the lockfile is the baseline, and the check is a cargo-style satisfiability test of the locked versions against the current [requires] — see §5.1.
  • 15vibe install <pkgref> builds the FULL resolution (bug ruling, 2026-08-24). An explicit-pkgref install solves over the union of every node's [requires] PLUS the named packages — the named ones re-resolved fresh, every untouched root held to its locked pin (§5.3) — never over the named packages alone. The apply phase treats any resolution as the whole world (its prune drops every slot outside it — correct for a full resolution, catastrophic for a partial one): vibe install X --offline used to erase every other slot and rewrite the lock to X's closure. A partial solve MUST never reach apply.
  • Zero dependencies is a normal state (bug ruling, 2026-08-24). A bare vibe install over a project whose [requires] union is empty is NOT an error — it is a fresh project. The run regenerates the boot artifacts of the empty world (the authored snippets alone) and exits 0; the old NothingToInstall refusal made vibe init → vibe install fail out of the box.

2.3 Materialise only the diff

16Decision, revised by PROP-054 §9. An identity-current dependency slot is skipped. When a slot must be refreshed, its .vibe-slot.toml is the ownership boundary: materialisation reconciles the old recorded footprint with the incoming shippable tree instead of replacing the whole directory.

  • 17Versions are immutable (PROP-002), so presence of the exact resolved version under its current representation may earn the immutable fast path. When refresh is required, whole-directory replacement is still unjustified: the record identifies exactly what the materialiser owns.
  • A recorded refresh writes only new or changed owned files, removes only previously recorded files absent from the new footprint, and never removes an on-disk path outside the old record. Equal per-file hashes leave bytes, inode and mtime untouched. A legacy slot without a record pays one final full replacement and receives a record.
  • For a vibe install that changed one subtree, this turns a re-copy of the dependency corpus into a per-file reconciliation of only the slots that actually moved.
  • 18The skip trusts slot-presence-for-a-version as a proxy for correctness — by default it does not re-hash the slot.
  • That is deliberate: hashing every immutable slot on every install would defeat the optimisation. The integrity escape hatch remains vibe reinstall --force (§2.5), which re-fetches and reconciles the materialiser-owned footprint without deleting unrecorded build output.
  • Presence remains the default fast-path proof for an immutable, representation-current resolved version. When that proof does not hold, a valid slot record turns refresh into an owned footprint diff; a missing legacy record triggers one final full migration, and a malformed record is a hard error rather than authority to wipe unknown paths.
  • Whether the fast path additionally verifies the slot record's source identity and owned-file hashes before trusting it is a configurable strategy — the slot_integrity setting, trust-presence by default (§5.2).

2.4 Boot regeneration stays whole-tree — and why that is fine

19Decision. Boot-artifact regeneration (regenerate_boot over every node) is kept whole-tree. It is not the expensive part, and scoping it is low-value:

  • 20It is cheap: per node, an in-memory topological sort plus writing INDEX.md (a small TOML), the redirects (~1 KB each), and STATIC.md only when the node has static dependencies. The cost is O(nodes) small operations, not the corpus-sized I/O of materialisation.
  • It does not churn git: git tracks content, so regenerating a byte-identical INDEX.md produces no diff.
  • Whole-tree regeneration is self-healing: a boot artifact left stale by an earlier bug is silently corrected on the next install. A scoped regeneration would preserve such staleness.
  • 21Scoping boot regeneration to the affected set — a changed node plus its ancestors, the shape PROP-009 §2.10 already specifies for vibe reinstall — is possible but out of scope by owner decision: it is the cheap phase, and effort belongs on §2.2 and §2.3.
  • It is recorded here only so the option is not lost: should a workspace ever grow large enough that O(nodes) small writes genuinely matter, §2.10's node-plus-ancestors shape is the ready answer. It is not a deliverable of this PROP.

2.5 Bypasses — no new flag

22Decision. PROP-011 adds no force flag to vibe install. The two skips (§2.2, §2.3) each already have an explicit, named bypass:

  • 23to re-resolve even though [requires] is unchanged — vibe update (re-resolves and may pick newer versions; PROP-009 §2.7 / FU3);
  • to re-materialise even though the slots are present — vibe reinstall --force (re-fetches from source and reconciles each materialiser-owned footprint; PROP-009 §2.10).
  • 24The skips are safe precisely because these bypasses exist.
  • Keeping them as the bypass — rather than adding vibe install --force — avoids a redundant flag and keeps each command's job distinct.

2.6 In-workspace file:// sources are mutable

25Decision. §2.2 and §2.3 both rest on version immutability — a locked version is correct content, so its resolution can be skipped (§2.2) and its present slot trusted (§2.3).

  • 26That premise holds for a published registry version (PROP-002) and for a content-addressed git ref.
  • It is false for a package resolved from a file:// source *inside the workspace — the in-repo self-hosting registry (packages/, --registry packages) the author edits in place while authoring* a package: the source is a working tree whose content changes with no version or [requires] edit, exactly like a path- or git-source dependency (which §2.2 already excludes from the fast path).
  • An external local registry or mirror — a file:// path outside the workspace — is a static dependency source, not an edited working tree, so it stays immutable and keeps the fast path.

27So an in-workspace file://-sourced registry dependency is treated as mutable:

  • 28Freshness (§2.2): it can never be proven fresh cheaply, so the freshness check reports Stale for it and the depsolver re-runs — re-reading the source and re-hashing it. This mirrors the existing path/git handling: the check is "conservative by construction", so a mutable source yields Stale and vibe install falls back to a full resolution (always correct, and for a local source merely a no-network walk).
  • Materialisation (§2.3), revised by PROP-054 §9.3. An in-workspace file:// slot is never trusted by version-presence alone. Resolution re-fetches the mutable source and supplies its current shippable-tree content_hash; a valid slot record carrying the same source_hash earns the materialisation skip. A missing legacy record takes the one-time migration path, a mismatched record flows to record-aware reconciliation, and a malformed record refuses rather than authorising a wipe. Under slot_integrity = "verify", equality of source identity does not hide payload drift: recorded files are still verified and any divergence is repaired through reconciliation. External immutable registries retain the ordinary presence fast path; in-place packages retain their dedicated git-native update path.
  • 29Scope — in-workspace file:// only, and not the giants. Confined to file:// sources under the workspace root (the self-hosting registry), which are non-reproducible by nature (a working directory, like a path dependency) and are actively edited — so honouring the edit concedes no reproducibility the source did not already concede.
  • The discriminator is is_in_workspace_file_source: the lockfile's source_url has a file:// prefix and its decoded path lies under the (canonicalised, \\?\-free) workspace root (the path test is component-wise and case-insensitive on Windows; a git+file:// local git repo is content-addressed and does not match the file:// prefix).
  • It explicitly excludes in-place (PROP-022) packages (materialization.is_in_place()): re-hashing or re-copying a giant working tree on every install is precisely the cost in-place materialisation exists to avoid, so an in-place package keeps the §2.2/§2.3 fast path and is refreshed only through its dedicated incremental path (vibe update <pkg>, the git fetch of PROP-022).
  • The choice is source-and-path-based, not mtime-based (§6 rejects mtime — file mtimes do not survive git clone).

30No new flag (consistent with §2.5). This is automatic and source-aware — the author edits the in-repo source and runs vibe install; nothing to remember, and neither vibe update nor reinstall --force is needed for the local-authoring loop, while those bypasses remain for the immutable case.

3. Command and crate surface

  • 31vibe-workspace — the freshness check feeding apply_resolution; the strict .vibe-slot.toml, record-aware per-file reconciliation and slot verification; the install orchestration's resolution and materialisation skip paths.
  • vibe-clivibe install wires the freshness check ahead of the depsolver; its report distinguishes "unchanged — nothing re-resolved" from a real apply.
  • vibe.lock — unchanged; the lockfile is the freshness baseline (§5.1), so no schema bump and no new field.
  • vibevm user configuration — [install] slot_integrity (trust-presence default, or verify) selects the §2.3 materialisation strategy; verify checks the recorded source and owned-file identities before allowing a skip. Set once, it persists across runs.
  • No change to vibe update or vibe reinstall beyond their role as the §2.5 bypasses.

4. Migration

  • 32None. PROP-011 is purely an optimisation of an existing, correct operation — the output of vibe install is unchanged for any input where [requires] actually changed, and for an unchanged input the output is what vibe.lock already pinned.
  • The one observable change is intentional and improving: vibe install stops drifting versions within a constraint (§2.2).
  • Existing lockfiles are read as-is; a freshness-input digest, if added, is an optional meta field absent lockfiles simply force one resolution.

5. Resolved questions

33The three questions opened in draft 2 were resolved in an owner design session on 2026-05-22 (draft 3).

  1. 34The freshness oracle — cargo's model. No digest field is added to vibe.lock; the lockfile is the baseline.
  2. The freshness check is a satisfiability test, the shape cargo uses: re-read every node's [requires], and the lock is fresh iff every declared dependency has a [[package]] entry whose pinned version satisfies the current constraint.
  3. The declared root set must equal meta.root_dependencies; every locked package must have its vibedeps/ slot materialised.
  4. Transitive packages are trusted — they were resolved from roots, and an unchanged root set cannot have produced a different transitive closure (a transitive [requires] lives inside a vibedeps/ slot, immutable once materialised).
  5. Implementation scope: the check covers registry-resolved roots; a node carrying a git-/path-source dependency, a capability requirement, or an unresolved version.var is conservatively reported stale (never wrongly fresh), so the fast path serves the common case — a workspace of registry packages.
  6. The check reads only the resolved versions the lock already records — no schema bump, no new field; the per-package content_hash keeps gating fetched content at fetch time, unchanged. It is content-based, never mtime-based (§6).
  7. Slot integrity on the fast path — a configurable strategy. The §2.3 materialise-skip is governed by a slot_integrity setting in the vibevm user configuration, chosen once and persisted.
  8. Two values: trust-presence (the default — skip an immutable slot already present for the representation-current resolved version) and verify (a recorded slot is accepted only after source identity and every owned file verify; divergence is repaired through record-aware reconciliation with a warning naming the package).
  9. A missing legacy record takes the one-time migration path; a malformed record is a hard error. vibe reinstall --force and vibe update bypass presence trust but still reconcile only the materialiser-owned footprint, preserving every unrecorded path.
  10. Implementation note, historical and closed: the content_hash spot-check was deferred at v1 because compute_content_hash lived only in vibe-registry, which vibe-workspace does not depend on. Built 2026-08-21-eve (the P011V landing, commit 75c2d538): the dependency knot was cut by a seam, not a move — vibe-workspace exposes the SlotVerifier trait and vibe-install (which does depend on vibe-registry) supplies RegistrySlotVerifier, dispatching by the pin's own recipe label; the sentinel test proves a verified slot is not copied. This note stands as the record of why the seam exists.
  11. A project-level override is a possible later extension; neither is v1 scope.
  12. Re-resolution holds the lock — minimum churn. When [requires] has changed, vibe install re-resolves, but pins every registry-resolved root the lock still satisfies to its exact locked version, so only the changed dependency and its subtree move — an untouched dependency never drifts.
  13. A held pin that conflicts with the change is detected as a depsolver error and falls back to a full, free re-resolve.
  14. Implementation finding: the design assumed reuse of FU3's vibe update <pkgref> scoped resolution, but FU3 is correctness-relaxed — it splices a re-resolved subtree into the held lock without unifying them, which vibe update accepts as an operator-scoped action but vibe install's unified contract (one version per package, §6) cannot.
  15. Pinning via constraint-tightening holds the lock correctly with the current NaiveDepSolver; it does not skip the registry walk for an unchanged subtree. Walk-skipping needs the depsolver's pin-preference machinery (PROP-003 §2.1) and is deferred with the SAT solver.

35Closed in draft 2: scoped boot regeneration — boot regeneration stays whole-tree (§2.4), the cheap phase is not optimised.

6. Rejected / deferred alternatives

  • 36Subset the resolution per member. Rejected — resolution must be unified (one version per package; the diamond problem, PROP-007 §2.4). PROP-011 skips resolution when it is provably unneeded; it never resolves a subtree in isolation.
  • mtime-based freshness. Rejected — file mtimes are not preserved across git clone / git checkout, so an mtime oracle would mis-fire constantly. The freshness check is content-based (§5.1).
  • Scope or otherwise optimise boot regeneration. Out of scope by owner decision (§2.4) — boot regeneration is cheap, self-healing, and produces no git churn; effort goes to §2.2 and §2.3, the phases that are genuinely expensive.
  • A vibe install --force flag. Rejected — vibe update and vibe reinstall --force are already the bypasses (§2.5); a third spelling would be redundant.

7. Phase plan

  1. 37Skip resolution when fresh — the content-based freshness check; vibe install skips the depsolver on an unchanged [requires], becoming lockfile-respecting. The largest win and the observable-contract change.
  2. Materialise only the diff — identity-current slots skip; every required refresh reconciles the incoming shippable tree against the strict slot-record footprint, with slot_integrity selecting trust-presence (default) or recorded-file verification (§5.2).
  3. Minimum-churn re-resolution — a changed [requires] re-resolves, but holds the locked version of every untouched registry root (§5.3); a held-pin conflict falls back to a full re-resolve. Skipping the registry walk for unchanged subtrees is deferred to the SAT solver (PROP-003).
  4. Docs + VIBEVM-SPEC.md — the §9.1 edit (install respects the lock) under owner sanction; a docs/ note.

38Boot-regeneration scoping (§2.4) is out of scope by owner decision — not a phase.

8. Version history

  • 392026-05-21 — draft 1. Requirements captured in an owner discussion on incremental install: the resolution / application split (§2.1), skipping the depsolver when vibe.lock is fresh — which also makes vibe install lockfile-respecting (§2.2), materialising only changed vibedeps/ slots (§2.3), and the deliberate decision to leave boot regeneration whole-tree because it is the cheap phase (§2.4).
  • 2026-05-21 — draft 2. Owner review: the §2.4 decision — boot regeneration stays whole-tree, the cheap phase is not optimised — confirmed and made firm; the corresponding draft-1 open question is closed. The PROP stands on its two substantive wins, §2.2 (skip resolution when fresh) and §2.3 (materialise only the diff). Three §5 open questions — the freshness oracle, slot integrity, incremental re-resolution — remain for a follow-up owner design session. Not yet implementation-ready.
  • 2026-05-22 — draft 3. The three §5 open questions resolved in an owner design session. The freshness oracle is cargo's satisfiability model — the lockfile is the baseline, no new field (§5.1). Slot integrity on the fast path is a slot_integrity vibevm user-config setting, trust-presence by default (§5.2). A changed [requires] re-resolves incrementally, full re-resolve as fallback (§5.3). Implementation-ready.
  • 2026-06-27, completed by the R1 record successor. In-workspace file:// sources are mutable (§2.6), so resolution re-reads and re-hashes them rather than trusting version-presence. The current source hash may earn a record-backed materialisation skip; otherwise the owned footprint is reconciled. External local registries keep the immutable fast path and in-place packages keep their git-native update path. 6a7f750d closed the silent-staleness regression and 1cf4f189 made its refresh non-destructive.
  • 2026-05-22 — shipped; record-aware successor completed in R1. The original freshness, slot-skip, slot_integrity and pin-holding phases remain. 6d606ef2 added the strict slot record, 1cf4f189 replaced destructive refresh with the owned per-file diff, 6a7f750d added mutable-source hash freshness, and 4503fdb6/9c545f0d made the exact nonempty materialisation report drive hook reruns. FU3's scoped resolution remains correctness-relaxed and cannot serve vibe install's unified contract, so Phase 3 holds pins via constraint-tightening and registry-walk skipping remains deferred to PROP-003's SAT solver (§5.3).

For an agent

This page has a machine mirror. The citation carries the version rather than latest, so what an agent quotes does not move under it.

spec://org.vibevm.core/vibevm@1.0.0/modules/vibe-workspace/PROP-011-incremental-install

.md.xmlllms.txt