<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">PROP-011: Incremental install — skip resolution when fresh, materialise only the diff</title>
  <status stage="impl" state="done" comment="shipped 2026-05-22; record-aware R1 successor accepted through 6d606ef2, 1cf4f189, 6a7f750d, 4503fdb6 and 9c545f0d on 2026-09-08"/>
  <p p="1"><fact id="milestone-line" status="impl/done">**Milestone:** M1.21 ([`ROADMAP.md`](../../../ROADMAP.md)) — **shipped 2026-05-22.** Refines the install machinery of [PROP-009](PROP-009-loading-model.xml) (M1.18); no dependency on PROP-008 or PROP-010.</fact></p>
  <p p="2"><fact id="status-line" status="impl/done">**Status:** 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).</fact></p>
  <p p="3"><fact id="related" status="spec/done">**Related:** [PROP-009](PROP-009-loading-model.xml) (the loading model — `apply_resolution`, `regenerate_boot`, `vibedeps::materialise`, the `vibe install` orchestration this PROP refines; §2.10 `vibe reinstall`); [PROP-007](PROP-007-workspace.xml) (workspaces — unified resolution, the matryoshka); [PROP-010](../vibe-registry/PROP-010-local-package-cache.xml) (the local cache — skip-when-fresh makes the common path offline-clean for free, §2.6 there).</fact></p>
  <p p="4"><fact id="OWNER-SANCTION" status="impl/done">**Owner 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).</fact></p>
  <section id="motivation" title="1. Motivation">
    <list ordered="false" p="5">
      <item><fact id="correctness-first" status="impl/done">PROP-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.</fact></item>
      <item><fact id="whole-tree-unconditional" status="impl/done">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.</fact></item>
      <item><fact id="heavy-cost" status="impl/done">For a large workspace that is a heavy operation paid on every invocation.</fact></item>
    </list>
    <list ordered="false" p="6">
      <item><fact id="iteration-blocked" status="impl/done">A developer — or, increasingly, an agent — iterating fast inside a large project is blocked by this.</fact></item>
      <item><fact id="authoring-decoupled" status="impl/done">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.</fact></item>
      <item><fact id="needed-must-be-cheap" status="impl/done">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.</fact></item>
    </list>
    <list ordered="false" p="7">
      <item><fact id="fix-standard" status="impl/done">The fix is standard package-manager practice.</fact></item>
      <item><fact id="lockfile-oracle" status="impl/done">`cargo build` and `npm install` treat the lockfile as a freshness oracle: work the lockfile proves unchanged is skipped; only the diff is touched.</fact></item>
      <item><fact id="brings-discipline" status="impl/done">PROP-011 brings that discipline to `vibe install`.</fact></item>
    </list>
  </section>
  <section id="decisions" title="2. Decisions">
    <section id="two-phases" title="2.1 Separate resolution from application">
      <p p="8"><fact id="TWO-PHASES-SPLIT" status="impl/done">**Decision.** `vibe install` is understood as two phases, optimised independently — the current code conflates them.</fact></p>
      <list ordered="false" p="9">
        <item><fact id="PHASE-RESOLUTION" status="impl/done">**Resolution** — 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).</fact></item>
        <item><fact id="PHASE-APPLICATION" status="impl/done">**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).</fact></item>
      </list>
      <p p="10"><fact id="UNIFIED-NOT-WHOLE-TREE" status="impl/done">Resolution being unified does not force application to be whole-tree. PROP-011 keeps unified resolution and makes everything around it incremental.</fact></p>
    </section>
    <section id="skip-resolution" title="2.2 Skip resolution when the lockfile is fresh">
      <p p="11"><fact id="FRESHNESS-CHECK" status="impl/done">**Decision.** 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.</fact></p>
      <list ordered="false" p="12">
        <item><fact id="SKIP-WHEN-FRESH" status="impl/done">If 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.</fact></item>
        <item><fact id="FAST-PATH-COST" status="impl/done">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.</fact></item>
      </list>
      <list ordered="false" p="13">
        <item><fact id="drift-wart" status="impl/done">It 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).</fact></item>
        <item><fact id="LOCKFILE-RESPECTING" status="impl/done" action="continue" actionstage="doc" audience="user">With the freshness check, **`vibe install` becomes lockfile-respecting**: unchanged `[requires]` ⇒ the locked versions are honoured verbatim.</fact></item>
        <item><fact id="UPDATE-MOVES-LOCK" status="impl/done" action="continue" actionstage="doc" audience="user">`vibe update` remains the explicit "re-resolve and pick newer" command.</fact></item>
        <item><fact id="CARGO-CONTRACT-ALIGN" status="impl/done">This aligns `vibe install` with the `cargo build` / `npm install` contract — *install respects the lock; update moves it* — and makes a build reproducible.</fact></item>
      </list>
      <list ordered="false" p="14">
        <item><fact id="HOLD-THE-LOCK" status="impl/done">When `[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.</fact></item>
        <item><fact id="HELD-PIN-CONFLICT" status="impl/done">A held pin that conflicts with the change is detected as a depsolver error and falls back to a full, free re-resolve.</fact></item>
        <item><fact id="NO-NEW-FIELD" status="impl/done">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.</fact></item>
      </list>
      <list ordered="false" p="15">
        <item><fact id="EXPLICIT-PKGREF-FULL-SOLVE" status="impl/plan">**`vibe install &lt;pkgref&gt;` 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.</fact></item>
        <item><fact id="EMPTY-REQUIRES-IS-A-NO-OP" status="impl/plan">**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.</fact></item>
      </list>
    </section>
    <section id="materialise-diff" title="2.3 Materialise only the diff">
      <p p="16"><fact id="SLOT-SKIP" status="impl/done">**Decision, 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.</fact></p>
      <list ordered="false" p="17">
        <item><fact id="IMMUTABLE-PREMISE" status="impl/done">Versions 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.</fact></item>
        <item><fact id="DIFF-ONLY" status="impl/done">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.</fact></item>
        <item><fact id="one-subtree-win" status="impl/done">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.</fact></item>
      </list>
      <list ordered="false" p="18">
        <item><fact id="TRUST-PRESENCE" status="impl/done">The skip trusts slot-presence-for-a-version as a proxy for correctness — by default it does not re-hash the slot.</fact></item>
        <item><fact id="trust-presence-why" status="impl/done">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.</fact></item>
        <item><fact id="RECORDED-REFRESH" status="impl/done">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.</fact></item>
        <item><fact id="SLOT-INTEGRITY-SETTING" status="impl/done">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).</fact></item>
      </list>
    </section>
    <section id="boot-regen" title="2.4 Boot regeneration stays whole-tree — and why that is fine">
      <p p="19"><fact id="BOOT-WHOLE-TREE" status="impl/done">**Decision.** Boot-artifact regeneration (`regenerate_boot` over every node) is **kept whole-tree**. It is not the expensive part, and scoping it is low-value:</fact></p>
      <list ordered="false" p="20">
        <item><fact id="boot-cheap" status="impl/done">It 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.</fact></item>
        <item><fact id="boot-git-quiet" status="impl/done">It does not churn git: git tracks content, so regenerating a byte-identical `INDEX.md` produces no diff.</fact></item>
        <item><fact id="boot-self-healing" status="impl/done">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.</fact></item>
      </list>
      <list ordered="false" p="21">
        <item><fact id="SCOPING-OUT-OF-SCOPE" status="impl/done">Scoping 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.</fact></item>
        <item><fact id="scoping-option-recorded" status="spec/done">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.</fact></item>
      </list>
    </section>
    <section id="force" title="2.5 Bypasses — no new flag">
      <p p="22"><fact id="NO-NEW-FLAG" status="impl/done">**Decision.** PROP-011 adds **no force flag to `vibe install`**. The two skips (§2.2, §2.3) each already have an explicit, named bypass:</fact></p>
      <list ordered="false" p="23">
        <item><fact id="BYPASS-UPDATE" status="impl/done">to re-resolve even though `[requires]` is unchanged — `vibe update` (re-resolves and may pick newer versions; PROP-009 §2.7 / FU3);</fact></item>
        <item><fact id="BYPASS-REINSTALL" status="impl/done" action="continue" actionstage="doc" audience="user">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).</fact></item>
      </list>
      <list ordered="false" p="24">
        <item><fact id="BYPASSES-MAKE-SAFE" status="impl/done">The skips are safe precisely *because* these bypasses exist.</fact></item>
        <item><fact id="no-redundant-flag" status="impl/done">Keeping them as the bypass — rather than adding `vibe install --force` — avoids a redundant flag and keeps each command's job distinct.</fact></item>
      </list>
    </section>
    <section id="local-mutable-source" title="2.6 In-workspace `file://` sources are mutable">
      <p p="25"><fact id="IMMUTABILITY-PREMISE" status="impl/done">**Decision.** §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).</fact></p>
      <list ordered="false" p="26">
        <item><fact id="premise-holds" status="impl/done">That premise holds for a published registry version (PROP-002) and for a content-addressed git ref.</fact></item>
        <item><fact id="IN-WORKSPACE-MUTABLE" status="impl/done">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).</fact></item>
        <item><fact id="EXTERNAL-FILE-IMMUTABLE" status="impl/done">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.</fact></item>
      </list>
      <p p="27"><fact id="mutable-treatment-lead" status="impl/done">So an *in-workspace* `file://`-sourced registry dependency is treated as **mutable**:</fact></p>
      <list ordered="false" p="28">
        <item><fact id="MUTABLE-FRESHNESS" status="impl/done">**Freshness (§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).</fact></item>
        <item><fact id="MUTABLE-MATERIALISATION" status="impl/done">**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.</fact></item>
      </list>
      <list ordered="false" p="29">
        <item><fact id="SCOPE-IN-WORKSPACE-ONLY" status="impl/done">**Scope — 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.</fact></item>
        <item><fact id="DISCRIMINATOR" status="impl/done">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).</fact></item>
        <item><fact id="IN-PLACE-EXCLUDED" status="impl/done">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 &lt;pkg&gt;`, the `git fetch` of PROP-022).</fact></item>
        <item><fact id="NOT-MTIME" status="impl/done">The choice is source-and-path-based, **not** mtime-based (§6 rejects mtime — file mtimes do not survive `git clone`).</fact></item>
      </list>
      <p p="30"><fact id="AUTOMATIC-NO-FLAG" status="impl/done" action="continue" actionstage="doc" audience="author">**No 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.</fact></p>
    </section>
  </section>
  <section id="surface" title="3. Command and crate surface">
    <list ordered="false" p="31">
      <item><fact id="SURF-WORKSPACE" status="impl/done">`vibe-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.</fact></item>
      <item><fact id="SURF-CLI" status="impl/done">`vibe-cli` — `vibe install` wires the freshness check ahead of the depsolver; its report distinguishes "unchanged — nothing re-resolved" from a real apply.</fact></item>
      <item><fact id="SURF-LOCK-UNCHANGED" status="impl/done">`vibe.lock` — unchanged; the lockfile *is* the freshness baseline (§5.1), so no schema bump and no new field.</fact></item>
      <item><fact id="SURF-CONFIG" status="impl/done">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.</fact></item>
      <item><fact id="SURF-NO-OTHER-CHANGES" status="impl/done">No change to `vibe update` or `vibe reinstall` beyond their role as the §2.5 bypasses.</fact></item>
    </list>
  </section>
  <section id="migration" title="4. Migration">
    <list ordered="false" p="32">
      <item><fact id="NO-MIGRATION" status="impl/done">None. 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.</fact></item>
      <item><fact id="INTENTIONAL-CHANGE" status="impl/done">The one observable change is intentional and improving: `vibe install` stops drifting versions within a constraint (§2.2).</fact></item>
      <item><fact id="LOCKFILES-AS-IS" status="impl/done">Existing lockfiles are read as-is; a freshness-input digest, if added, is an optional `meta` field absent lockfiles simply force one resolution.</fact></item>
    </list>
  </section>
  <section id="open" title="5. Resolved questions">
    <p p="33"><fact id="resolved-lead" status="impl/done">The three questions opened in draft 2 were resolved in an owner design session on 2026-05-22 (draft 3).</fact></p>
    <list ordered="true" p="34">
      <item><fact id="RES-FRESHNESS-ORACLE" status="impl/done">**The freshness oracle — `cargo`'s model.** No digest field is added to `vibe.lock`; the lockfile *is* the baseline.</fact></item>
      <item><fact id="ORACLE-SAT-TEST" status="impl/done">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.</fact></item>
      <item><fact id="ORACLE-ROOT-SET" status="impl/done">The declared root set must equal `meta.root_dependencies`; every locked package must have its `vibedeps/` slot materialised.</fact></item>
      <item><fact id="ORACLE-TRANSITIVE-TRUST" status="impl/done">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).</fact></item>
      <item><fact id="ORACLE-SCOPE" status="impl/done">*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.</fact></item>
      <item><fact id="ORACLE-CONTENT-BASED" status="impl/done">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).</fact></item>
      <item><fact id="RES-SLOT-INTEGRITY" status="impl/done">**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.</fact></item>
      <item><fact id="INTEGRITY-VALUES" status="impl/done">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).</fact></item>
      <item><fact id="INTEGRITY-VERIFY" status="impl/done">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.</fact></item>
      <item><fact id="INTEGRITY-SPOT-CHECK-DEFERRED" status="impl/done">*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.</fact></item>
      <item><fact id="INTEGRITY-LATER-EXTENSION" status="spec/done">A project-level override is a possible later extension; neither is v1 scope.</fact></item>
      <item><fact id="RES-HOLD-LOCK" status="impl/done">**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.</fact></item>
      <item><fact id="HOLD-CONFLICT-FALLBACK" status="impl/done">A held pin that conflicts with the change is detected as a depsolver error and falls back to a full, free re-resolve.</fact></item>
      <item><fact id="FU3-FINDING" status="impl/done">*Implementation finding:* the design assumed reuse of FU3's `vibe update &lt;pkgref&gt;` 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.</fact></item>
      <item><fact id="WALK-SKIP-DEFERRED" status="spec/done">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.</fact></item>
    </list>
    <p p="35"><fact id="draft2-closed" status="spec/done">Closed in draft 2: scoped boot regeneration — boot regeneration stays whole-tree (§2.4), the cheap phase is not optimised.</fact></p>
  </section>
  <section id="rejected" title="6. Rejected / deferred alternatives">
    <list ordered="false" p="36">
      <item><fact id="REJ-SUBSET-RESOLUTION" status="spec/done">**Subset 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.</fact></item>
      <item><fact id="REJ-MTIME" status="spec/done">**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).</fact></item>
      <item><fact id="REJ-BOOT-SCOPE" status="spec/done">**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.</fact></item>
      <item><fact id="REJ-FORCE-FLAG" status="spec/done">**A `vibe install --force` flag.** Rejected — `vibe update` and `vibe reinstall --force` are already the bypasses (§2.5); a third spelling would be redundant.</fact></item>
    </list>
  </section>
  <section id="phases" title="7. Phase plan">
    <list ordered="true" p="37">
      <item><fact id="PHASE-1-FRESHNESS" status="impl/done">**Skip 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.</fact></item>
      <item><fact id="PHASE-2-DIFF" status="impl/done">**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).</fact></item>
      <item><fact id="PHASE-3-MIN-CHURN" status="impl/done">**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).</fact></item>
      <item><fact id="PHASE-4-DOCS" status="impl/done">**Docs + `VIBEVM-SPEC.md`** — the §9.1 edit (install respects the lock) under owner sanction; a `docs/` note.</fact></item>
    </list>
    <p p="38"><fact id="phases-note" status="spec/done">Boot-regeneration scoping (§2.4) is out of scope by owner decision — not a phase.</fact></p>
  </section>
  <section id="history" title="8. Version history">
    <list ordered="false" p="39">
      <item><fact id="HISTORY-DRAFT-1" status="spec/done">**2026-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).</fact></item>
      <item><fact id="HISTORY-DRAFT-2" status="spec/done">**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.</fact></item>
      <item><fact id="HISTORY-DRAFT-3" status="spec/done">**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.</fact></item>
      <item><fact id="HISTORY-REFINEMENT" status="spec/done">**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.</fact></item>
      <item><fact id="HISTORY-SHIPPED" status="spec/done">**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).</fact></item>
    </list>
  </section>
</spec>
