Skip to main content
The install pipeline splits into two phases: plan (pure routing) and commit (all resolution, integrity verification, materialization, and writes). 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 fieldContentsSource command
AdditionsUser’s specifier verbatim (1.2.3, 0.*, *, latest, or bare name)facet add
RemovalsBare facet namesfacet remove
(empty)No additions, no removalsfacet install
The specifier is passed through untouched. Plan does not decide what version a wildcard or 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-disk facets.json, facets.lock, the , and the delta from plan.
1

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.
2

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.
3

Drift removal

Facets the receipt records as materialized but the desired set no longer wants are removed offline.
4

Transactional tri-write

On success, facets.json, facets.lock, and the install receipt are written together. A failure anywhere rolls back all materialization via the journal and leaves all three files unchanged.

Three registry interactions

Commit makes three distinct kinds of registry interaction, gated independently:
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.
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.
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.
These compose independently: a step may need any combination, or none. The cache short-circuits archive resolution; a satisfying lockfile entry short-circuits integrity confirmation.
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:
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.
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.
In both cases, once an exact version is in hand, the cache is checked first and archive resolution happens only on a miss.

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.
1

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.
2

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.
3

Integrity confirmation

When no satisfying lockfile entry exists, the audited integrity must equal the registry’s published content_integrity. An unreachable registry fails the commit: a lockfile entry is never written on trust.
On a miss, the downloaded content’s canonical fingerprint is genuinely recomputed from the extracted bytes (per-asset hashes plus the canonical-archive hash) — never taken from the build manifest’s self-declared claim. The recomputed value is verified against the registry’s published 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:
SpecifierManifest valueLockfile value
Bare name (no version)Pinned to resolved exact (1.2.3)Resolved exact + integrity
Explicit (1.2.3, 0.*, *, latest)Written verbatimResolved exact + integrity
Reproduction (not an addition)UnchangedUnchanged 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.
A frozen commit with a non-empty delta (any addition or removal) is rejected immediately. Only a plain facet install can run frozen.
BehaviorFrozen mode
Additions or removalsRejected
Version resolutionForbidden — absent/stale entry fails immediately
Integrity confirmationNever fires (no entry creation)
Archive resolutionAllowed (downloading locked content is reproduction)
Bidirectional consistencyRequired (manifest ↔ lockfile)
Integrity before materializationRequired for every facet (including local sources)
Drift removalRuns; receipt is rewritten
Lockfile writeNever
Manifest writeNever