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 installcorrect: run anywhere in a workspace, it re-resolves the whole graph, re-materialises everyvibedeps/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), andvibedeps::materialisedoes aremove_dir_allfollowed 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 installat 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 installis 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 buildandnpm installtreat 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 (onevibe.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.lockalready records, and the run proceeds straight to application (§2.3) against the locked versions. - This makes a
vibe installwhere 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 installalways re-resolves, so it silently bumps a package within its constraint on every run (a^0.3pin drifts to the newest0.3.xavailable). - With the freshness check,
vibe installbecomes lockfile-respecting: unchanged[requires]⇒ the locked versions are honoured verbatim. vibe updateremains the explicit "re-resolve and pick newer" command.- This aligns
vibe installwith thecargo build/npm installcontract — 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.lockfield: the lockfile is the baseline, and the check is acargo-style satisfiability test of the locked versions against the current[requires]— see §5.1.
- 15
vibe 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 --offlineused 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 installover 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 oldNothingToInstallrefusal madevibe init → vibe installfail 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 installthat 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_integritysetting,trust-presenceby 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), andSTATIC.mdonly when the node has static dependencies. The cost isO(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.mdproduces 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 apath- orgit-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
Stalefor it and the depsolver re-runs — re-reading the source and re-hashing it. This mirrors the existingpath/githandling: the check is "conservative by construction", so a mutable source yieldsStaleandvibe installfalls 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-treecontent_hash; a valid slot record carrying the samesource_hashearns 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. Underslot_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-placepackages retain their dedicated git-native update path.
- 29Scope — in-workspace
file://only, and not the giants. Confined tofile://sources under the workspace root (the self-hosting registry), which are non-reproducible by nature (a working directory, like apathdependency) 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'ssource_urlhas afile://prefix and its decoded path lies under the (canonicalised,\\?\-free) workspace root (the path test is component-wise and case-insensitive on Windows; agit+file://local git repo is content-addressed and does not match thefile://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 costin-placematerialisation exists to avoid, so anin-placepackage keeps the §2.2/§2.3 fast path and is refreshed only through its dedicated incremental path (vibe update <pkg>, thegit fetchof 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
- 31
vibe-workspace— the freshness check feedingapply_resolution; the strict.vibe-slot.toml, record-aware per-file reconciliation and slot verification; the install orchestration's resolution and materialisation skip paths. vibe-cli—vibe installwires 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-presencedefault, orverify) selects the §2.3 materialisation strategy;verifychecks the recorded source and owned-file identities before allowing a skip. Set once, it persists across runs. - No change to
vibe updateorvibe reinstallbeyond 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 installis unchanged for any input where[requires]actually changed, and for an unchanged input the output is whatvibe.lockalready pinned. - The one observable change is intentional and improving:
vibe installstops drifting versions within a constraint (§2.2). - Existing lockfiles are read as-is; a freshness-input digest, if added, is an optional
metafield 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).
- 34The freshness oracle —
cargo's model. No digest field is added tovibe.lock; the lockfile is the baseline. - The freshness check is a satisfiability test, the shape
cargouses: 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. - The declared root set must equal
meta.root_dependencies; every locked package must have itsvibedeps/slot materialised. - 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 avibedeps/slot, immutable once materialised). - Implementation scope: the check covers registry-resolved roots; a node carrying a git-/path-source dependency, a capability requirement, or an unresolved
version.varis conservatively reported stale (never wrongly fresh), so the fast path serves the common case — a workspace of registry packages. - The check reads only the resolved versions the lock already records — no schema bump, no new field; the per-package
content_hashkeeps gating fetched content at fetch time, unchanged. It is content-based, never mtime-based (§6). - Slot integrity on the fast path — a configurable strategy. The §2.3 materialise-skip is governed by a
slot_integritysetting in the vibevm user configuration, chosen once and persisted. - Two values:
trust-presence(the default — skip an immutable slot already present for the representation-current resolved version) andverify(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). - A missing legacy record takes the one-time migration path; a malformed record is a hard error.
vibe reinstall --forceandvibe updatebypass presence trust but still reconcile only the materialiser-owned footprint, preserving every unrecorded path. - Implementation note, historical and closed: the
content_hashspot-check was deferred at v1 becausecompute_content_hashlived only invibe-registry, whichvibe-workspacedoes 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-workspaceexposes theSlotVerifiertrait andvibe-install(which does depend onvibe-registry) suppliesRegistrySlotVerifier, 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. - A project-level override is a possible later extension; neither is v1 scope.
- Re-resolution holds the lock — minimum churn. When
[requires]has changed,vibe installre-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. - A held pin that conflicts with the change is detected as a depsolver error and falls back to a full, free re-resolve.
- 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, whichvibe updateaccepts as an operator-scoped action butvibe install's unified contract (one version per package, §6) cannot. - 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 --forceflag. Rejected —vibe updateandvibe reinstall --forceare already the bypasses (§2.5); a third spelling would be redundant.
7. Phase plan
- 37Skip resolution when fresh — the content-based freshness check;
vibe installskips the depsolver on an unchanged[requires], becoming lockfile-respecting. The largest win and the observable-contract change. - Materialise only the diff — identity-current slots skip; every required refresh reconciles the incoming shippable tree against the strict slot-record footprint, with
slot_integrityselectingtrust-presence(default) or recorded-file verification (§5.2). - 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). - Docs +
VIBEVM-SPEC.md— the §9.1 edit (install respects the lock) under owner sanction; adocs/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.lockis fresh — which also makesvibe installlockfile-respecting (§2.2), materialising only changedvibedeps/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 aslot_integrityvibevm user-config setting,trust-presenceby 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 andin-placepackages keep their git-native update path.6a7f750dclosed the silent-staleness regression and1cf4f189made its refresh non-destructive. - 2026-05-22 — shipped; record-aware successor completed in R1. The original freshness, slot-skip,
slot_integrityand pin-holding phases remain.6d606ef2added the strict slot record,1cf4f189replaced destructive refresh with the owned per-file diff,6a7f750dadded mutable-source hash freshness, and4503fdb6/9c545f0dmade the exact nonempty materialisation report drive hook reruns. FU3's scoped resolution remains correctness-relaxed and cannot servevibe 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).