facet add, facet remove, and facet install all converge on the same commit phase — they differ only in the they produce.
Plan
Pure routing. Produces additions + removals. No network, no cache, no lockfile.
Commit
Resolves, verifies, materializes, and writes manifest + lockfile + receipt atomically.
Frozen
Lockfile is authoritative. No mutations to the locked set. Receipt-only writes.
Plan phase
Plan turns a user request into a delta — a list of additions and removals.Plan performs no network I/O, no lockfile lookups, no cache reads, and no version resolution. Its only job is to determine what is changing. All resolution is the commit phase’s responsibility.
| Delta field | Contents | Source command |
|---|---|---|
| Additions | User’s specifier verbatim (1.2.3, 0.*, *, latest, or bare name) | facet add |
| Removals | Bare facet names | facet remove |
| (empty) | No additions, no removals | facet install |
latest resolves to — that decision belongs to commit. The is established here: anything in additions is an explicit user request; anything commit later reads from the manifest but not in additions is reproduction.
Commit phase
Commit applies the delta to the project. Its inputs are the on-diskfacets.json, facets.lock, the , and the delta from plan.
Merge delta in memory
The on-disk manifest is read, additions are upserted, removals are deleted — all in memory. The on-disk
facets.json is never written ahead of the install.Resolve and materialize each facet
For each facet in the desired set, the commit phase resolves the version (if needed), checks the cache, verifies integrity, and materializes assets into every selected adapter. See the sections below for detail on each sub-step.
Drift removal
Facets the receipt records as materialized but the desired set no longer wants are removed offline.
Three registry interactions
Commit makes three distinct kinds of registry interaction, gated independently:Version resolution
Version resolution
Determines the exact version a specifier refers to (turning
0.*, *, latest, or a bare name into a concrete X.Y.Z).Needed only when no exact version is already known. An exact specifier needs none; a satisfying lockfile entry needs none.Archive resolution
Archive resolution
Downloads the archive bytes for an exact
name@version.Needed only on a cache miss. A warm cache skips it entirely, regardless of how the exact version was obtained.Integrity confirmation
Integrity confirmation
Verifies the cached or downloaded content against the registry’s published (
content_integrity).Needed only when a lockfile entry is being created or replaced. A satisfying lockfile entry serves as the trust anchor instead. An unreachable registry fails the operation rather than writing an unconfirmed entry.Registry versions are immutable — the bytes for a published
name@version never change. Integrity confirmation and lockfile comparison enforce this invariant client-side rather than assuming it.The structural discriminator
Whether the lockfile is trusted for version resolution depends on where an entry comes from, not on a flag:Additions (explicit request)
Additions (explicit request)
The lockfile is NOT trusted for version resolution. A non-exact specifier always triggers version resolution to the newest matching version, even when the lockfile already satisfies it.The user explicitly asked for this facet — we honor the request.
Manifest (reproduction)
Manifest (reproduction)
The lockfile IS trusted when satisfying. A satisfying recorded version needs no version resolution. Only an absent or stale entry triggers it.The facet was already declared — we reproduce the locked state.
Per-version materialization
Once a fully-qualified version is in hand, the cache and integrity path is identical regardless of how the version was obtained.A cache hit is never taken at face value. Three checks layer on the hit path, each catching what the previous cannot.
Cache self-audit
Recompute the cached content’s hashes (per-asset and canonical archive) against the written at populate time. A failure evicts the slot and falls through to a download. Tampered content is never materialized.
Lockfile comparison
When the lockfile pins this version, the audited integrity must equal the locked integrity (string comparison). A mismatch is a hard failure — the highest-priority check. Not a silent re-download.
content_integrity and, when the lockfile pins this version, against the locked integrity, before the verified content populates the cache.
Manifest-write policy
The manifest value written for an addition depends on the specifier shape:| Specifier | Manifest value | Lockfile value |
|---|---|---|
| Bare name (no version) | Pinned to resolved exact (1.2.3) | Resolved exact + integrity |
Explicit (1.2.3, 0.*, *, latest) | Written verbatim | Resolved exact + integrity |
| Reproduction (not an addition) | Unchanged | Unchanged or re-resolved |
Machine-local install receipt
Receipts are per-project, machine-local records stored outside the project’s version-controlled tree ($FACET_DIR/receipts/). They track what has been materialized so drift removal can clean up correctly — even when the lockfile no longer mentions the facet.
Per-project isolation
Each project has its own receipt, identified by a hash of the project’s canonical path. Two projects never share a receipt.
Self-identifying
The receipt embeds the canonical path. A mismatch on load fails closed — treated as absent and re-bootstrapped from the lockfile.
Untrusted input
Asset entries with crafted names (path traversal, backslashes) are reported and skipped individually — the rest of the receipt still loads and is processed. A corrupted entry can cause a skipped cleanup, never a deletion outside the project’s adapter trees.
Contained deletion
Deletion goes through adapters using validated semantic asset tuples — never raw filesystem paths taken from the receipt.
Drift removal
Anything the receipt records as materialized but the desired set no longer wants is removed. The comparison is against the receipt, not the on-disk lockfile — this is what makes orphan-on-pull recoverable. The asset tuples to delete come from the receipt, so removal needs no cache and no network.Transactional tri-write
On success, three files are written together: the manifest, the lockfile, and the receipt. A failure anywhere leaves all three exactly as they were.A failed operation leaves the project exactly as it was — no snapshot/restore needed.
Frozen lockfile
Frozen mode (--frozen-lockfile) treats the lockfile as the complete, authoritative source of truth.
| Behavior | Frozen mode |
|---|---|
| Additions or removals | Rejected |
| Version resolution | Forbidden — absent/stale entry fails immediately |
| Integrity confirmation | Never fires (no entry creation) |
| Archive resolution | Allowed (downloading locked content is reproduction) |
| Bidirectional consistency | Required (manifest ↔ lockfile) |
| Integrity before materialization | Required for every facet (including local sources) |
| Drift removal | Runs; receipt is rewritten |
| Lockfile write | Never |
| Manifest write | Never |